Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional props in Vue2

I have a use case where I may need to pass an object props as props to a child component.

Initially, I had a form and a table contained within a component. This form would take input, an asynchronous request would be performed and the table would be rendered for the user to make a selection. The user could then hit a button and have the table hidden and the form brought back up so parameters could be re-entered. Since the content of the form was dependent on the state of its parent, the last search parameters were still in the form.

The issue arose when I refactored the component to make both the form and the table sub-components of the parent. Now the form would $emit the event to its parent, which would perform the asynchronous operation and pass the result as props to the table. This worked fine, but when the user hits the "return to form" button, the form is re-rendered, thus resetting its state to the initial value.

I tried storing the content of the form in the parent and passing it back to the form as props but that led to the problem of initially setting the values. I didn't want to mutate the props directly, so I tried this approach:

FormContainer.vue

<template>
  <div v-if="formShown">
    <form-component :initialValues="formValues" @formSubmitted="displayResults"></form-component>
  </div>
  <div v-if="tableShown">
    <table-component :results="fetchedResults" @returnToForm="returnToForm"></table-component>
  </div>
</template>
<script>
  export default {
    data(){
      return{
        formShown: true,
        tableShown: false,
        formValues:{
          address1: '',
          address2: '',
          address3: '',
          country: ''
        },
        fetchedResults: []
      }
    },
    methods:{
     async displayResults(){
        this.fetchedResults = await someAsynchronousCall();
        this.formShown = false;
        this.tableShown = true;
     },
     returnToForm(){
       this.tableShown = false;
       this.formShown = true;
     }
    }
  }
</script>

FormComponent.vue

<template>
  <!--Some form fields here, bound to data(){}, ommitted for brevity-->
</template>
<script>
  export default{
    props:['initialValues'],
    data(){
      return{
        //Original structure
        /*selectedAddress:{
          address1: '',
          address2: '',
          address3: '',
          country: ''
        }*/
        selectedAddress: JSON.parse(JSON.stringify(this.initialValues)) //Object.assign({}, this.initialValues) //tried both of these for deep copy
      }
    }
  }
</script>

The problem with this was that when the component was initially created, it is getting passed an empty object (or rather an object with only empty properties, I assume they amount to the same thing), which means my data properties are getting initialized as undefined.

I then tried using a ternary to initialize the data object with the values in the props or an empty string, like this:

data(){
  return{
    selectedAddress: {
      address1: this.initialValues.address1 ? this.initialValues.address1 :'',
      address2: this.initialValues.address2 ? this.initialValues.address2 :'',
      address3: this.initialValues.address3 ? this.initialValues.address3 :'',
      country:this.initialValues.country ? this.initialValues.country :''
    }

  }
},

But this throws errors saying that selectedAddress is not defined on the instance but is referenced during render. I'm guessing this means that using a ternary to initialize props is wrong.

I then tried to check for props in the mounted(){} lifecycle hook and set the data properties there, like this:

mounted(){
      if(!this.initialValues || _.isEmpty(this.initialValues)){
        Logger.info(`no props passed, no setup needed`);
        return;
      }
      if(this.initalValues.address1){
        Logger.info(`setting address 1`);
        this.selectedAddress.address1 = this.initalValues.address1;
      }
      if(this.initialValues.address2){
        Logger.info(`setting address 2`);
        this.selectedAddress.address2 = this.initalValues.address2;
      }
      if(this.initialValues.address3){
        Logger.info(`setting address 3`);
        this.selectedAddress.address3 = this.initalValues.address2;
      }
      if(this.initialValues.country){
        Logger.info(`setting country`);
        this.selectedAddress.country = this.initalValues.country;
      }
    }

This works for the first run of the form, but this.initialValues is always undefined when the component is mounted. I have inspected the component state and found that initialValues does exist, just not during the mounted life cycle hook. This approach felt 'hacky' anyway.

I'm not really sure where to go from here. I could commit the form data to the store and get it again if the form is remounted, but I don't feel like committing something that should only exist while its parent is mounted is the right approach.

Can anyone guide me to a more Vue way of achieving this?

like image 629
JoeWemyss Avatar asked Sep 23 '17 07:09

JoeWemyss


1 Answers

I think you should be able to sort this out by shifting the code in FormComponent from data to computed.
Computed properties are reactive, so when the initialValues prop eventually gets some data the form should reflect it.
Object.assign will work if initialValues always has the address1, address2, etc structure (even if they are null initially). If not (i.e initialValues initially = {}, you will have to do the property-by-property check.

<template>
  <!--Some form fields here, bound to data(){}, ommitted for brevity-->
  <div>selectedAddress.address1</div>
  <div>selectedAddress.address2</div>
</template>
<script>
  export default{
    props:['initialValues'],
    data(){
    },
    computed: {
      selectedAddress() {
        return Object.assign({}, this.initialValues)
      }
    }
  }
</script>

To keep the form values for next round you need to pass selectedAddress up through the emit and apply them to the parent formValues.

An alternative I quite like is Vuex, where the form gets it's initial value from the store and posts back to the store via an action, which kicks off the async work and updates the store with results. Essentially your displayResults method becomes the action, but you don't need async..await.

Vuex formalizes the data flow pattern so you can more easily reason about it and debug things. Essentially, you don't have to worry about passing data in to props and out through emits.

Another thought, you can avoid resetting the FormComponent values by using v-show instead of v-if in FormContainer. That should retain the form div (and it's state) but hide it. You then may not need to have formValues on parent at all.

<template>
  <div v-show="formShown">
    <form-component :initialValues="formValues" @formSubmitted="displayResults"></form-component>
  </div>
  <div v-show="tableShown">
    <table-component :results="fetchedResults" @returnToForm="returnToForm"></table-component>
  </div>
</template>
like image 93
Richard Matsen Avatar answered Sep 30 '22 15:09

Richard Matsen