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

Javascriptvue.js

Javascript Problem Overview


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!

Javascript Solutions


Solution 1 - Javascript

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 - Javascript

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 - Javascript

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 - Javascript

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);

}

Solution 5 - Javascript

I would just configure a mixin

export const sharedMethods =  {
    // Generic funcion
    methods: {
        axiosGet(route){
            return axios.get(route)
            .then(response => response.data)
            .catch((error)=> {console.log(error)})
        },
        axiosPost(route, postObj, resolveCallback, rejectCallback){
            axios.post(route, postObj)
            .then(resolveCallback)
            .catch(rejectCallback);
        },
}

And then use the mixin in the component, calling its functions as you would do with the functions of the component itself, as if the functions were part of the component you are importing the mixin in:

Put it on the component: mixins: [sharedMethods],

Inside the component, I call the mixin function:

this.axiosPost(
      '/route',
      data,
      () => console.log('success'),
      () => console.log('fail')
)

It is useful because using an aproach where you just import functions from a js file, the functions in the file being imported will take their own 'this' to use in the functions which might cause some confusion, whereas mixin will use the 'this' of the component it is being imported in, as if its functions were part of the component, as I said.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMikeView Question on Stackoverflow
Solution 1 - JavascriptcoderofsalvationView Answer on Stackoverflow
Solution 2 - JavascriptPeterView Answer on Stackoverflow
Solution 3 - JavascriptLouis Jr. LorView Answer on Stackoverflow
Solution 4 - JavascriptJeffView Answer on Stackoverflow
Solution 5 - JavascriptmrssolarisView Answer on Stackoverflow