nicholasdrzewiecki
nicholasdrzewiecki

Reputation: 121

VueJS accessing data from component in root Vue instance

I'm trying to access a component's data in my root Vue instance. What I'm trying to accomplish is, you click on a button and a set of inputs appear. Each set of inputs is its own object in localStorage with the keys id, bags, and amount. When I update a set of inputs, their values should update in localStorage. I'm having trouble figuring out how to access my component's data for bags and amount. What I think I'm supposed to be using is props because that's what I used in React. However, I'm not too comfortable with using them yet in Vue.

Here is what I have so far:

var STORAGE_KEY = "orders";

var orderStorage = {
  fetch: function() {
    var orders = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
    orders.forEach(function(order, index) {
      order.id = index;
    });
    orderStorage.uid = orders.length;
    return orders;
  },
  save: function(orders) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(orders));
  }
};

Component

Vue.component('order', {
  template: `
    <div>
      <select v-model="selected">
        <option v-for="option in options" v-bind:value="option.value">
          {{ option.text }}
        </option>
      </select>
      <input class="input" id="bags" v-model="bags" type="number" placeholder="Bags">
      <input class="input" id="amount" v-model="amount" type="number" placeholder="Amount">
      <button @click="$emit('remove')">Remove</button>
    </div>
  `,
  props: {
    bags: this.bags, // I must be doing this wrong
    amount: this.amount
  },
  data() {
    return {
      bags: null,
      amount: null,
      selected: null,
      options: [{
        text: "Product (25kg)",
        value: 25
      }, {
        text: "Product (50kg)",
        value: 50
      }, {
        text: "Product (75kg)",
        value: 75
      }]
    }
  },
  watch: {
    bags(newValue) {
      this.amount = newValue * this.selected;
    },
    amount(newValue) {
      this.bags = newValue / this.selected;
    }
  }
});

Root Vue Instance

new Vue({
  el: '#app',
  data: {
    orders: orderStorage.fetch(),
    bags: null,
    amount: null,
  },
  watch: {
    orders: {
      handler: function(orders) {
        orderStorage.save(orders);
      },
      deep: true
    }
  },
  methods: {
    addProduct: function() {
      this.orders.push({
        id: orderStorage.uid++,
        bags: this.bags, // component.bags ?
        amount: this.amount// component.amount?
      });
      console.log(localStorage);
    }
  }
});

HTML

<div id="app">
  <button @click="addProduct">Add</button>

  <div class="orders">
    <order v-for="(order, index) in orders" :id="index" :key="order" @remove="orders.splice(index, 1)" :bags="bags" :amount="amount"></order>
  </div>
</div>

Example: https://codepen.io/anon/pen/YVwxpz?editors=1010

Upvotes: 3

Views: 5311

Answers (2)

Roy J
Roy J

Reputation: 43881

If props are confusing you, be sure to read about Composing Components, which talks about the "props down, events up" convention.

Props are data that is controlled from outside the component. The component should not make modifications to its props (as you are doing in your watch, and as you are implicitly doing by using them with v-model).

You probably want to make component data items that you initialize from the props (give them names differerent from the props names!) You can then manipulate those values.

Since you want the parent to be aware of changes to the values, you should emit events when changes happen. The parent can catch those events and take appropriate action, like saving them to the order object and to localStorage.

Upvotes: 1

Bert
Bert

Reputation: 82459

Typically, you do not want to reach into a component to get its data. You want the component to $emit it. Vue is, generally, props down, events up.

I've modified your order component to support v-model. This allows the changes to the component to be reflected in the parent automatically.

Vue.component('order', {
  props:["value"],
  template: `
    <div>
      <select v-model="order.value">
        <option v-for="option in options" v-bind:value="option.value">
          {{ option.text }}
        </option>
      </select>
      <input class="input" id="bags" v-model="order.bags" type="number" @input="onBags" placeholder="Bags">
      <input class="input" id="amount" v-model="order.volume" type="number" @input="onVolume" placeholder="Amount">
      <button @click="$emit('remove')">Remove</button>
    </div>
  `,
  data() {
    return {
      options: [{
        text: "Product (25kg)",
        value: 25
      }, {
        text: "Product (50kg)",
        value: 50
      }, {
        text: "Product (75kg)",
        value: 75
      }],
      order: this.value
    }
  },
  methods: {
    onBags() {
      this.order.volume = this.order.bags * this.order.value;
      this.$emit("input", this.order)
    },
    onVolume() {
      this.order.bags = this.order.volume / this.order.value;
      this.$emit("input", this.order)
    }
  }
})

new Vue({
  el: '#app',
  data: {
    orders: orderStorage.fetch(),
  },
  watch: {
    orders: {
      handler: orders => orderStorage.save(orders),
      deep: true
    }
  },
  methods: {
    addProduct: function(order) {
      this.orders.push({
        id: orderStorage.uid++,
        bags: 0,
        volume: 0,
        value: 25
      });
    }
  }
});

Doing this, you don't have to pass bags and amount, you just pass the order. Then, when the order changes, the modified order is emitted.

A working example is here.

Additionally, I added the value property to your order objects. If you don't save that, then you can't properly calculate the changes to bags and volume.

Upvotes: 2

Related Questions