Vue component .$on use case - vuejs2

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)

Related

How to pass events from a non-Vue member object

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

Vue.js extend component and data updates

I'm using vue.js extends for the first time. I have a component that extends another and it needs to read the root components data to update the status in its own component.
What I'm finding is that the component that extends the other only seems to take a copy of the root's data when it's rendered but if I update a property in the root component it's not updated in the extended component.
So I might not be going about this the right way if the extended component doesn't update when the root does. For example I want to check the length of an array on the root component and update another data value. It updates the value on the root but not on the extended component.
Is this the expected behaviour and is there a way I can send the updated data down to the extended component?
Sample code:
<a inline-component>
<input type="text" v-model="myArray" />
<button v-on:click="saveData">Save</button>
</a>
<b inline-component>
<div v-if="myArray.length > 0">On Target</div>
</b>
var a = Vue.component('a', {
data: function () {
return {
myArray: [],
}
},
methods: {
saveData : function(){
var vm = this;
axios.post('/save', {
})
.then(function (response) {
vm.myArray = response.data;
})
.catch(function (error) {
console.log(error);
});
},
}
});
Vue.component('b', {
extends: a,
});
I have a component that extends another and it needs to read the root components data to update the status in its own component.
For the purposes of my answer I'm going to assume that you have two component definitions, A and B, and B extends A. I assume that when you say root you just mean A.
What I'm finding is that the component that extends the other only seems to take a copy of the root's data when it's rendered but if I update a property in the root component it's not updated in the extended component.
Rendering is not really relevant here. The data properties are set up when a component instance is created. Typically rendering will happen just after creation but merging any data happens much earlier in the component life-cycle. Even if the component isn't rendered the data will still be initialised.
No copying takes place. Let's consider a data function on component A:
data () {
return {
myArray: []
}
}
Every time this function is invoked it is going to return a new object, each containing a new array. This is precisely what happens if you create an instance of A directly. For each instance, Vue will call this function and get a new object defining the data. Generally that's what you'd want, rather that having components sharing data.
Now let's consider B. That might define its own data function. When an instance of B is created Vue will call the data function for both A and B and then merge the objects. No copying takes place, just merging. If you want to know more about how Vue handles merging in general see the documentation but for data the strategy is pretty simple. Properties from both objects will be combined with B taking precedence over A if there's a clash of property names. There is no recursive merging of properties.
So the idea of updating 'a property in the root component' is not particularly well-defined. You might be thinking of it as a bit like a prototype chain, where modifying a superclass would impact the subclass, but that isn't what's going on here. The data functions are invoked when the component is created and that's that. There isn't a lasting link back to the component definition like there is with a prototype chain.
If you really want all your component instances to share the same data value then it can be done, you just need to make sure that the data function is returning the same object/array every time. e.g.
const myArray = []
export default {
name: 'A',
data () {
return {
myArray
}
}
}
Written this way all instances of A will share the same array for myArray. So long as B doesn't define it's own value for myArray it will share it too.
For example I want to check the length of an array on the root component and update another data value. It updates the value on the root but not on the extended component.
I'm struggling a bit to understand what that means. It seems there are lots of assumptions about things being shared, single instances here. It's not entirely clear how you update the 'root' given it's a component definition and not a component instance.
If possible you should use a computed property for this. That would be inherited by B. Each instance of A (or B) would have their own value for this computed property, which might be a little wasteful if they're all going to be the same, but it's probably still the best way to go.
You could in theory use a watch. That should be inherited too but keep in mind it would be manipulating values for that particular instance.
Reading between the lines a little, if you wanted to update something on the 'root' so that it magically appeared in the subcomponents you could use the same shared reference-type trickery that I demonstrated earlier for myArray. You may need to be careful with how you update it though. If, for example, you used a watch you might find the you end up updating the same object many times, once for each instance of the component.
Update:
Based on the code you've posted it could be made to work something like this:
var myArray = [];
var a = Vue.component('a', {
data: function () {
return {
myArray: myArray // Note: using the same, shared array
}
},
methods: {
saveData : function(){
var vm = this;
axios.post('/save', {
})
.then(function (response) {
// Note: Updating the array, not replacing it
var myArray = vm.myArray;
myArray.splice(0, myArray.length);
myArray.push.apply(myArray, response.data);
})
.catch(function (error) {
console.log(error);
});
},
}
});
Vue.component('b', {
extends: a,
});
Your example didn't include any ES6 so I've refrained from using it but it would be a bit simpler if that were available.
The example above works by sharing the same array between all instances of the component and then mutating that instance. Assigning a new array to that property won't work as it would only update that particular component instance.
However, all that said, this is increasingly looking like a case where you should give up on trickery and just use the Vuex store instead.

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>

vue2: can not find a proper way to initialize component data by ajax

I have a component whose data is initialized by ajax. I know vue.js has provide several lifecycle hooks: Lifecycle-Diagram. But for ajax to initialize the data, which hook(beforeCreate, create, mounted, etc) is the best place to do it:
hook_name: function() {
ajaxCall(function(data) {
me.data = data;
});
}
Currently, i do it in mounted, making it to re-render the component. But i think we should get the data before the first render. Can someone figure out the best way to do it?
If you want to initialize your component with data you receive from a request, created() would be the most appropriate hook to use but it is a request, it might not resolve by the end of created or even mounted() (when even your DOM is ready to show content!).
So do have your component initialized with empty data like:
data () {
return {
listOfItems: [],
someKindOfConfig: {},
orSomeSpecialValue: null
}
}
and assign the actual values when you receive them in your created hook as these empty data properties would be available at that point of time, like:
created () {
someAPICall()
.then(data => {
this.listOfItems = data.listOfItems
})
/**
* Notice the use of arrow functions, without those [this] would
* not have the context of the component.
*/
}
It seems like you aren't using (or aren't planning to use) vuex but I'd highly recommend you to use it for for managing your data in stores. If you use vuex you can have actions which can make these api calls and by using simple getters in your component you would have access to the values returned by the request.