0

It's very well possible this isn't meant to work, but I'm trying to learn Vue and fail to make a nested form. The usecase is that I want my user to order various amounts of different types of tickets;

e.g. 2 times "standard" and 4 times "premium". Keep in mind I have no idea how many positions / configurations the user wants to have. Might be one, might be 100.

---

The way I broke it down was into Base components for a number and a single selection:

<script setup lang="ts">

const model = defineModel()

type Option = { id: number, name: string }
defineProps<{ options: Option[] }>()

</script>

<template>
<p>Single Select (dropdown)</p>
<select v-model="model">
    <option disabled value="">please select something</option>
    <option
        v-for="selectionOption in options"
        :key="selectionOption.id"
        :value="selectionOption.name">
        {{ selectionOption.id }}
    </option>
</select>
</template>

Then I planned to put both of those in a form for "configuration" of a single order position and have two-way binding to a reactive that's like this:

type positionConfiguration = { amount: number; ticketTier: { id: number, name: string } }.

And then I thought I could just have a max form where I have a reactive / ref configurations of type

type orderConfiguration = { positionConfigurations: positionConfiguration[] }

that is an array of configurations, and like, call it in <OrderConfiguration /> as such:

<Configuration :v-for="configuration in configurations" :configuration="configuration" :model-value="configurations" />

However, I'm failing spectacularly and anything that comes up from googling "nested form components" and the like in Vue doesn't seem to hit the mark. Is this just something I have to do with manuel emits, and not possible with v-model?

3
  • What exactly is the error, you get? Or it runs, but shows nothing? Is seems to me, that you are trying to make to complex structure. Possibly you should reduce number of layers and don't extract select into separate component. This will make you code easier and maybe you will find what's the problem. Commented 2 days ago
  • Also a curious point is why are you passing both configuration and an array of all configurations as props to your component? Supposedly, this component should deal with one configuration and it doesn't need that array. And for writing you can use that object, which you've passed into :configuration prop, and the changes will appear in the array, as if configuration is an object, it is passed by link. Commented 2 days ago
  • @titovtima first of all, thanks for the reply! I don't get errors per se, but it's either not responding or claiming "hey you added an event listener and I don't like that". Thanks for confirming my suspicion that extracting the select might be wrong (I thought, well, think in reusable elements as much as possible). As for passing both the configuration and all configurations: I'm passing the array of configurations into the parent form and then render a single configuration form via v-for in the child form, because I thought this would be correct. I'm gonna try to make it as you describe Commented 2 days ago

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.