How to pass events from a non-Vue member object - vue.js

I have a component (Foo), one of whose data props is an instance of a class that's handling a lot of non-UI stuff (Bar). There will be some events within Bar that need to trigger behaviour in Foo. I'd like to wire them up in a way that respects the Vue data-flow model as much as possible. What would be the recommended approach for this? My naive solution would be to have some methods in Bar to set event handlers, so eg. in mounted I do this.bar.onMyEvent(this.onBarEvent). Is this the best I can hope for, or is there a more Vuey way to do it?
export default {
name: 'Foo',
data: () => ({
bar: new Bar(/* ... */)
}),
methods: {
onBarEvent (data) {
// do stuff with data
}
},
}

have you tried using the Computed Properties ?
https://v2.vuejs.org/v2/guide/computed.html

Related

$.parent vs emitting and listening events performance comparison

Suppose I am importing component B, which fires a method defined in the parent component A
Component B:
....,
methods: {
onSomeEvent: function() {
this.$parent.doSomething();
}
},
....
Component A:
import ComponentB from 'componentB.vue'
....,
methods: {
doSomething: function() {
console.log('Something done by the parent');
}
},
....
The other way to achieve the same effect would be to emit event in Component B and then listening to it in Component A:
Component B:
....,
methods: {
onSomeEvent: function() {
this.$emit('eventForParent');
}
},
....
Component A:
....
<ComponentB #eventForParent="doSomething()" />
....
import ComponentB from 'componentB.vue'
....,
methods: {
doSomething: function() {
console.log('Something done by the parent');
}
},
....
The first approach has a draw back, it only works if B is the direct child of A while the second approach works with indirect imports as well. But does it come at a cost if we are talking about a complex UI/UX design?
Is the first approach faster than second one? Any help would be appreciated?
This question is bound to be flagged as being open to opinion. However - I'll do my best to answer:
In short - it really depends on your use case.
If you're building a component that you will re-use throughout your project, or in other projects (say for example, a simple datepicker), it would be best to take the route of passing properties and emitting events. Doing so gives the flexibility of not relying on what methods are available in it's parent component.
However, if your component is only being used in one specific place - then it might be OK to go down the route of using Vue.$parent(). However - you should consider whether having logic in your parent component is necessary, if it will only be used to control the child component. So even in this case, using events and properties would possibly be the better option.
One case where using Vue.$parent() is very useful, and suitable - is when you have a component nested through <router-view>. Passing properties and setting up events in this case wouldn't be suitable - so, if there is a method in the parent component that you needed to access - Vue.$parent() would be a good solution here.

How to automatically construct watch property based on data attributes in Vue.js?

I have standard Vue.js component and I'd like to convert attributes in data property to watcher or in other words I want to construct a watch object based on data property attributes automatically
my idea looks something like this
watch: {
...(() => {
const watchers = {}
Object.keys(this.$data).forEach(key => {
watchers[key] = () => {
new ProductNutrientUpdate(this).run()
}
})
return watchers
})(),
},
the problem with this approach is that this.$data is not constructed yet
maybe there is some way how I can add watchers in created hook for example??
Vue already watches properties of the data object (note if any of these values are themselves objects, I think you need to update the whole object, i.e. change its value to a shallow copy with the desired nested key-values).
Refer to: https://v2.vuejs.org/v2/guide/reactivity.html
You can then use the update lifecycle hook to watch for all changes to data: https://v2.vuejs.org/v2/api/#updated
I was able to resolve a challenge using the following approach
created() {
Object.keys(this.$data).forEach(key => {
this.$watch(key, function() {
// ... some logic to trigger on attribute change
})
})
}

vue.js: scoping custom option merge strategies instead going global

