Home > OS >  Change Vue.js data from outside Vue.js
Change Vue.js data from outside Vue.js

Time:03-14

I have a simple cart demo.

It has 3 Vue applications, a global object as a shared context, and a button that is in Vanilla JS.

If you add products to the cart via a Vue app, everything works.

But if you use the Vanilla JS button, nothing works.

Basically, I want to change the global object outside the Vue application, and then somehow force the view to capture the changes.

How can I do that?

P.S. the reason I'm doing this is because I'm using Vue.js progressively in a new application. Part of is jQuery, and part of it Vue. They both manipulate a global object as a shared object.

This is my JS code:

let cart = { orderLines: [], itemsCount: 0 }

let addViaJs = document.querySelector('#addViaJs');
addViaJs.addEventListener('click', () => {
    cart.orderLines[0].quantity  = 1; // this line does not cause update in UI
});

let miniCart = {
    data() {
        return {
            cart,
        }
    },
    computed: {
        itemsCount() {
            let itemsCount = 0;
            for (let i = 0; i < this.cart.orderLines.length; i  ) {
                itemsCount  = this.cart.orderLines[i].quantity || 0;
            }
            return itemsCount;
        }
    },
};
Vue.createApp(miniCart).mount('#miniCart');

let bigCart = {
    data() {
        return {
            cart,
        }
    },
    computed: {
        itemsCount() {
            let itemsCount = 0;
            for (let i = 0; i < this.cart.orderLines.length; i  ) {
                itemsCount  = this.cart.orderLines[i].quantity || 0;
            }
            return itemsCount;
        }
    },
};
Vue.createApp(bigCart).mount('#bigCart');

let productList = {
    data() {
        return {
            cart,
            products: [
                { id: 1, label: 'Product A' },
                { id: 2, label: 'Product B' },
                { id: 3, label: 'Product C' },
            ]
        }
    },
    methods: {
        addToCart(product) {
            const line = this.cart.orderLines.find(line => line.id === product.id);
            if (line) {
                line.quantity  ;
            } else {
                this.cart.orderLines.push({ ...product, quantity: 1 });
            }          
        },
    }
};
Vue.createApp(productList).mount('#productList');

CodePudding user response:

Use a reactive store, external to all your apps and inject it wherever needed.

You could implement either pinia or vuex. The simplest solution would be a reactive() object.

Here's a basic example, with 3 separate apps, demonstrating the principle:

const { createApp, reactive, toRefs } = Vue;
const store = reactive({
  count: 0
})

createApp({
  setup: () => ({
    ...toRefs(store),
    increaseCount: () => store.count  = 1
  })
}).mount('#app-1');

createApp({
  setup: () => ({
    ...toRefs(store),
    decreaseCount: () => store.count -= 1
  })
}).mount('#app-2');

createApp({
  setup: () => ({
    ...toRefs(store)
  })
}).mount('#app-3');
.container {
  display: flex;
  justify-content: space-evenly;
}
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div >
  <div id="app-1">
    <h3> App 1</h3>
    {{ count }}
    <button @click="increaseCount">Increase</button>
  </div>
  <div id="app-2">
    <h3> App 2</h3>
    {{ count }}
    <button @click="decreaseCount">Decrease</button>
  </div>
  <div id="app-3">
    <h3> App 3</h3>
    {{ count }}
    <input v-model.number="count" type="number">
  </div>
</div>


Same thing, using pinia and placing increase and decrease methods on the store:

const { createApp, reactive, toRefs } = Vue;
const { createPinia, defineStore } = Pinia;
const pinia = createPinia();

const useCounter = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increase() {
      this.count  = 1;
    },
    decrease() {
      this.count -= 1;
    }
  }
});

[1, 2, 3].forEach(key => createApp({
  setup: () => ({
    counter: useCounter(pinia)
  })
}).mount(`#app-${key}`))
.container {
  display: flex;
  justify-content: space-evenly;
}
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue-demi"></script>
<script src="https://unpkg.com/[email protected]/dist/pinia.iife.prod.js"></script>
<div >
  <div id="app-1">
    <h3> App 1</h3>
    {{ counter.count }}
    <button @click="counter.increase">Increase</button>
  </div>
  <div id="app-2">
    <h3> App 2</h3>
    {{ counter.count }}
    <button @click="counter.decrease">Decrease</button>
  </div>
  <div id="app-3">
    <h3> App 3</h3>
    {{ counter.count }}
    <input v-model.number="counter.count" type="number"/>
  </div>
</div>

Pinia and Vuex come with the advantage they're integrated with vue devtools, which means you can inspect changes (a.k.a. mutations), undo or replay them. It makes debugging a lot simpler, basically.

