How to pass object handler into vue component - javascript

I want to cache state inside a handler passed by props.
Problem is, the template renders and text content changes well, but the console always throw error:
[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'apply' of undefined"
Codes below:
<template>
<picker-box :options="item.options" #change="handlePickerChange($event, item)">
<text>{{ item.options[getPickerValue(item)].text }}</text>
</picker-box>
</template>
<script>
export default {
props: {
item: {
type: Object,
default() {
return {
options: [{ text: 'op1' }, { text: 'op2' }],
handler: {
current: 0,
get() {
return this.current
},
set(value) {
this.current = value
},
},
/* I also tried this: */
// new (function () {
// this.current = 0
// this.get = () => {
// return this.current
// }
// this.set = (value) => {
// this.current = value
// }
// })(),
}
},
},
},
methods: {
handlePickerChange(value, item) {
item.handler.set(value)
},
getPickerValue(item) {
return item.handler.get()
},
},
}
</script>
I know it's easy using data() or model prop, but I hope to cahce as this handler.current, in other words, I just want to know why this handler object isn't correct (syntax layer), how can I fix it.

What exactly state are you trying pass?
If you need to keep track of a static property, you could use client's localstorage, or a watcher for this.
If you need to keep of more dynamic data, you could pass it to a computed property that keeps track and sets them everytime they change.

Related

Vue custom directive to simulate getter/setter of computed property

There is an input tag when user type something, I want to add some symbols to the input value. Well, when I need the real value I should remove those symbols from the value. I know I can achieve this using computed property like following:
<template>
<input type="text" v-model="computedTest" /><!-- shows 20$ -->
</template>
<script>
export default {
data() {
return {
test: 20,
}
},
computed: {
computedTest: {
get() {
return this.test + "$"
},
set(val) {
this.test = this.val.replace(/$/g, "")
},
},
methods: {
doSomething() {
console.log(this.test) // 20
},
},
},
}
</script>
Since Vuejs can do this using computed property feature, I believe I can achieve this feature using custom directive. But, I have no idea of the way to do. This is the first try what I did:
<template>
<input type="text" v-model="test" v-custom />
</template>
<script>
export default {
data() {
return {
test: 0
}
},
}
</script>
And the code for v-custom directive:
export default {
bind: function(el, binding, vnode) {
Vue.set(el, "value", toLanguage(el.value, "en")) // toLanguage is a custom function
},
componentUpdated: function(el, binding, vnode) {
// getContext is a custom function and the `contextObj` variable will be equal to
// the whole data property of the component context and the `model` will be equal to
// the v-model expression (in this example, `test`).
let { contextObj, model } = getContext(vnode)
const vnodeValue = get(contextObj, model) // lodash `get` function
// parsing v-model value to remove symbols
const parsedValue = parseInputValue(el, vnodeValue) ?? vnodeValue
if (contextObj) {
Vue.set(contextObj, model, parsedValue)
}
el.value = toLanguage(el.value, "en")
el.dispatchEvent(new Event("input", { bubbles: true }))
},
}
But this directive snippet creates an infinite loop. I'm using Vue2.x. I'll appreciate anybody can help.

Vue - dynamically created computed property not found on page refresh

I have these computed properties in a component:
computed: {
messageText: {
get() {
return this.getMessageProp('messageText') // Maps to a vuex getter
},
set(value) {
this.setMessageProp(['messageText', value]) // Maps to a vuex mutation
}
},
location: {
get() {
return this.getMessageProp('location')
},
set(value) {
this.setMessageProp(['location', value])
}
}
}
This works on browser refresh. As you can see it's a bit repetitive (There are a few more in other components).
I tried to create them dynamically like this:
data() {
return {
stepProps: {
messageText: {},
location: {},
}
}
},
created() {
Object.keys(this.stepProps).forEach((key) => {
if (!this.$options.computed[key]) {
this.$options.computed[key] = {
get() {
return this.getMessageProp(key)
},
set(value) {
this.setMessageProp([key, value])
}
}
}
})
}
But it does not work on browser refresh. I get the error:
Property or method "messageText" is not defined on the instance but referenced during render....
It works when switching components (I am using Vue router)
How can I get this, or something similar, to work? Ultimately I wanted to put it into a mixin to reuse in other components.

Vue Computed Property not updating. Very strange behaviour

Yes, it's another 'Vue computed property is not updating question...
Below is an excerpt of my component with the issue. I have a computed property 'fieldModel' this uses Vue.set to set a new value, then i console log that computed property immediately after assigning it a new value the javascript object updates and is viewable in devtools, the computed property however has not updated, and neither has the DOM.
export default {
props:{
value:{
type:Object,
required:true,
}
},
data() {
return {
model:this.value,
key:'something',
}
},
created() {
var self = this;
setTimeout(function() {
self.fieldModel = 'Apples';
}, 1000);
},
computed:{
fieldModel:{
get() {
return this.model[this.key];
},
set(value) {
var self = this;
self.$set(self.model, self.key, value);
console.log(self.model[self.key], self.fieldModel);
//Logs out 'Apples', undefined,
}
}
}
}
The example code i posted in the original question works correctly, This lead me to break down my code and resolve my issue.
I had this component in a v-for loop with recursive nesting, Another component appeared to mutate the v-model object without updating these components resulting in some very strange behaviour.
I was able to solve the problem by adding a watcher for 'value' to update the model field and a watcher for 'model' to $emit('input') any changes to the model to it's parent.
This results in an infinite loop that crashes the browser, however i was able to resolve that by adding a check to see if the model/value is the same object
Example code is simplified for brevity:
{
watch:{
value(newValue) {
if(this.model != newValue) {
this.model = newValue;
}
},
model(newModel) {
this.$emit('input', newModel)
},
}
}

Vue prop undefined in initial call to computed functions

I have the following Vue component:
Vue.component('result', {
props: ['stuff'],
data: () => ({}),
template: "<img :src='tag' class='result'></img>",
computed: {
tag: function tag() {
return `pages/search/img/${this.stuff.type.toLowerCase()}_tag.png`;
}
}
});
When the component is created, an error is thrown:
TypeError: Cannot read property 'toLowerCase' of undefined
at VueComponent.tag
However, when I remove the call to toLowerCase(), the method executes properly, generating a string with the expected type. I could work around this by changing my filenames to have capital letters, but I would rather understand why Vue is behaving this way. Why would a property be undefined only when methods are called on it?
Update: after some troubleshooting, I found that this.stuff.type is undefined the first time tag() is computed. Calling toLowerCase() just forces an error on an otherwise silent bug. Is there a reason the props aren't defined when computed functions are called for the first time? Should I be writing my component differently?
The stuff prop is undefined when the result component is created.
There are two options to fix this problem:
Either use the v-if directive in the parent component's template to make sure stuff has a value when the result component is created:
<template>
<result v-if="stuff" :stuff="stuff" />
</template>
Or handle the stuff prop being undefined in the result component.
Vue.component('result', {
props: {
// Object with a default value
stuff: {
type: Object,
// Object or array defaults must be returned from
// a factory function
default: () => ({ type: 'someType'})
},
},
data: () => ({}),
template: "<img :src='tag' class='result' >",
computed: {
tag: function tag() {
return `pages/search/img/${this.stuff.type.toLowerCase()}_tag.png`;
}
}
})
Note: The img element is a void element, it does not require an end tag.
Props are by default null but you can give them a default value to overcome this problem.
Example :
Vue.component('result', {
props: {
stuff: {
type: Object,
default: {
type: ''
}
}
},
data: () => ({}),
template: "<img :src='tag' class='result'></img>",
computed: {
tag: function tag() {
return `pages/search/img/${this.stuff.type.toLowerCase()}_tag.png`;
}
}
});

Vue: Accessing data from an unmounted component

I have an issue where I want to retreive data from a child component, but the parent needs to use that data, before the child is mounted.
My parent looks like this
<template>
<component :is="childComp" #mounted="setData"/>
</template>
<script>
data : {
childComp : null,
importantData : null
},
methods : {
addComponent : function() {
this.prepareToAdd(this.importantData);
this.childComp = "componentA"; //sometimes will be other component
},
setData : function(value) {
this.importantData = value;
},
prepareToAdd : function(importantData){
//something that has to be run before childComp can be mounted.
}
}
</script>
My child (or rather, all the potential children) would contain something like this:
<script>
data : {
importantData : 'ABC',
},
created: function() {
this.$emit('mounted', this.importantData);
},
</script>
This clearly doesn't work - importantData is set when the childComponent is mounted, but prepareToAdd needs that data first.
Is there another way of reaching in to the child component and accessing its data, before it is mounted?
You can use $options to store your important data and have it available in beforeCreate. You can also use it to initialize a data item, and you can emit data items in created (you don't have to initialize from $options to emit in created, I'm just pointing out two things that can be done). The $options value is, itself, reactive (to my surprise) and can be used like any data item, with the added benefit that it is available before other data items.
new Vue({
el: '#app',
methods: {
doStuff(val) {
console.log("Got", val);
}
},
components: {
theChild: {
template: '<div>Values are {{$options.importantData}} and {{otherData}}</div>',
importantData: 'abc',
data() {
return {
otherData: this.$options.importantData
};
},
beforeCreate() {
this.$emit('before-create', this.$options.importantData);
},
created() {
this.$emit('on-created', this.otherData + ' again');
// It's reactive?!?!?
this.$options.importantData = 'changed';
}
}
}
});
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<the-child #before-create="doStuff" #on-created="doStuff"></the-child>
</div>
My bad :(
We cannot get the data inside beforeCreated() hook.
Use the beforeCreate() hook instead of created() hook:
beforeCreate: function() {
this.$emit('mounted', this.importantData);
},
We can use a watcher or computed option, so now your parent component would look:
data: {
childComp: null,
importantData: null,
isDataSet: false
},
methods: {
addComponent: function() {
this.prepareToAdd(this.importantData);
this.childComp = "componentA"; //sometimes will be other component
},
setData: function(value) {
this.importantData = value;
this.isDataSet = true;
},
prepareToAdd: function(importantData) {
//something that has to be run before childComp can be mounted.
}
},
watch: {
isDataSet: function(newValue, oldValue) {
if (newValue) {
this.addComponent();
}
}
}
I would suggest to use computed method as it caches the results. Use watcher when you have to perform asynchronous code.

Categories

Resources