I'm attempting to establish a watcher in Vue.js to duplicate an input conditionally. Using the value property, I keep experiencing null references, will someone elaborate to why this may be so I may better understand the issue?
My HTML:
<div id="company-form">
<label>Name</label>
<input v-model="legalName" type="text"/>
<label>Name To Call</label>
<input v-model="communicationsName" />
</div>
My Vue code:
new Vue({
el: '#company-form',
data: {
legalName: null,
communicationsName: null,
},
watch: {
legalName: function() {
if (!this.communicationsName.value || this.legalName.value == this.communicationsName.value) {
this.communicationsName.value = this.legalName.value;
}
}
},
});
Console Errors:
[Vue warn]: Error in callback for watcher "legalName": "TypeError: Cannot read property 'value' of null"
vue.js:18 TypeError: Cannot read property 'value' of null
The v-model directive is used to create two-way data bindings.
Instead of doing this.communicationsName.value just do this.communicationsName.
The data property communicationsName already holds the value you are looking for, it is not an HTMLInputElement instance for it to have a value property.
Try the following:
watch: {
legalName: function() {
//Check to see if communicationsName's value is null or equal to legalName's value before duplicating input field text
if (!this.communicationsName || this.legalName == this.communicationsName) {
this.communicationsName = this.legalName;
}
}
},
Note: The if condition this.legalName == this.communicationsName might not be necessary. The data properties already have the same value.
Use Computed Properties instead.
new Vue({
el: '#company-form',
data: {
communicationsName: null,
},
computed: {
legalName() {
return this.communicationsName
}
},
});
You can tweak this code as per your use case.
Related
This question already has answers here:
Access vue instance/data inside filter method
(3 answers)
Closed 2 years ago.
I'm creating a simple Vuejs div component (to show a specific value) which needs to receive: a lists, a placeholder and a value as props. What I'm trying to do is displaying the value with the data from my database, if the user picks a new value from the lists, it should take that new value and display it. However, if the user never picks a new value and the data from the database is empty, it should display the placeholder.
So I have used filters to achieve this. However, it outputs an error: "Cannot read property 'lists' of undefined", which comes from the filters (I know because it outputs no error if I comment out the filters). When I changed the filter to this:
filters: {
placeholderFilter () {
return this.placeholderText || this.placeholder
}
}
It says:""Cannot read property 'placeholderText' of undefined"". So I was wondering if the filters properties executed before the data and props properties. What is the execution order of them? I have attached some of the relevant code down below. Anyway, If you could come up with a better way to achieve this. I would appreciate it!
Here is my component:
<template>
<div>{{ placeholderText | placeholderFilter }}</div>
<li #click="pickItem(index)" v-for="(list,index) in lists" :key="index">{{ list }}</li>
</template>
<script>
export default {
props: {
lists: {
type: Array,
required: true
},
value: {
type: [String, Number],
default: ''
},
placeholder: {
type: String,
default: ''
}
},
data () {
return {
selected: -1,
placeholderText: this.value || this.placeholder
}
},
methods: {
pickItem (index) {
this.selected = index
}
},
filters: {
placeholderFilter () {
return this.lists[this.selected] || this.placeholderText || this.placeholder
}
}
}
</script>
And this is where I use it:
<my-component
placeholder="Please type something"
value="Data from database"
lists="['option1','option2','option3']"
>
</my-component>
Filters aren't bound to the component instance, so they simply don't have access to it through the this keyword. They are meant to always be passed a parameter and to return a transformed version of that parameter. So in other words, they're just methods. They were removed in Vue 3 entirely probably for that reason.
And yeah, what you're looking for here is a computed!
I'm coordinating input elements with the keys of an object on Vuex state. Let's say I've got this object:
myObj: { foo: 1, bar: 2 }
And a computed property in a component:
myObjVals: {
get(){ return this.$store.state.myObj },
set(//?) { //? },
},
In the template, I can get values from the store:
<input type="number" v-model="myObjVals['foo']"/> // displays "1"
<input type="number" v-model="myObjVals['bar']"/> // displays "2"
But when I adjust the value on an input, I get the error: "Do not mutate vuex store state outside mutation handlers."
One obvious solution here is to have a different computed property for each key in myObj... but for larger objects, this gets repetitious/cumbersome. I am wondering if there is any way to code this as I am attempting to above, that is, using only one computed property to reference the object for both get and set functions in v-model.
Come to think of your problem once more. One solution could be something suggested here: https://vuex.vuejs.org/guide/forms.html
<input type="number" :value="myObjVals['foo']" #input="changeMyObj('foo', $event)"/> // displays "1"
<input type="number" :value="myObjVals['bar']" #input="changeMyObj('bar', $event)"/> // displays "2"
with computed property and a method:
computed: {
myObjVals: function () {
return this.$store.state.myObj
}
},
methods: {
changeMyObj(key, evt) {
this.$store.commit('changeMyObj', {key: key, value: evt.data})
}
}
With a mutation inside the store:
mutations: {
changeMyObj(state, objInput) {
state.myObj[objInput.key] = objInput.value
}
}
Here is a "working" fiddle: http://jsfiddle.net/zfab6tzp/346/
Not sure if this is the best solution possible, but it seems to be working
Why I can't bind the object properties in Vue? The object addr is not reactive immediately, but test is reactive, how come? In this case, how should I bind it?
HTML
<div id="app">
<input type="text" id="contactNum" v-model="addr.contactNum" name="contactNum">
<input type="text" id="test" v-model="test" name="test">
<br/>
{{addr}}<br/>
{{addr.contactNum}}<br/>
{{test}}
</div>
Javascript
var vm = new Vue({
el: '#app',
data: {
addr: {},
test: ""
}
});
Jsfiddle
During initialisation Vue sets up getters and setters for every known property. Since contactNum isn't initially set up, Vue doesn't know about that property and can not update it properly. This can be easly fixed by adding contactNum to your addr object.
var vm = new Vue({
el: '#app',
data: {
addr: {
contactNum: "" // <-- this one
},
test: ""
}
});
The above is called reactivity in Vue. Since Vue doesn't support adding properties dynamically to its reactivity system, we may need some kind of workaround. A possible solution is provided by the API. In case of dynamically added properties we can use Vue.set(vm.someObject, 'b', 2).
Doing so the markup would need to get some update. Instead of using v-model it'd be better to use an event listener like #input. In this case our markup could look like this.
<input type="text" id="contactNum" #input="update(addr, 'contactNum', $event)" name="contactNum">
So basically the function will get triggered every time the input elements value changes. Obviously doing so will also require some adjustments on the JS part.
var vm = new Vue({
el: '#app',
data: {
addr: {},
test: ""
},
methods: {
update: function(obj, prop, event) {
Vue.set(obj, prop, event.target.value);
}
}
});
Since Vue triggers Vue.set() on any reactive element, we simply call it on our own because Vue doesn't recognizes a dynamically added property as a reactive one. Of course, this is only one possible solution and there may be lots of other workarounds. A fully working example can be seen here.
As per my comments, there are several things that you want to consider:
The reason why your code is not working is due to the inherent inability of JS to watch for changes in object properties. This means that even though addr is reactive, any properties added to addr that is not done when it is declared will make it non-reactive. Refer to the VueJS docs for more details: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
If you are going to have an arbitrary number of input fields, you are probably better of composing a custom input component, and simply use v-for to iteratively inject input fields based on the number of input fields you have.
Now back to the second point, if you know what fields addr will have, you can simply declare it in your app. We create a new updateFormData method, which is called by the component:
data: {
addrFields: ['contactNum', ...],
addr: {},
test: ""
},
methods: {
updateFormData: function(id, value) {
this.$set(this.addr, id, value);
}
}
We can still store your form data in the addr object, which will be updated by the updateFormData method based on the received payload using .$set(). Now, we can then create a custom Vue component for your input element.
In the example below, the component will iterate through all your addrFields, and pass down the addrField as a prop using :id="addrField". We also want to make sure that we capture the custom-named updated event emitted from within the component.
<my-input
v-for="(addrField, i) in addrFields"
:key="i"
:id="addrField"
v-on:inputUpdated="updateFormData"></my-input>
The template can look something like the following. It simply uses the id prop for both its id, name, and placeholder attribute (the latter for easy identification in the demo). We bind the #change and #input events, forcing it to trigger the updated callback:
<script type="text/template" id="my-input">
<input
type="text"
:id="id"
:name="id"
:placeholder="id"
#input="updated"
#change="updated">
</script>
In the component logic, you let it know that it will receive id as a prop, and that it should emit an inputUpdated event using $.emit(). We attach the ID and value as payloads, so that we can inform the parent what has updated:
var myInput = Vue.component('my-input', {
template: '#my-input',
props: {
id: {
type: String
}
},
methods: {
updated: function() {
this.$emit('inputUpdated', this.id, this.$el.value);
}
}
});
With the code above, we have a working example. In this case, I have created an arbirary array of input fields: contactNum, a, b, and c:
var myInput = Vue.component('my-input', {
template: '#my-input',
props: {
id: {
type: String
}
},
methods: {
updated: function() {
this.$emit('updated', this.id, this.$el.value);
}
}
});
var vm = new Vue({
el: '#app',
data: {
addrFields: ['contactNum', 'a', 'b', 'c'],
addr: {},
test: ""
},
methods: {
updateFormData: function(id, value) {
this.$set(this.addr, id, value);
}
}
});
<script src="https://unpkg.com/vue#2.1.3/dist/vue.js"></script>
<div id="app">
<my-input
v-for="(addrField, i) in addrFields"
:key="i"
:id="addrField"
v-on:updated="updateFormData"></my-input>
<input type="text" id="test" v-model="test" name="test" placeholder="test">
<br/>
<strong>addr:</strong> {{addr}}<br/>
<strong>addr.contactNum:</strong> {{addr.contactNum}}<br />
<strong>test:</strong> {{test}}
</div>
<script type="text/template" id="my-input">
<input
type="text"
:id="id"
:name="id"
:placeholder="id"
#input="updated"
#change="updated">
</script>
Edit your Vue data with this since it's getter and setter methods are not set up. Also, check out Declarative Reactive Rendering on Vue docs here:
data: {
addr: {
contactNum: "" // <-- this one
},
test: ""
}
I am trying to implement an associated array combined with accessing the property within the value, the key is based on the value of the campaign object.
<li v-for="campaign in campaigns">
<input type="text" v-model="configuration[campaign._id].value"> {{ configuration[campaign._id].value }}
</li>
https://jsfiddle.net/4yc3bujt/1/
Am I missing anything, it feels really bugged out. About exactly the same happens when trying to do this in VueJS 1, both times it's not throwing any errors.
This is happening due to caveats of reactivity in vue.js. You have just defined configuration: {} initially in data, so configuration[key] are not reactive. To make these reactive, you have to use this.$set:
Set a property on an object. If the object is reactive, ensure the property is created as a reactive property and trigger view updates. This is primarily used to get around the limitation that Vue cannot detect property additions.
use like following:
this.campaigns.forEach((campaign) => {
var id = campaign._id;
this.$set(this.configuration, id, {'value': 'default value'})
})
var app = new Vue({
el: '#app',
data: {
campaigns: [],
configuration: {}
},
mounted: function() {
this.campaigns = [{
_id: 'abcdefg'
}, {
_id: 'kejwkfe'
}, {
_id: 'iruwiwe'
}, {
_id: 'reiwehb'
}];
this.campaigns.forEach((campaign) => {
var id = campaign._id;
this.$set(this.configuration, id, {'value': 'default value'})
})
}
})
<script src="https://vuejs.org/js/vue.js"></script>
<div id="app">
<ul>
<li v-for="campaign in campaigns">
<input type="text" v-model="configuration[campaign._id].value" />
{{ configuration[campaign._id].value }}
</li>
</ul>
</div>
Properties with reactivity must be in a Vue instance which is created by Vue constructor.
It ensures that properties are reactive and trigger view updates, i.e., they are initialed with Object.defineProperty and MutationObserver underlyingly.
So, use Vue.set to add new properties, and delete by Vue.delete.
View more info - Reactivity in Depth
I'm not too entirely sure why my computed property isn't returning updated values.
I have a list of options that a user can click through, and the action updates a property, which is an Ember Object, for the controller. I have a computed property that loops through the object, looks for keys that have non-null values for that Ember Object property, and if it does find one, returns false, otherwise true.
Here's the stuff:
App.SimpleSearch = Ember.Object.extend({
init: function() {
this._super();
this.selectedOptions = Ember.Object.create({
"Application" : null,
"Installation" : null,
"Certification" : null,
"Recessed Mount" : null,
"Width" : null,
"Height" : null,
"Heating" : null,
"Power" : null
});
},
selectedOptions: {},
numOfOptions: 0,
allOptionsSelected: function() {
var selectedOptions = this.get('selectedOptions');
for (var option in selectedOptions) {
console.log(selectedOptions.hasOwnProperty(option));
console.log(selectedOptions[option] === null);
if (selectedOptions.hasOwnProperty(option)
&& selectedOptions[option] === null) return false;
}
return true;
}.property('selectedOptions')
});
App.SimpleSearchRoute = Ember.Route.extend({
model: function() {
return App.SimpleSearch.create({
'SimpleSearchOptions': App.SimpleSearchOptions,
'numOfOptions': App.SimpleSearchOptions.length
});
},
setupController: function(controller, model) {
controller.set('model', model);
}
});
App.SimpleSearchController = Ember.ObjectController.extend({
getProductsResult: function() {
var productsFromQuery;
return productsFromQuery;
},
setSelection: function (option, selectionValue) {
this.get('selectedOptions').set(option, selectionValue);
this.notifyPropertyChange('allOptionsSelected');
},
actions: {
registerSelection: function(option) {
console.log('registering selection');
console.log(this.get('allOptionsSelected'));
console.log(this.get('selectedOptions'));
this.setSelection(option.qname, option.value);
},
The action in the controller, registerSelection is firing just fine, but I only see the console.log from the SimpleSearch model once. Once the property is computed that first time, it isn't paid attention to after that, which means that the computed property isn't observing the changes to selectedOptions whenever this is called:
setSelection: function (option, selectionValue) {
this.get('selectedOptions').set(option, selectionValue);
this.notifyPropertyChange('allOptionsSelected');
},
Edit:
I actually solved my issue without changing anything.
I've changed the following line:
this.notifyPropertyChange('allOptionsSelected');
to:
this.get('model').notifyPropertyChange('selectedOptions');
notifyPropertyChange needs to be called within the context of the model (or the Ember Object that has observers of a specific property), and the string sent as an argument is the name of the property that was updated.
After I made that change, it worked as intended.
Ember doesn't observe objects for any change on the object, it observes a single property.
How is this affecting you? Well in this method you are watching selectedOptions, but that object itself is still the same object, you might be changing properties on it, but not the object itself. And then you are telling Ember in the scope of the controller that allOptionsSelected has changed, so it regrabs it, but it doesn't recalculate it because it's not dirty, it just changed. You'd really want to say selectedOptions has changed to get allOptionsSelected to recalculate its value. Unfortunately you're doing this in the scope of the controller, so telling the controller that property has changed doesn't matter to it.
allOptionsSelected: function() {
var selectedOptions = this.get('selectedOptions');
for (var option in selectedOptions) {
console.log(selectedOptions.hasOwnProperty(option));
console.log(selectedOptions[option] === null);
if (selectedOptions.hasOwnProperty(option)
&& selectedOptions[option] === null) return false;
}
return true;
}.property('selectedOptions')
Here's a dummy example showing what things cause it to actually update.
http://emberjs.jsbin.com/iCoRUqoB/1/edit
Honestly since you're not watching particular properties I'd probably do an array, or create a method on the object that handles adding/removing/modifying the properties so you could fire from within it's scope a property change updating all parent listeners with the changes.
Ember computed property dependent keys can be a
property key. in example: 'jobTitle'
computed property key. in example: 'companyName'
property key of an object. in example:
'salesAccount.name and salesAccount.website'
Example extracted from Ember.model definition:
...
jobTitle : DS.attr('string'),
salesAccount: belongsTo('salesAccount'),
companyName: Ember.computed('jobTitle', 'salesAccount.name', {
get: function () {
return this.get('salesAccount.name');
}
}),
companyWebsite: Ember.computed('salesAccount.website', 'companyName', {
get: function () {
return this.get('salesAccount.website');
}
})
...