In Vue.js, is there a way to register an event if any component updates its data?
My usecase: I am modeling a RPG character via a set of Javascript classes. The TCharacter
class has several attributes that can be modified: name, level, HP, magic. While “name” is a simple string, “HP” and “magic” is a custom class TResource
which has its own consumption and refill rules.
Instance of the TCharacter
class is a source of truth, and I created some Vue components that are views of it.
I created a character
component and a resource
component in Vue, vaguely like this:
<div class=template id=character> <input v-model="ch.name"> <resource :attr="ch.magic"></resource> <resource :attr="ch.hp"></resource> </div> <div class="template" id="resource"> you have {{ attr.available }} points <button @click="attr.consume">X</button> </div> <div id="main"> <character :ch="lancelot"></character> </div>
and the javascript:
class TCharacter { constructor() { this.name = "Lancelot" this.hp = new Resource(20) this.magic = new Resource(10) } } class TResource { constructor(limit) { this.available = limit this.limit = limit } consume() { if (this.available > 0) this.available--; } } let lancelot = new TCharacter() Vue.component('character', { template: '#character', props: ['ch'], }) Vue.component('resource', { template: '#resource', props: ['attr'], }) new Vue({ el: "#main", data() { return { lancelot } } })
(I’m not sure the code works exactly as written, but hopefully the intent is clear. Something very similar to this is already working for me.)
Now, I’d like to save the character object to localstorage every time the user makes a modification: changes its name, clicks on a button that consumes a point of magic, etc.
So for instance, I want to be notified that the value of ch.name
changed because the user typed something into the input box. Or that a magic point was lost because the user clicked a button for that.
I could detect changes to the character
component by installing an updated()
handler, which notifies me whenever a DOM is modified (viz). However, this won’t trigger when the child component resource
is modified. I’d need to add a separate updated()
handler to all other components. This gets tedious very fast.
I’m imagining something like a global updated()
handler that would fire any time any component has registered a change. Or better, a way to specify that update
should fire on component’s children changes as well.
edit: I have reworded parts of the question to clarify what I’m trying to accomplish. Some of you already suggested Vuex. But, from what I understood, Vuex enforces being the single source of truth — I already have a single source of truth. How is Vuex different / better?
Advertisement
Answer
You’re going to need a serialized version of lancelot
to write out. You can do that with a computed
. Then you can watch
the computed to see when anything changes.
Alternatively, you could watch
each individual trait, and write it out as it changes.
class TCharacter { constructor() { this.name = "Lancelot" this.hp = new TResource(20) this.magic = new TResource(10) } } class TResource { constructor(limit) { this.available = limit this.limit = limit } consume() { if (this.available > 0) this.available--; } } let lancelot = new TCharacter() Vue.component('character', { template: '#character', props: ['ch'], }) Vue.component('resource', { template: '#resource', props: ['attr'], }) const vm = new Vue({ el: "#main", data() { return { lancelot } }, computed: { serializedLancelot() { return JSON.stringify(this.lancelot); } }, watch: { serializedLancelot(newValue) { console.log("Save update:", newValue); } } }); setTimeout(() => { vm.lancelot.hp.consume(); }, 500);
<script src="https://unpkg.com/vue@latest/dist/vue.js"></script> <div id="main"> </div>