Good Day Fellows,
Quick summary: how can I use custom option merge strategies on an individual basis per component and not globaly?
My problem:
I am extending my components via Mixins and it is working great so far. However, while it is working great with the likes of component methods, I often need to override some lifecycle hooks, like mounted, created, etc. The catch is, Vue - by default - queues them up in an array and calls them after another. This is of course defined by Vues default merge strategies.
However in some specific cases I do need to override the hook and not have it stack. I know I can customize Vue.config.optionMergeStrategies to my liking, but I want the mergeStrategy customized on a per component basis and not applying it globably.
My naive approach on paper was to create a higher function which stores the original hooks, applies my custom strategy, calls my component body and after that restores Vues original hooks.
Let's say like this
export default function executeWithCustomMerge(fn) {
const orig = deep copy Vue.config.optionMergeStrategies;
Vue.config.optionMergeStrategies.mounted = (parent, child) => [child];
fn();
Vue.config.optionMergeStrategies = deep copy orig;
}
And here's it in action
executeWithCustomMerge(() => {
Vue.component('new-comp', {
mixins: [Vue.component("old-comp")],
},
mounted() {
//i want to override my parent thus I am using a custom merge strategy
});
});
Now, this is not going to work out because restoring the original hook strategies still apply on a global and will be reseted before most hooks on my component are being called.
I wonder what do I need to do to scope my merge strategy to a component.
I had a look at optionMergeStrategies in more detail and found this interesting quote from the docs (emphasis mine):
The merge strategy receives the value of that option defined on the parent and child instances as the first and second arguments, respectively. The context Vue instance is passed as the third argument.
So I thought it would be straightforward to implement a custom merging strategy that inspects the Vue instance and looks at its properties to decide which strategy to use. Something like this:
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal, vm) {
if (vm.overrideCreated) {
// If the "overrideCreated" prop is set on the component, discard the mixin's created()
return [vm.created];
}
return mergeCreatedStrategy(toVal, fromVal, vm);
};
It turns out though that the 3rd argument (vm) is not set when the strategy function is called for components. It's a new bug! See https://github.com/vuejs/vue/issues/9623
So I found another way to inform the merge strategy on what it should do. Since JavaScript functions are first-class objects, they can have properties and methods just like any other object. Therefore, we can set a component's function to override its parents by setting a property on it and looking for its value in the merge strategy like so:
Vue.mixin({
created() {
this.messages.push('global mixin hook called');
}
});
const mixin = {
created() {
this.messages.push('mixin hook called');
},
};
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal) {
if (fromVal.overrideOthers) {
// Selectively override hooks injected from mixins
return [fromVal];
}
return mergeCreatedStrategy(toVal, fromVal);
};
const app = {
el: '#app',
mixins: [mixin],
data: { messages: [] },
created() {
this.messages.push('component hook called');
},
};
// Comment or change this line to control whether the mixin created hook is applied
app.created.overrideOthers = true;
new Vue(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>Messages from hooks</h1>
<p v-for="message in messages">{{ message }}</p>
</div>

Calling method of another router-view's component / Vue.js

I have two <router-view/>s: main and sidebar. Each of them is supplied with a component (EditorMain.vue and EditorSidebar.vue).
EditorMain has a method exportData(). I want to call this method from EditorSidebar on button click.
What is a good way of tackling it?
I do use vuex, but i don't wanna keep this data reactive since the method requires too much computational power.
I could use global events bus, but it doesn't feel right to use it together with vuex (right?)
I could handle it in root of my app by adding event listener to router-view <router-view #exportClick="handleExportData"> and then target editor component, but it does not feel right as well as later i could need 100 listeners.
Is there any good practice for this? Or did i make some mistakes with the way app is set up? Did is overlooked something in documentation?
After two more years of my adventure with Vue I feel confident enough to answer my own question. It boils down to communication between router views. I've presented two possible solutions, I'll address them separately:
Events bus
Use global events bus (but it doesn't feel right to use it together with vuex)
Well, it may not feel right and it is surely not a first thing you have to think about, but it is perfectly fine use-case for event-bus. The advantage of this solution would be that the components are coupled only by the event name.
Router-view event listeners
I could handle it in root of my app by adding event listener to router-view <router-view #exportClick="handleExportData"> and then target editor component, but it does not feel right as well as later i could need 100 listeners.
This way of solving this problem is also fine, buy it couples components together. Coupling happens in the component containing <router-view/> where all the listeners are set.
Big number of listeners could be addressed by passing an object with event: handler mapping pairs to v-on directive; like so:
<router-view v-on="listeners"/>
...
data () {
return {
listeners: {
'event-one': () => console.log('Event one fired!'),
'event-two': () => console.log('The second event works as well!')
}
}
You could create a plugin for handling exports:
import Vue from 'vue'
ExportPlugin.install = function (Vue, options) {
const _data = new Map()
Object.defineProperty(Vue.prototype, '$exporter', {
value: {
setData: (svg) => {
_data.set('svg', svg)
},
exportData: () => {
const svg = _data.get('svg')
// do data export...
}
}
})
}
Vue.use(ExportPlugin)
Using like:
// EditorMain component
methods: {
setData (data) {
this.$exporter.setData(data)
}
}
// EditorSidebar
<button #click="$exporter.exportData">Export</button>

Vue component .$on use case

Its not clear to me how to use the .$on(...) method available in every Vue instance. I am sure I am probably missing some use case where an event would be emitted and consumed by the same Vue component (?) but currently I am not able to imagine many. Also, where would this wiring be performed. Would that be in a lifecycle method ?
My problem: I have unrelated (that is non-sibling, non-descendant or non-common-parent) components which change view based on interactions made on a different component. And, $on(...) does not seem to help my purpose.
And, there arises the need to understand how/why .$on(..) is made available in the framework. Thank you.
You can use the $on-method for implementation of CommunicationHub -- common mixin, for non parent <--> child communication (like in your case).
For example: you have two Vue root applications: RootAppA and RootAppB. To communicate between them, you can create CommunicationHub mixin with next code:
let CommunicationHub = new Vue();
Vue.mixin({
data: function () {
return {
communicationHub: CommunicationHub
}
}
});
Now you can send data by emitting custom event from RootAppA with $emit-method, and get this data by subscribing on this event in RootAppB, with method $on:
let RootAppA = {
methods: {
sendData(){
this.communicationHub.$emit('customEvent', {foo: 'bar', baz: 1, comment: 'custom payload object'});
}
}
}
let RootAppB = {
created(){
this.communicationHub.$on('customEvent', (payload) => {
console.log(payload); //{foo: 'bar', baz: 1, comment: 'custom payload object'}
});
}
}
By the way, please mention that CommunicationHub-pattern is not so flexible solution for bigger apps. So if your application will grow up, perhaps you will want to use Vuex-library (see my example in previous so-answer)