How can I share a method between components in Vue.js?

Check out this simple shopping cart demo:

http://plnkr.co/edit/CHt2iNSRJAJ6OWs7xmiP?p=preview

A user can pick a veggie and a fruit, and it will be added into the cart array. The function that adds a fruit/veggie is very similar, and I want to combine it into a function that can be shared across both components.

    selectFruit: function(product){
       var cart = this.cart
       for(p in cart){
       if (cart[p]["type"] == "fruit"){
           console.log("We already got a fruit!, Let's remove " + cart[p]["name"] + " and add in " + product["name"]);
              this.cart.$remove(cart[p])
             }
            }
            console.log("Adding " + product.name + " to cart.");
            var productName = product.name
            var cartFruit = {name: product.name, type: 'fruit'}
            this.cart.push(cartFruit)
}

selectVeggie: function(product){
    var cart = this.cart
    for(p in cart){
        if (cart[p]["type"] == "veggie"){
           console.log("We already got a veggie!, Let's remove " + cart[p]["name"] + " and add in " + product["name"]);
           this.cart.$remove(cart[p])
        }
    }
    console.log("Adding " + product.name + " to cart.");
    var productName = product.name
    var cartVeggie = {name: product.name, type: 'veggie'}
    this.cart.push(cartVeggie)
}

How can I make it so I can alter this method and have it used globally? I'm using the Vue Router with this project btw, thanks for any help!


Solution 1:

I found this technique to be more simple/satisfactory, as I prefer composition over inheritance:

src/shared.js

export default {
  foo: function() { alert("foo!") }
}

src/yourcomponent.vue

<template>...</template>

<script>
  import shared from './shared'

  export default {
    created() { 
      this.foo = shared.foo // now you can call this.foo() (in your functions/template)
    }
  }
</script>

This will also allow you to write Vue-agnostic tests.

NOTE: if you need foo to run in Vue-scope replace this.foo = shared.foo with this.foo = shared.foo.bind(this)

Solution 2:

Option 1

One approach for sharing your method across components is to use a mixin. Here's a cartMixin that contains a selectProduct method:

var cartMixin = {
  methods: {
    selectProduct: function (product) {
      var cart = this.cart
      for(p in cart){
          if (cart[p]["type"] == product.type){
             console.log("We already got a "+ product.type +"!, Let's remove " + cart[p]["name"] + " and add in " + product["name"]);
             this.cart.$remove(cart[p])
          }
      }
      console.log("Adding " + product.name + " to cart.");
      var productName = product.name
      var cartProduct = {name: product.name, type: product.type}
      this.cart.push(cartProduct)
    }
  }
};

You can reference this in each component like this:

var Vegetable = Vue.extend({
    template: '#vegetable',
    mixins: [cartMixin],
    data: function(){
        return sourceOfTruth
    }
})

... and then use it in your templates like this:

<li v-for="product in food | showOnly 'fruit'" @click="selectProduct(product)">
  {{product.name}}
</li>

Here's a fork of your Plunker.

Option 2

After thinking about this some more, another option you might consider is to create a base Product component and extend that to create your Fruit and Vegetable components. You would then put your common functionality in the base component.

var Product = Vue.extend({
  data: function(){
      return sourceOfTruth
  },
  methods: {
    selectProduct: function (product) {
      var cart = this.cart
      for(p in cart){
          if (cart[p]["type"] == product.type){
             console.log("We already got a "+ product.type +"!, Let's remove " + cart[p]["name"] + " and add in " + product["name"]);
             this.cart.$remove(cart[p])
          }
      }
      console.log("Adding " + product.name + " to cart.");
      var productName = product.name
      var cartProduct = {name: product.name, type: product.type}
      this.cart.push(cartProduct)
    }
  }
})

var Vegetable = Product.extend({
  template: '#vegetable',
});
var Fruit = Product.extend({
  template: '#fruit',
});

Here's a Plunker with this approach.

Given that your Fruit and Vegetable templates are so similar, you might be able to take this idea even further and use a common template from the base component.

Solution 3:

If you are trying to share the same component logic between or along multiple vue template and layout, you can simply try this approach below:

before:

a.vue

<template>
  <div>
     <h1>{{title}}</h1>
     <small>{{datetime}}</small>
  </div>
</template>

<script>
export default {
  props: {
    title: String
  },
  data() {
    return {
      datetime: new Date()
    }
  }
}
</script>

b.vue

<template>
  <div>
     <h3>{{title}}</h3>
     <h6>{{datetime}}</h6>
  </div>
</template>

<script>
export default {
  props: {
    title: String
  },
  data() {
    return {
      datetime: new Date()
    }
  }
}
</script>

after:

a.vue

<template>
  <div>
     <h1>{{title}}</h1>
     <small>{{datetime}}</small>
  </div>
</template>

<script>
import shared from "./shared.js";
export default Object.assign({}, shared);
</script>

b.vue

<template>
  <div>
     <h3>{{title}}</h3>
     <h6>{{datetime}}</h6>
  </div>
</template>

<script>
import shared from "./shared.js";
export default Object.assign({}, shared);
</script>

shared.js

export default {
  props: {
    title: String
  },
  data() {
    return {
      datetime: new Date()
    }
  }
}

Solution 4:

You can put the method in your root Vue instance and then dispatch an event from the child instance when a veggie is selected, or when a fruit is selected. Events look for a handler on their parent component, and if they don't find an event handler they keep going up the chain until they do. So on your root instance:

events: {
    'choose-fruit':function(fruit){

        //handle the choosing of fruit

    }
}

Then on the child instance:

selectFruit: function(product){

    this.$dispatch('choose-fruit', product);

}