CodePudding user response:

I analyzed the code that you put in your codepen. First of all if you are new to "vue.js" I suggest that do not build such an app that uses jQuery and pure js and vue-js all in some part of your app. Because this may cause some conflicts that you could not understand clearly.

Anyway the problem that you have here is: not considering Vue reactivity. In simple words when you use Vue.createApp(...).mount("some id"), only the part of html codes that are the children of that id are accessed and controlled by Vue. So when you have a button outside that part, you can not expect that Vue recognize and handle it correctly. Because of this fact, if you want to change a number (here the itemsCount data) and see the result immediately in your html part (that is the reactivity behavior of Vue) with your own javascript code, you must write all the code yourself. For example here I put some codes that illustrate what I mean:

let cart = { orderLines: [
    /* change the structure of defined array */
        {
            quantity: 0
        }
    ], itemsCount: 0 }


let addViaJs = document.querySelector('#addViaJs');
addViaJs.addEventListener('click', () => {
    /* manually change the "html" content with js codes */
    let currentValue = parseInt(document.getElementById("itemsSpan").innerText)

    cart.orderLines[0].quantity = currentValue   1;
    cart.itemsCount = currentValue   1;
    document.getElementById("itemsSpan").innerText = cart.itemsCount;
});

let miniCart = {
    data() {
        return {
            cart,
        }
    },
    computed: {
        /* this "itemsCount is different from "cart.itemsCount" */
        itemsCount() {
            let itemsCount = 0;
            for (let i = 0; i < this.cart.orderLines.length; i  ) {
                itemsCount  = this.cart.orderLines[i].quantity || 0;
            }
            return itemsCount;
        }
    },
};
Vue.createApp(miniCart).mount('#miniCart');

let bigCart = {
    data() {
        return {
            cart,
        }
    },
    computed: {
        itemsCount() {
            let itemsCount = 0;
            for (let i = 0; i < this.cart.orderLines.length; i  ) {
                itemsCount  = this.cart.orderLines[i].quantity || 0;
            }
            return itemsCount;
        }
    },
};
Vue.createApp(bigCart).mount('#bigCart');

let productList = {
    data() {
        return {
            cart,
            products: [
                { id: 1, label: 'Product A' },
                { id: 2, label: 'Product B' },
                { id: 3, label: 'Product C' },
            ]
        }
    },
    methods: {
        addToCart(product) {
            /* change the related values of "count" object */
            this.cart.orderLines[0].quantity  
            this.cart.itemsCount  
            // const line = this.cart.orderLines.find(line => line.id === product.id);
            // if (line) {
            //     line.quantity  ;
            // } else {
            //     this.cart.orderLines.push({ ...product, quantity: 1 });
            // }
        },
    }
};
Vue.createApp(productList).mount('#productList');
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    

<div id="productList" v-cloak>
    <h2>Products</h2>
    <div v-for="product in products">
        <span>{{ product.label }}</span>
        <button @click="addToCart(product)">Add</button>
    </div>
</div>

<div id="miniCart" v-cloak>
    <h2>Mini cart</h2>
    <div>itemsCount:
        <span id="itemsSpan">
            {{ itemsCount }}
        </span>
    </div>
</div>

<div id="bigCart" v-cloak>
<!--    <h2>Cart ({{ itemsCount }})</h2>-->
<!--    <ul>-->
<!--        <li v-for="line in cart.orderLines">{{ line.label}} ({{ line.quantity }})</li>-->
<!--    </ul>-->
</div>

<button id='addViaJs'>
    Add via JS (outside Vue)
</button>
    

<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>

    
</body>
</html>

That is not exactly similar to your code. I commented some parts and made data simpler to better show the result. At first I made the structure of cart data to the one that must be after adding elements (if you don't add any object to orderLines array, javascript gives you error when you want to add the value of for example orderLines.quantity). After that in addEventListener part I get the content from html part, add value to that and returned the new value to html. All of these kind of processes could handle by vue, if your button is children of your Vue app and not outside that. Also in your Vue part you must change cart.itemsCount and other properties when and where needed.

You also must notice that the itemsCount computed property that you defined is completely different from cart.itemsCount and this should not cause error in your code. With all the above descriptions that I said above, again I don't recommend to build your app in this way. You may encounter other errors in your code in this situations. You can use jQuery or pure js to change the data (for example cart.quantity), but for showing them in your html parts that are handled by Vue, use buttons and other methods that are part of Vue itself not outside the Vue and also monitor the change of data in your Vue Devtools to debug correctly your code.

  • Related