If I understand correctly you want to create the following structure:
Form -> Position Component -> Select and Input component
If this structure is correct, you can do it with Vue but you need to take into account that props or model values that are not stablish are not tacked and will not update. Although objects with undefined attributes will work correctly.
Position component
<script setup lang="ts">
import type { Option, positionConfiguration } from './types';
import InputComponent from './InputComponent.vue';
import SelectComponent from './SelectComponent.vue';
import { defineProps, ref, watch } from 'vue';
// You can hardcode the options here or have them in the form container to control that the options to show are not more than the possible options and prevent duplicate of positions for the same ticked type
const props = defineProps<{
options: Option[]
}>();
// I created a default but if the default is used, the object was not passed and it won't be two-ways binding
const position = defineModel<positionConfiguration>({
default: {
amount: undefined,
ticketTier: {
id: undefined,
name: undefined
}
}
});
// Reference to get the selected option, I changed the select to send the id of the ticket and get the name from the options array
const optionSelected = ref<number>();
// I do the update of the object for the ticket variables because computer properties shouldn't have side effects, in other words, modify other variables.
watch(optionSelected, () => {
if (optionSelected.value) {
position.value.ticketTier = {
id: optionSelected.value,
name: props.options[optionSelected.value]?.name ?? ''
}
}
})
</script>
<template>
<div class="container-row">
<InputComponent :min="1" :max="10" v-model="position.amount"></InputComponent>
<SelectComponent :options="props.options" v-model="optionSelected">
</div>
</template>
Form Component
<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import RowComponent from './RowComponent.vue';
import type { Option, orderConfiguration } from './types';
//Default object with the elements to undefined to use as a dummy to pass an object to the position component in the initialization
const defaultConfiguration = {
amount: undefined,
ticketTier: {
id: undefined,
name: undefined
}
};
// Reactive element that will save the positions
const configurations: orderConfiguration = reactive({
positionConfigurations: [{ ...defaultConfiguration }]
});
//Number of positions rows shown at the moment
const configurationsShown = ref(1);
// Validation to check that other rows are selected before letting add more, this doesn't takes into consideration the unselect of previews positions
const otherConfigurationsSelected = computed(() => {
let allSelected = false;
if (configurationsShown.value == configurations.positionConfigurations.length) {
allSelected = true;
for (const configuration of configurations.positionConfigurations) {
if (configuration.amount == undefined || configuration.ticketTier.id == undefined) {
allSelected = false;
break;
}
}
}
return allSelected;
});
const idsOptionsSelected = computed(() => {
return configurations.positionConfigurations.map(element => element.ticketTier.id).filter(element => element != undefined);
});
// Function to only allow options not selected or the already selected property
function getOptions(index: number) {
const selectedOption = configurations.positionConfigurations[index];
console.log(idsOptionsSelected.value);
return options.filter((element) => element.id == selectedOption?.ticketTier.id || !idsOptionsSelected.value.includes(element.id));
}
const options: Option[] = [
{ id: 1, name: "Standar" },
{ id: 2, name: "Premium" }
];
// Computed to check that there are more options without repeating the already selected ones
const hasNewOptionsStill = computed(() => {
return options.length > configurationsShown.value;
});
// Function to add new positions, it adds a copy of the dummy object to have a reference to bind between the different components.
function handleConfigurations() {
if (otherConfigurationsSelected.value) {
// Diconstruct the element to make them independent objects, if not every instance will have the same reference an a change in one will affect the others
const newConfiguration = { ...defaultConfiguration };
console.log(newConfiguration)
configurations.positionConfigurations.push(newConfiguration);
configurationsShow.value++;
}
}
</script>
<template>
<form>
<!-- Iterates the configurationShown to show the added configurations taking into account the default unselected -->
<template v-for="(numConfigurations, index) in configurationsShown" :key="index">
<!-- It uses the index to pass as an v-model the configuration to use as binding, it is necessary to not be undefined or it won't be tracked and won't update -->
<RowComponent :options="getOptions(index)" v-model="configurations.positionConfigurations[index]"></RowComponent>
</template>
<template v-if="otherConfigurationsSelected && hasNewOptionsStill">
<button @click="handleConfigurations">New configuration</button>
</template>
</form>
<!-- Visualizator of selected configurations -->
Configurations:
{{ configurations.positionConfigurations }}
</template>
I would also like to explain why I choose reactive vs ref. In the example the configuration is an object and we alter its properties, therefore, in any moment the reference of the object change. If we had used ref, we wouldn't have the data with the changes done by the user. To use ref in configurations we would need to emit the events of change in the rows and replace the object each time something changes.
Ref in objects or arrays can be good to use when we replace the whole object with its modifications, per example, when its an obejct were we store the result of a fetch that we want to have store only the data of each call or a call that has too many data and we don't want to make the elements flick while we add each one.
One example of this would be:
type DataType = {id: number, name: string};
const data = ref<Record<number, DataType>>([]);
async funciton getData(ids: number[]) {
const response = await fetch('www.example.com', {method: 'POST', body: JSON.stringfy(ids)});
if(response.ok) {
const newData = response.json();
const copyCurrentData = {...data.value};
for(const id in newData) {
const currNewData = newData[id];
copyCurrentData[id] = currNewData;
}
// Due to the replacement of the reference, it will trigger the reactivity and we will see the new data;
data.value = copyCurrentData;
}
}
I hope this example of nested properties using more or less the structures that you provided will help you.