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>
Related
I am using Vue 3 to build an app with a third party library (Azure Communication Calling). I'm using the options API. My issue is that I need event handlers that call functions initialized by the third party library. The only way I can find to do this is to assign the entire object created by the third party constructor to Vue's component data. I realize this is not best practice (although it does work). Is there a better way?
I am initializing the Constructor in the Mounted hook. My event handlers need access to methods inside the object created in the Constructor. It seems like you should be able to assign event handlers in Mounted, or assign methods in Mounted. I can't get either of these ideas to work.
My template is like this:
<template>
<div>
<child-component #someEvent="doSomething">
</child-component>
<div>
The rest of the app goes here..
</div>
</div>
</template>
<script>
import {Constructor} from 'third-party-library'
import ChildComponent from './components/ChildComponent'
export default {
data() {
return {
complexObject: null
}
},
components: {
ChildComponent
},
mounted() {
this.complexObject = new Constructor()
},
methods: {
doSomething() {
this.complexObject.thirdPartyMethod()
}
}
}
</script>
I just thought you weren't supposed to put objects with their own methods in data. I thought data is only for primitive data types.
That's not true. It's acceptable to return non-primitives from data(). Normally, data() is used to specify properties intended for reactivity (e.g., in the template). However, you could also specify non-reactive data by Object.freeze-ing the property:
export default {
data() {
return {
complexObject: Object.freeze(new Constructor()) // non-reactive
}
},
}
Alternatively, you could attach the property in created():
export default {
data() {
return {
// complexObject: null, ❌ remove this
}
},
created() {
this.complexObject = new Constructor() 👈
},
}
If using TypeScript, the downside here is there would be no type inference for the attached property.
Did you try to use mixins? https://vuejs.org/api/options-composition.html#mixins This way you can also use this mixin for other components too.
I solved this issue, in a similar way to Tony19's second suggestion. This turned out to be much simpler than I expected. Unless there is some reason why I should not create a new top-level property on a Vue component, this seems best.
export default {
complexObject: null,
mounted() {
this.complexObject = new Constructor()
},
}
The constructor does have to be in the mounted hook, BTW.
I created the child using:
const ComponentClass = Vue.extend(someComponent);
const instance = new ComponentClass({
propsData: { prop: this.value }
})
instance.$mount();
this.$refs.container.appendChild(instance.$el);
When this.value is updated in the parent, its value doesn't change in the child. I've tried to watch it but it didn't work.
Update:
There's an easier way to achieve this:
create a <div>
append it to your $refs.container
create a new Vue instance and .$mount() it in the div
set the div instance's data to whatever you want to bind dynamically, getting values from the parent
provide the props to the mounted component from the div's data, through render function
methods: {
addComponent() {
const div = document.createElement("div");
this.$refs.container.appendChild(div);
new Vue({
components: { Test },
data: this.$data,
render: h => h("test", {
props: {
message: this.msg
}
})
}).$mount(div);
}
}
Important note: this in this.$data refers the parent (the component which has the addComponent method), while this inside render refers new Vue()'s instance. So, the chain of reactivity is: parent.$data > new Vue().$data > new Vue().render => Test.props. I had numerous attempts at bypassing the new Vue() step and passing a Test component directly, but haven't found a way yet. I'm pretty sure it's possible, though, although the solution above achieves it in practice, because the <div> in which new Vue() renders gets replaced by its template, which is the Test component. So, in practice, Test is a direct ancestor of $refs.container. But in reality, it passes through an extra instance of Vue, used for binding.
Obviously, if you don't want to add a new child component to the container each time the method is called, you can ditch the div placeholder and simply .$mount(this.$refs.container), but by doing so you will replace the existing child each subsequent time you call the method.
See it working here: https://codesandbox.io/s/nifty-dhawan-9ed2l?file=/src/components/HelloWorld.vue
However, unlike the method below, you can't override data props of the child with values from parent dynamically. But, if you think about it, that's the way data should work, so just use props for whatever you want bound.
Initial answer:
Here's a function I've used over multiple projects, mostly for creating programmatic components for mapbox popups and markers, but also useful for creating components without actually adding them to DOM, for various purposes.
import Vue from "vue";
// import store from "./store";
export function addProgrammaticComponent(parent, component, dataFn, componentOptions) {
const ComponentClass = Vue.extend(component);
const initData = dataFn() || {};
const data = {};
const propsData = {};
const propKeys = Object.keys(ComponentClass.options.props || {});
Object.keys(initData).forEach(key => {
if (propKeys.includes(key)) {
propsData[key] = initData[key];
} else {
data[key] = initData[key];
}
});
const instance = new ComponentClass({
// store,
data,
propsData,
...componentOptions
});
instance.$mount(document.createElement("div"));
const dataSetter = data => {
Object.keys(data).forEach(key => {
instance[key] = data[key];
});
};
const unwatch = parent.$watch(dataFn || {}, dataSetter);
return {
instance,
update: () => dataSetter(dataFn ? dataFn() : {}),
dispose: () => {
unwatch();
instance.$destroy();
}
};
}
componentOptions is to provide any custom (one-off) functionality to the new instance (i.e.: mounted(), watchers, computed, store, you name it...).
I've set up a demo here: https://codesandbox.io/s/gifted-mestorf-297xx?file=/src/components/HelloWorld.vue
Notice I'm not doing the appendChild in the function purposefully, as in some cases I want to use the instance without adding it to DOM. The regular usage is:
const component = addProgrammaticComponent(this, SomeComponent, dataFn);
this.$el.appendChild(component.instance.$el);
Depending on what your dynamic component does, you might want to call .dispose() on it in parent's beforeDestroy(). If you don't, beforeDestroy() on child never gets called.
Probably the coolest part about it all is you don't actually need to append the child to the parent's DOM (it can be placed anywhere in DOM and the child will still respond to any changes of the parent, like it would if it was an actual descendant). Their "link" is programmatic, through dataFn.
Obviously, this opens the door to a bunch of potential problems, especially around destroying the parent without destroying the child. So you need be very careful and thorough about this type of cleanup. You either register each dynamic component into a property of the parent and .dispose() all of them in the parent's beforeDestroy() or give them a particular selector and sweep the entire DOM clean before destroying the parent.
Another interesting note is that in Vue 3 all of the above will no longer be necessary, as most of the core Vue functionality (reactivity, computed, hooks, listeners) is now exposed and reusable as is, so you won't have to $mount a component in order to have access to its "magic".
when i add computed() instead mounted() it throws an error
export default {
components: {
MainLayout
},
mounted(){
var x = document.getElementById('homeTabPanes');
// x.style.background = "blue";
console.log("check the value of x", x);
}
}
computed is an object containing methods that returns data, mounted is a life hook executed after the instance gets mounted, check out the links to the docs it have really good explanation
From the docs
..computed properties are cached based on their dependencies. A computed property will only re-evaluate when some of its dependencies have changed.
If you want data to be cached use Computed properties on the other hand mounted is a lifecycle hook, a method which is called as soon as the Vue instance is mounted on the DOM.
In-template expressions are very convenient, but they are meant for simple operations. Putting too much logic in your templates can make them bloated and hard to maintain.
That’s why for any complex logic, you should use a computed property.
Basic Example
<div id="reverseMessageContainer">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
look at the js below:
var vm = new Vue({
el: '#reverseMessageContainer',
data: {
message: 'Hello'
},
computed: {
// a computed getter
reversedMessage: function () {
// `this` points to the vm instance
return this.message.split('').reverse().join('')
}
}
})
Here we have declared a computed property reversedMessage. The function we provided will be used as the getter function for the property vm.reversedMessage:
You can open the console and play with the example vm yourself. The value of vm.reversedMessage is always dependent on the value of vm.message.
console.log(vm.reversedMessage) // => 'olleH'
vm.message = 'Goodbye'
console.log(vm.reversedMessage) // => 'eybdooG'
For more better understanding you can visit
https://v2.vuejs.org/v2/guide/computed.html
I'm a beginner, this is probably more of a javascript problem than vue but anyway:
there a plugin for spreadsheet named handsontable and in the normal use you make the table by doing this
hot = new Handsontable(container, {option})
and then you can use the method like hot.loadData() etc..
To use handsontable with vuejs, there a wrapper we can find here https://github.com/handsontable/vue-handsontable-official. With the wrapper you make a table like this :
<template>
<div id="hot-preview">
<HotTable :root="root" :settings="hotSettings"></HotTable>
</div>
</template>
<script>
import HotTable from 'vue-handsontable-official';
import Vue from 'vue';
export default {
data: function() {
return {
root: 'test-hot',
hotSettings: {
data: [['sample', 'data']],
colHeaders: true
}
};
},
components: {
HotTable
}
mounted () {
localforage.config({
driver: localforage.INDEXEDDB,
name: 'matchlist-database'
})
localforage.getItem('DB').then(function (value) {
console.log('then i fetch the DB: ' + JSON.stringify(value))
if (value !== 'null') {
console.log('dB contain something')
**root**.loadData(value)
}
</script>
So it work fine when i give an array but to load the data from a DB you must call the handsontable method hot.loadData(data).
i cannot find how to call this method in vuejs i always get the error
TypeError: root.loadData is not a function
i tried with all i could think of instead of root ex: HotTable.loadData(value)
but to no avail
Can someone point me out how i would call handsontable methods from the vuejs wrapper. Or point me out what kind of reading i should do to understand my mistake. Thank a lot
There are two problems here, not bad ones :)
1st problem:
If you want to refer to your data inside Vue's methods/computed properties/watchers/lifecycle events, you should use the this keyword. If you have data: function() { return { root: "root-value" }} and you would like to console.log that "root-value" string, you should write console.log(this.root) inside your mounted handler.
If you had something like:
data: function() {
return {
hot = new Handsontable(container, {option})
....
};
You could call hot.loadData() like so:
mounted() {
this.hot.loadData();
...
}
So this refers to the Vue instance which exposes your data properties.
2nd problem:
If I understand the component wrapper correctly, you are supposed to pass data to it as props, not call any Handsontable methods directly.
<HotTable :root="root" :settings="hotSettings"></HotTable>
This means that Vue passes whatever you have as root in your data to the HotTable component. It also passes whatever you have as settings in your data. In the example, HotTable receives these:
root: 'test-hot',
hotSettings: {
data: [['sample', 'data']],
colHeaders: true
}
Now if you want to change/update/modify/add data that should be passed to the HotTable component, you should update your data in the Vue instance. You should do something like this.hotSettings = something new and this.root = something else and the HotTable component would receive those.
To understand what's really happnening with the HotTable, read all of the component documentation. Really. You will save lots of time if you read through the documentation. It all makes sense after that!
https://v2.vuejs.org/v2/guide/components.html
I'm using the vue-cli scaffold for webpack
My Vue component structure/heirarchy currently looks like the following:
App
PDF Template
Background
Dynamic Template Image
Static Template Image
Markdown
At the app level, I want a vuejs component method that can aggregate all of the child component's data into a single JSON object that can be sent off to the server.
Is there a way to access child component's data? Specifically, multiple layers deep?
If not, what is the best practice for passing down oberservable data/parameters, so that when it's modified by child components I have access to the new values? I'm trying to avoid hard dependencies between components, so as of right now, the only thing passed using component attributes are initialization values.
UPDATE:
Solid answers. Resources I found helpful after reviewing both answers:
Vuex and when to use it
Vuex alternative solution for smaller apps
In my child component, there are no buttons to emit changed data. It's a form with somewhat 5~10 inputs. the data will be submitted once you click the process button in another component. so, I can't emit every property when it's changing.
So, what I did,
In my parent component, I can access child's data from "ref"
e.g
<markdown ref="markdowndetails"></markdown>
<app-button #submit="process"></app-button>
// js
methods:{
process: function(){
// items is defined object inside data()
var markdowns = this.$refs.markdowndetails.items
}
}
Note: If you do this all over the application I suggest move to vuex instead.
For this kind of structure It's good to have some kind of Store.
VueJS provide solution for that, and It's called Vuex.If you are not ready to go with Vuex, you can create your own simple store.
Let's try with this
MarkdownStore.js
export default {
data: {
items: []
},
// Methods that you need, for e.g fetching data from server etc.
fetchData() {
// fetch logic
}
}
And now you can use those data everywhere, with importing this Store file
HomeView.vue
import MarkdownStore from '../stores/MarkdownStore'
export default {
data() {
sharedItems: MarkdownStore.data
},
created() {
MarkdownStore.fetchData()
}
}
So that's the basic flow that you could use, If you dont' want to go with Vuex.
what is the best practice for passing down oberservable data/parameters, so that when it's modified by child components I have access to the new values?
The flow of props is one way down, a child should never modify its props directly.
For a complex application, vuex is the solution, but for a simple case vuex is an overkill. Just like what #Belmin said, you can even use a plain JavaScript object for that, thanks to the reactivity system.
Another solution is using events. Vue has already implemented the EventEmitter interface, a child can use this.$emit('eventName', data) to communicate with its parent.
The parent will listen on the event like this: (#update is the shorthand of v-on:update)
<child :value="value" #update="onChildUpdate" />
and update the data in the event handler:
methods: {
onChildUpdate (newValue) {
this.value = newValue
}
}
Here is a simple example of custom events in Vue:
http://codepen.io/CodinCat/pen/ZBELjm?editors=1010
This is just parent-child communication, if a component needs to talk to its siblings, then you will need a global event bus, in Vue.js, you can just use an empty Vue instance:
const bus = new Vue()
// In component A
bus.$on('somethingUpdated', data => { ... })
// In component B
bus.$emit('somethingUpdated', newData)
you can meke ref to child component and use it as this
this.$refs.refComponentName.$data
parent-component
<template>
<section>
<childComponent ref="nameOfRef" />
</section>
</template>
methods: {
save() {
let Data = this.$refs.nameOfRef.$data;
}
},
In my case I have a registration form that I've broken down into components.
As suggested above I used $refs, In my parent I have for example:
In Template:
<Personal ref="personal" />
Script - Parent Component
export default {
components: {
Personal,
Employment
},
data() {
return {
personal: null,
education: null
}
},
mounted: function(){
this.personal = this.$refs.personal.model
this.education = this.$refs.education.model
}
}
This works well as the data is reactive.