2

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

2 Answers 2

3

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 class="container">
  <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 class="container">
  <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.

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

2 Comments

yeah, it worked. Such an elegant solution (the first one). thank you
You're welcome. When using it extensively, the tendency is to use pinia or vuex, as it allows you to encapsulate the entire business logic (state + methods) related to a particular functionality into one entity (a store), rather than having the methods scattered around in various component. The real power comes from the fact it's completely unrelated to DOM, so you don't depend on various components being rendered. You can have as many stores as you want, obviously. Another thing to note is that you can modify a store from outside of a Vue app, which is quite handy.
1

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.

1 Comment

Thank you for this long reply and the time you put into it. I realized the problem as you explained it very clearly. But since using reactive method solved our problem I thing that's a better way to get the results. Less lines of codes and more maintainable.

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.