Search code examples
vue-componentvuejs3vue-composition-apivue-reactivity

How to call a function inside a child component in `Vue3` created through a `v-for` loop?


I am currently building a form builder with vue3 composition API. The user can add in different types of inputs like text, radio buttons etc into the form before saving the form. The saved form will then render with the appropriate HTML inputs. The user can edit the name of the question, eg Company Name <HTML textInput.

Currently, when the user adds an input type eg,text, the type is saved into an ordered array. I run a v-for through the ordered array and creating a custom component formComponent, passing in the type.

My formComponent renders out a basic text input for the user to edit the name of the question, and a place holder string for where the text input will be displayed. My issue is in trying to save the question text from the parent.

    <div v-if="type=='text'">
        <input type="text" placeholder="Key in title"/> 
        <span>Input field here</span>
    </div>

I have an exportForm button in the parent file that when pressed should ideally return an ordered array of toString representations of all child components. I have tried playing with $emit but I have issue triggering the $emit on all child components from the parent; if I understand, $emit was designed for a parent component to listen to child events.

I have also tried using $refs in the forLoop. However, when I log the $refs they give me the div elements.

<div v-for="item in formItems" ref="formComponents">
   <FormComponent :type="item" />
</div>

The ideal solution would be to define a method toString() inside each of the child components and have a forLoop running through the array of components to call toString() and append it to a string but I am unable to do that.

Any suggestions will be greatly appreciated!


Solution

  • At first:

    You don't really need to access the child components, to get their values. You can bind them dynamically on your data. I would prefer this way, since it is more Vue conform way to work with reactive data.

    But I have also implemented the other way you wanted to achieve, with accessing the child component's methods getValue().

    I would not suggest to use toString() since it can be confused with internal JS toString() function.

    In short:

    • the wrapping <div> is not necessary
    • the refs should be applied to the <FormComponents> (see Refs inside v-for)
    • this.$refs.formComponents returns the Array of your components
    • FormComponent is used here as <form-components> (see DOM Template Parsing Caveats)
    • The values are two-way bound with Component v-model

    Here is the working playground with the both ways of achieving your goal. Pay attention how the values are automatically changing in the FormItems data array.

    const { createApp } = Vue;
    
    const FormComponent = {
      props: ['type', 'modelValue'],
      emits: ['update:modelValue'],
      template: '#form-component',
      data() { 
        return { value: this.modelValue }
      },
      methods: {
        getValue() {
          return this.value;
        }
      }
    }
    
    const App = { 
      components: { FormComponent },
      data() {
        return {
          formItems: [
            { type: 'text', value: null },
            { type: 'checkbox', value: false }
          ]
        }  
      },
      methods: {
        getAllValues() {
          let components = this.$refs.formComponents;
          let values = [];
          for(var i = 0; i < components.length; i++) {
            values.push(components[i].getValue())
          }
          console.log(`values: ${values}`);
        }
      }
    }
    const app = createApp(App)
    app.mount('#app')
    #app { line-height: 2; }
    [v-cloak] { display: none; }
    label { font-weight: bold; }
    th, td { padding: 0px 8px 0px 8px; }
    <div id="app">
      <label>FormItems:</label><br/>
      <table border=1>
        <thead><tr><th>#</th><th>Item Type:</th><th>Item Value</th></tr></thead>
        <tbody><tr v-for="(item, index) in formItems" :key="index">
          <td>{{index}}</td><td>{{item.type}}</td><td>{{item.value}}</td>
          </tr></tbody>
      </table>  
      <hr/>
      <label>FormComponents:</label>
      <form-component 
        v-for="(item, index) in formItems" 
        :type="item.type" v-model="item.value" :key="index" ref="formComponents">
      </form-component>
      <button type="button" @click="getAllValues">Get all values</button>
    </div>
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
    <script type="text/x-template" id="form-component">
      <div>
        <label>type:</label> {{type}}, 
        <label>value:</label> <input :type='type' v-model="value" @input="$emit('update:modelValue', this.type=='checkbox' ? $event.target.checked : $event.target.value)" />
      </div>
    </script>