1

I am making my first Vue 2 component (within a Laravel 5.3 app) but run into a instance/scope problem which I have spent hours trying to solve, but just cannot get it to work!

My component generates a list of orders and within each order there is a list of users (advisors). I want to be able to select a user for an order and then assign the user to the order.

The latest version of my component looks like this;

Vue.component('unassigned-orders', {

    template: `           
        <table id="assignOrder" class="table table-hover">
            <thead>
                // table headers
            </thead>

            <tr v-for="(unassignedOrder, key) in unassignedOrders" 
                                                 :key="unassignedOrder.order_id">
                <td>{{ unassignedOrder.order_id }}</td>
                <td>{{ unassignedOrder.first_name }} {{ unassignedOrder.last_name }}</td>
                <td>{{ unassignedOrder.order_status }}</td>
                <td><select @change="assignAdvisor()">
                    <option  v-for="advisor in advisors" :value="advisor.id" >
                {{ advisor.first_name }} {{ advisor.last_name }}
                    </option>
                </select></td>
                <td><a class="btn btn-default">
                           Set Advisor {{ unassignedOrder.assignTo }}</a></td>
            </tr>
        </table>`,

    data: function() {
        return {
            unassignedOrders: [{'assignTo' : ''}], 
            advisors: ''
        }
    },

    methods: {
        assignAdvisor: function() {
            this.assignTo = event.target.value;
        }
    },


    mounted() {
        axios.get('url').then(response => this.advisors = response.data);
        axios.get('url').then(response => this.unassignedOrders = response.data);
    }
});

I do not get any errors, but the functionality does not work (I just need to update var assignTo to the relevant order, I am sure I'll be able to figure the rest out).

I am sure I am missing something really simple, anyone got any pointers?

Thanks

4
  • Took a quick look at your sample code 👀 What happens if you add the property assignTo in the object returned by the component's data method? You can set the value to null or an empty string. Commented Feb 16, 2017 at 10:46
  • I had tried that previously, i.e. having assignTo: '', on its own line within the data function as opposed to nested within unassignedOrders, but it had the same result, i.e. nothing.. Commented Feb 16, 2017 at 10:57
  • Ahh okai. I'm trying to get a better understanding on what you are trying to achieve. Here's my interpretation – correct me if I'm wrong. Your component renders a representation of a list of items called unassignedOrders. When the select in an unassigned order emits a change event it is supposed to set the assignTo property of that order. Does that sound right? Commented Feb 16, 2017 at 11:04
  • @wing yep, you got it! Then when the assignTo button is clicked I will run an AJAX to update the database (to add the selected user to the order). I just can't get the value into assignTo... Commented Feb 16, 2017 at 11:37

2 Answers 2

1

After some discussion in your question's comments we were able to break down your problem to this:

  • You are rendering a list of items. This list is an array of objects.
  • There is some functionality in the rendered item that allows you to change the corresponding object in the array.

But how is it possible to update the corresponding object in the array?

Below is a snippet demonstrating a solution.

const items = {
  template: `
    <div>
      <ul>
        <li v-for="(item, index) in proxyItems">
          {{ item.name }} - {{ item.option }}
          <select @change="change(index, $event.target.value)">
            <option>No option</option>
            <option>Option 1</option>
            <option>Option 2</option>
          </select>
        </li>
      <ul>
    </div>
  `,
  
  props: {
    items: {
      type: Array,
    },
  },
  
  data() {
    return {
      proxyItems: this.items,
    };
  },
  
  methods: {
    change(index, value) {
      this.proxyItems.splice(index, 1, { ...{}, ...this.proxyItems[index], ...{ option: value } });
      console.log(this.proxyItems);
    },
  },
};

new Vue({
  render(createElement) {
    const props = {
      items: [
        { name: 'Item 1', option: 'No option' },
        { name: 'Item 2', option: 'No option' },
      ],
    };

    return createElement(items, { props });
  },
}).$mount('#app');
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <div id="app"></div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
</body>
</html>

Here is the outline of the solution:

  • To update the array we need to find the index of the item you want to update then update it.
this.proxyItems.splice(index, 1, { ...{}, ...this.proxyItems[index], ...{ option: value } });
  • The component is actually passed the items as a value in the prop items, however it is not recommended that you mutate props directly

In addition, every time the parent component is updated, all props in the child component will be refreshed with the latest value. This means you should not attempt to mutate a prop inside a child component. If you do, Vue will warn you in the console.

https://v2.vuejs.org/v2/guide/components.html#One-Way-Data-Flow

Therefore we create a proxy of the items in the component's data method.

{
  props: {
    items: {
      type: Array,
    },
  },
  
  data() {
    return {
      proxyItems: this.items,
    };
  },
}

All mutations of data in items will occur in proxyItems.

Hopefully the above will help resolve your issue. However one suggestion I would make:

It may be better to break down your list items into their own components. This will mean every item will have their own scope, you won't have to proxy the items and you won't have to do complicated splicing and object creation with spread.

Below is an example of this.

const item = {
  template: `
    <li>
      {{ name }} - {{ selectedOption }}
      <select @change="change($event.target.value)">
        <option>No option</option>
        <option>Option 1</option>
        <option>Option 2</option>
      </select>
    </li>
  `,
  
  props: ['name'],
  
  data() {
    return {
      selectedOption: 'No option',
    };
  },
    
  methods: {
    change(value) {
      this.selectedOption = value;
      console.log(this.selectedOption);
    },
  },
};

const items = {
  components: {
    item,
  },

  template: `
    <div>
      <ul>
        <item v-for="item in items" :name="item.name"></item>
      <ul>
    </div>
  `,
  
  props: {
    items: {
      type: Array,
    },
  },
};

new Vue({
  render(createElement) {
    const props = {
      items: [
        { name: 'Item 1', option: 'No option' },
        { name: 'Item 2', option: 'No option' },
      ],
    };

    return createElement(items, { props });
  },
}).$mount('#app');
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <div id="app"></div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
</body>
</html>

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

1 Comment

Hi, thanks a lot for this, fantastic answer! I've been playing with your second suggestion for the last couple of hours and have it working. It took me a while to get my head around the idea of not mutating props (still not sure if I quite have it) but it works none the less. Thank you so much
0

May be unassignedOrder and advisor are complex components with their own getters and setters, so you may try to use computed properties or watchers for them.

1 Comment

I am not sure how you would apply a watcher though, as I don't believe you can have multiple instance of the same model, so you could not apply a model to the select (advisor) input. Or am I misunderstanding?

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.