3
\$\begingroup\$

I have a form in which I am collecting data from two applicants. In order to minimize HTML duplication I built an applicant Vue component that generates each applicant from a template.

Each applicant has an applicant data object. My question is regarding the best practice for data binding these applicant objects inside the component back to the root Vue instance. Here is my working code:

Vue Root Instance

var app = new Vue({
   el: '#app',
   data: {
      form: {
         borrower: {
            firstname: 'John',
            middlenames: '',
            surname: 'Rambo'
         },
         coborrower: {
            firstname: 'Homer',
            middlenames: '',
            surname: 'Simpson'
         }
      } 
   }
})

Template

<script type="x-template" id="applicant-container">    
   <div class="row">
      <div class="form-group col-md-4">
         <label>Firstname</label>
         <input type="text" class="form-control" v-model="applicant.firstname" />
      </div>
      <div class="form-group col-md-4">
         <label>Middlename/s</label>
         <input type="text" class="form-control" v-model="applicant.middlenames" />
      </div>
      <div class="form-group col-md-4">
         <label>Surname</label>
         <input type="text" class="form-control" v-model="applicant.surname" />
      </div>
   </div> 
</script>

Component

The applicant prop gives me access to the applicant objects in the root Vue instance

Vue.component('applicant', {
   template: '#applicant-container',
   props: ['applicant'],
})

Component Instance

I pass each applicant object to the component instance through the applicant prop.

<applicant :applicant="form.borrower"></applicant>
<applicant :applicant="form.coborrower"></applicant>

This code works perfectly. Any changes that are made using the inputs generated from the components is reflected when I output the applicant objects e.g

{{ form.borrower }}
{{ form.coborrower }}

The thing that I'm unsure about is this approach best practice? The Vue's docs state:

...the parent may need to pass data down to the child, and the child may need to inform the parent of something that happened in the child. However, it is also very important to keep the parent and the child as decoupled as possible via a clearly-defined interface

Every component instance has its own isolated scope. This means you cannot (and should not) directly reference parent data in a child component’s template1

Since I'm passing the parent applicant object to the component and am modifying it directly through that passed in reference using the v-model directive am I not directly modifying the parent object so therefore keeping the data coupled as the docs recommended against.


1https://vuejs.org/v2/guide/components.html#Composing-Components

\$\endgroup\$
1
\$\begingroup\$

General feedback

The use of the <script> tag as a template is nice and I have aimed to use that more frequently in my own projects when not using a larger template library.

Doesn't it seem a bit redundant to have a component called applicant, which has a property called applicant? Perhaps a more generic name for those properties would be more appropriate - e.g. attributes. However, the advice below, which responds to your question, suggests binding the applicant to the model attribute instead of a property.

Main question

Since I'm passing the parent applicant object to the component and am modifying it directly through that passed in reference using the v-model directive am I not directly modifying the parent object so therefore keeping the data coupled as the docs recommended against.

Your question appears nearly identical to this SO question, though in your case you have working code and are passing an object instead of a single value. To follow the guidelines in the documentation, perhaps altering the code as described in the answer by asemahle would be an optimal route.

  1. Change the property from applicant to value.

    props: ['value'],
    

    And in the markup, use v-model instead of the property for applicant:

    <applicant v-model="form.borrower"></applicant>
    <applicant v-model="form.coborrower"></applicant>
    
  2. Add applicant via the the local data properties:

    data: function() {
        return {
            applicant: {}
        }
    },
    
  3. When the component is created, set the applicant to the value property:

    created: function() {
        this.applicant = this.value;
    }
    
  4. Watch for changes on the local data property applicant and emit an event in response to that change

    watch: {
        'applicant': function() {
            // When the internal value changes, we $emit an event. Because this event is 
            // named 'input', v-model will automatically update the parent value
            this.$emit('applicant', this.applicant);
        }
    }
    

Below is a demonstration:

Vue.component('applicant', {
  template: '#applicant-container',
  props: ['value'],
  watch: {
    'applicant': function(valueChanged) {
      // When the internal value changes, we $emit an event. Because this event is 
      // named 'input', v-model will automatically update the parent value
      this.$emit('applicant', this.applicant);
    }
  },
  data: function() {
    return {
      applicant: {}
    }
  },
  created: function() {
    this.applicant = this.value;
  }
})

// Root Vue instance
const app = new Vue({
  el: '#app',
  data: {
    form: {
      borrower: {
        firstname: 'John',
        middlenames: '',
        surname: 'Rambo'
      },
      coborrower: {
        firstname: 'Homer',
        middlenames: '',
        surname: 'Simpson'
      }
    }
  }
});
.row {
  border: 2px solid #5CD7FF;
  margin: 2px;
  padding: 4px;
}
.label {
  font-style: italic;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.js"></script>

<div id="app">
  <h3>Borrower:</h3> in parent:{{ form.borrower }} 
  <applicant v-model="form.borrower"></applicant>
  <h3>Co-borrower:</h3> in parent:{{ form.coborrower }}
  <applicant v-model="form.coborrower"></applicant>
</div>
<script type="x-template" id="applicant-container">
  <div class="row"><span class="label">Applicant Template:</span>
    <div class="form-group col-md-4">
      <label>Firstname</label>
      <input type="text" class="form-control" v-model="applicant.firstname" />
    </div>
    <div class="form-group col-md-4">
      <label>Middlename/s</label>
      <input type="text" class="form-control" v-model="applicant.middlenames" />
    </div>
    <div class="form-group col-md-4">
      <label>Surname</label>
      <input type="text" class="form-control" v-model="applicant.surname" />
    </div>
  </div>
</script>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.