Appearance
TypeScript usage
Composable
With internal ref list generator
Code
<script setup lang="ts">
import { useIncrementalList } from "../index";
import ResultBox from "../ResultBox.vue";
interface Example {
value: string;
}
const model = (): Example => ({ value: "" });
const list = useIncrementalList<Example>({
model,
initialValue: [{ value: "Hello" }, { value: "World" }],
});
const { shownItems, clearEmpty } = list;
</script>
<template>
<ResultBox>
<div v-for="(item, index) in shownItems" :key="index">
<label>
<input v-model="item.value" @blur="clearEmpty()" />
</label>
</div>
<template #debug="{ format }">{{ format(list) }}</template>
</ResultBox>
</template>
Demo
Debug
{
"shownItems": [
{
"value": "Hello"
},
{
"value": "World"
},
{
"value": ""
}
],
"clearEmpty": "[Function: clearEmpty]",
"empty": [],
"newItem": {
"value": ""
},
"removeItems": "[Function: removeItems]",
"items": [
{
"value": "Hello"
},
{
"value": "World"
}
]
}With externally created ref list
Code
<script setup lang="ts">
import { ref } from "vue";
import { useIncrementalList } from "../index";
import ResultBox from "../ResultBox.vue";
interface Example {
value: string;
}
const model = (): Example => ({ value: "" });
const items = ref<Example[]>([{ value: "Hello" }, { value: "World" }]);
const list = useIncrementalList<Example>({
model,
items,
});
const { shownItems, clearEmpty } = list;
</script>
<template>
<ResultBox>
<div v-for="(item, index) in shownItems" :key="index">
<label>
<input v-model="item.value" @blur="clearEmpty()" />
</label>
</div>
<template #debug="{ format }">{{ format({ items, list }) }}</template>
</ResultBox>
</template>
Demo
Debug
{
"items": [
{
"value": "Hello"
},
{
"value": "World"
}
],
"list": {
"shownItems": [
{
"value": "Hello"
},
{
"value": "World"
},
{
"value": ""
}
],
"empty": [],
"newItem": {
"value": ""
}
}
}Component
Simple usage
Code
<script setup lang="ts">
import { ref } from "vue";
import { useIncrementalListComponent } from "../index";
import ResultBox from "../ResultBox.vue";
interface Example {
value: string;
}
const model = (): Example => ({ value: "" });
const value = ref<Example[]>([]);
const ExampleList = useIncrementalListComponent<Example>(model);
</script>
<template>
<ResultBox>
<ExampleList v-model="value">
<template #item="{ item, isNew, remove, clearEmpty }">
<div>
<label>
<input v-model="item.value" @blur="clearEmpty()" />
</label>
<button tabindex="-1" v-if="!isNew" @click="remove()">Remove</button>
</div>
</template>
</ExampleList>
<template #debug="{ format }">{{ format(value) }}</template>
</ResultBox>
</template>
Demo
Debug
{}Using different template for new items
Code
<script setup lang="ts">
import { ref } from "vue";
import { useIncrementalListComponent } from "../index";
import ResultBox from "../ResultBox.vue";
interface Example {
name: string;
nickname: string;
type: "Person" | "Company";
}
const model = (): Example => ({ file: "", label: "" });
const value = ref<Example[]>([]);
const ExampleList = useIncrementalListComponent<Example>(model);
const addFile = (item: Example, event: Event) => {
console.log(event);
const { files } = event.target as HTMLInputElement;
if (!files.length) return;
const [file] = files;
Object.assign(item, {
file: file.name,
label: file.name,
});
};
</script>
<template>
<ResultBox>
<ExampleList v-model="value">
<template #item="{ item, isNew, remove }">
<div>
<label>{{ item.type }} name: <input v-model="item.name" /></label>
<label v-if="item.type === 'Person'">
Nickname: <input v-model="item.nickname" />
</label>
<button tabindex="-1" v-if="!isNew" @click="remove()">Remove</button>
</div>
</template>
<template #newItem="{ item }">
<div>
<label>
Add:
<select placeholder="select" v-model="item.type">
<option disabled value="">Select</option>
<option value="Person">Person</option>
<option value="Company">Company</option>
</select>
</label>
</div>
</template>
</ExampleList>
<template #debug="{ format }">{{ format({ value }) }}</template>
</ResultBox>
</template>
Demo
Debug
{
"value": []
}Nested lists
Code
<script setup lang="ts">
import { ref, nextTick } from "vue";
import { useIncrementalListComponent } from "../index";
import ResultBox from "../ResultBox.vue";
type PetType = "cat" | "dog" | "bird";
interface Pet {
name: string;
type: PetType | null;
}
interface Person {
name: string;
pets: Pet[];
}
const petModel = (): Pet => ({ name: "", type: null });
const personModel = (): Person => ({ name: "", pets: [] });
interface PetTypeOption {
value: PetType;
label: string;
}
const petTypes: PetTypeOption[] = [
{ value: "cat", label: "Gato" },
{ value: "dog", label: "Cachorro" },
{ value: "bird", label: "Ave" },
];
const people = ref<Person[]>([
{
name: "Magali",
pets: [{ name: "Mingau", type: "cat" }],
},
{
name: "Cebolinha",
pets: [{ name: "Floquinho", type: "dog" }],
},
{
name: "Mônica",
pets: [{ name: "Monicão", type: "dog" }],
},
]);
const PersonIncrementalList = useIncrementalListComponent<Person>(personModel);
const PetIncrementalList = useIncrementalListComponent<Pet>(petModel);
</script>
<template>
<ResultBox>
<PersonIncrementalList v-model="people">
<template #item="personRow">
<div class="row">
<div class="col">
<div>
<label>
Nome:
<input
v-model="personRow.item.name"
@blur="personRow.clearEmpty()"
/>
</label>
</div>
<div class="text-right" v-if="!personRow.isNew">
<button tabindex="-1" @click="personRow.remove()">Remover</button>
</div>
</div>
<template v-if="!personRow.isNew">
<div class="col pets">
<PetIncrementalList
v-model="personRow.item.pets"
@itemsRemoved="personRow.clearEmpty()"
>
<template #item="petRow">
<div>
<label>
Pet:
<input
v-model="petRow.item.name"
@blur="petRow.clearEmpty()"
/>
</label>
<label>
Type:
<select v-model="petRow.item.type">
<option
v-for="{ value, label } in petTypes"
:key="value"
:value="value"
>
{{ label }}
</option>
</select>
</label>
<button
tabindex="-1"
@click="
async () => {
petRow.remove();
await nextTick();
personRow.clearEmpty();
}
"
>
Remover
</button>
</div>
</template>
<template #newItem="petRow">
<div>
<label>Pet: <input v-model="petRow.item.name" /></label>
</div>
</template>
</PetIncrementalList>
</div>
</template>
</div>
<hr />
</template>
</PersonIncrementalList>
<template #debug="{ format }">{{ format({ people }) }}</template>
</ResultBox>
</template>
<style scoped>
.row {
padding: 8px 0;
display: flex;
flex-wrap: nowrap;
}
.col.pets {
flex: 1;
padding-left: 8px;
}
.text-right {
text-align: right;
padding-right: 16px;
}
label {
display: inline-block;
padding: 8px;
padding-right: 16px;
}
input,
select {
display: block;
}
hr.vl {
flex: 0 1 1px;
}
.index {
display: inline-block;
font-weight: bold;
white-space: nowrap;
width: 48px;
}
</style>
Demo
Debug
{
"people": [
{
"name": "Magali",
"pets": [
{
"name": "Mingau",
"type": "cat"
}
]
},
{
"name": "Cebolinha",
"pets": [
{
"name": "Floquinho",
"type": "dog"
}
]
},
{
"name": "Mônica",
"pets": [
{
"name": "Monicão",
"type": "dog"
}
]
}
]
}Extra
ResultBox
All examples above use this component to facilitate the demo exhibition. For reference, this is it's code:
Code
<script setup lang="ts">
const format = (payload: object): object =>
Object.fromEntries(
Object.entries(payload).map(([key, item]) => {
if (typeof item === "function") return [key, `[Function: ${item.name}]`];
return [key, item];
})
);
</script>
<template>
<details>
<summary>Demo</summary>
<div class="box">
<slot></slot>
<details>
<summary>Debug</summary>
<pre align="left"><slot name="debug" :format="format"></slot></pre>
</details>
</div>
</details>
</template>
<style scoped>
.box {
margin-top: 16px;
padding: 16px;
border-radius: 8px;
border: 1px solid currentColor;
}
.box:deep(> *) {
padding: 4px 0px;
}
.box:deep(label) {
padding: 8px;
margin: 8px 0px;
}
.box:deep(input),
.box:deep(select),
.box:deep(button) {
border: 1px solid currentColor;
border-radius: 4px;
padding: 0px 8px;
}
.box:deep(button) {
margin-left: 8px;
color: red;
border: 1px solid currentColor;
padding: 0px 8px;
}
</style>