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: ""
}
Related
I'm trying to use the v-calendar component from Vuetify.
I saw in the documentation I can use the event-start prop if my events don't have the same attributes' names.
The problem is that my events have embedded attributes and I don't know if event-start handles this case.
My events :
events: [
{
id: 'b9d93291-6d95-47b9-994a-ee9f266fb6b8',
type: 'reservation_item',
attributes: {
start_date: '2020-09-23T00:00:00.000Z',
end_date: '2020-09-25T00:00:00.000Z',
},
},
]
The events example from vuetify :
events: [
{
name: 'Weekly Meeting',
start: '2020-09-07 09:00',
end: '2020-09-07 10:00',
},
],
I tried to do something like that but it doesn't work.
<v-calendar
ref="calendar"
locale="fr-fr"
:now="today"
:value="today"
:events="events"
event-start="attributes.start"
color="primary"
type="month"
></v-calendar>
After spelunking the source code for the vuetify plugin, the latter expects that the value be present in the event object, as a direct property. So you cannot acces other nested "children", it has to be a direct property.
There are two alternatives to make this work:
1- map your events array by moving the properties inside attributes to the root of your object then pass this prop to v-calendar : event-start="startDate"
2- Create a javascript class (MyEvent) with a fromJson method that take the raw JSON from your API ( this way you encapsulate the JSON into domain objects) and return an array of MyEvent instances. this way you can do for example : events[0].start and you don't even have to pass it as a value to the event-start prop, since by default it expects a start attribute as a default value.
Another advantage of this alternative, is that since the event is now encapsulated into its own javascript class, you can add helper methods, or getters/setter or any logic that would otherwise be inside your "view" logic, and contribute to have a better separation of concerns.
To make this works, I had to change my events data
<template>
<v-calendar
ref="calendar"
locale="fr-fr"
:events="myEvents"
event-start="start"
color="primary"
type="month"
></v-calendar>
</template>
data: () => ({
events: [
{
id: 'b9d93291-6d95-47b9-994a-ee9f266fb6b8',
type: 'reservation_item',
attributes: {
start_date: '2020-09-23T00:00:00.000Z',
end_date: '2020-09-25T00:00:00.000Z',
},
},
],
}),
computed: {
myEvents() {
const reservations = this.reservations
reservations.forEach((element) => {
element.start = element.attributes.start_date
element.name = 'test'
element.end = element.attributes.end_date
})
return reservations
},
}
I have a Vuex state, like this (it also has a getter, named configs:
configs: {
1303401: {
exampleValue: 'test'
}
}
I also have an input where I v-model the exampleValue from the Vuex store's state:
<input type="text" v-model="config.exampleValue" />
Here is the computed property I use to return the config:
config: {
get () {
return this.$store.getters.configs[1303401]
},
set (value) {
//this should set the configs[1303401] field with the updated exampleValue
this.$store.dispatch('UPDATE_CONFIG', value)
}
}
The input's value changes to the value of config.exampleValue, so the computed data is not undefined, but the Vuex state does not update.
Also if I try to console.log the value in the setter, nothing appears in the console, so the setter isn't even executed
It is probably because it is not setting the config computed property, but the config.exampleValue, but I have no idea, how to handle that.
As #asi-ple mentioned above, changing the get to return configs[1303401].exampleValue would work, but the problem is, that the config has more fields, and the page has more inputs, and I'd need to create a computed property for all fields this way.
Actually you can make some logic here if you have more than fields.
Lets say that you have
configs: {
1303401: {
exampleValue: 'test',
exampleValue2: 'test2',
exampleValue3: 'test3',
...
}
}
Than you should change template to that:
<input type="text" :value="config[1303401].exampleValue1" #input="update_config($event, 'exampleValue1')" />
<input type="text" :value="config[1303401].exampleValue2" #input="update_config($event, 'exampleValue2')" />
<input type="text" :value="config[1303401].exampleValue3" #input="update_config($event, 'exampleValue3')" />
And your methods like this
methods:{
update_config($event, where){
this.$store.commit("UPDATE_CONFIG", {value: $event.target.data, where: where})
}
}
Then your mutation handler looks this
UPDATE_CONFIG(state, payload){
state.configs[1303401][payload.where] = payload.value
}
Basically above code, your making a config data in your state which should use two-way data binding in your template. Then you are creating your inputs :value working like get methods, and #input listener is working like set methods, then update_config is committing your changes and mutation handler setting them with right place
I think it's because you use v-model with property of the get object, and setter is not working on it.
You can just work with property only, here is how your computed property would look:
config: {
get () {
return this.$store.getters.configs[1303401].exampleValue
},
set (value) {
this.$store.dispatch('UPDATE_CONFIG', value)
}
}
and template:
<input type="text" v-model="config" />
So you will get new exampleValue value in your store action. And try to avoid using objects for getter/setter in computed properties (with forms), it has a lot of hidden traps.
I have an array of blog post inside a script tag on the page that looks like this:
var posts = [
{article_name: "name", category: "test".....},
{article_name: "name2", category: "test2".....},
{...}
]
There are 30 objects in each array. I need to grab the categories in each object and assign it to the data prop in a vue instance. I have an empty array inside the vue instance like:
var blog_posts_nav = new Vue({
el: '#blog-posts-nav',
data: {
tags: []
}
})
I want to have each individual "category" in the 30 objects mapped to the "tags" array in the data prop. I tried doing it via the created() hook but it seems that the created hook can't access data? I tried:
created() {
posts.forEach( function (item) {
this.tags.push(item.category)
});
}
But I get an error in the console that says tags is undefined. Any help how I would deal with this? Basically I want to do work on a set of data and assign it to an array inside vue before outputting it to the page and in a manner that vue can interact with the data.
So first and foremost, is the created() hook (or any hook) the best way to go about this? Or should I be using something like methods or computed?
What is the "best practices" way and how would I go about achieving this?
It's not the created() method that can't access this, it's the inner context of forEach.
You can capture this to a local constant first, then access it inside the loop.
console.clear()
var posts = [
{article_name: "name", category: "test" },
{article_name: "name2", category: "test2" },
]
Vue.component("tags",{
template:`
<div>
{{tags}}
</div>
`,
data(){
return {
tags: [],
}
},
created() {
const vm = this;
console.log('created', vm.tags)
posts.forEach( function (item) {
vm.tags.push(item.category)
console.log('created, posts.forEach', vm.tags)
});
}
})
new Vue({
el: "#app"
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.js"></script>
<div id="app">
<tags></tags>
</div>
I am converting a project from Angular to Web Components / Custom Elements and am trying to replace ng-model by creating a binding for the following text field:
<input type="search" class="form-control search_input" placeholder="Search for someone new" value$="[[userLookup:input]]" required autocomplete="off">
Obviously since this is converted from Angular, I need to be able to access this value in a JavaScript function:
(function(customElements) {
class RecentSearch extends PolymerMixins.LightDomMixin(Polymer.Element) {
static get is() { return 'recent-search'; }
static get properties() {
return {
properties: {
user: {
type: Object
},
userLookup: {
type: String,
reflectToAttribute: true,
value: '',
},
},
};
}
lookupUser() {
if (this.userlookup) {
$state.go('users', { query: userlookup });
}
};
}
customElements.define(RecentSearch.is, RecentSearch);
})(window.customElements);
How would I access the userLookup property (the one bound to the text field) from inside the lookupUser function?
You're already accessing userLookup correctly from lookupUser() with this.userLookup. The event handler's context (i.e., this) is the Polymer element instance.
However, your data binding is incorrectly a one-way data binding, so userLookup would not be updated. This kind of binding needs to be two-way (i.e., with curly brackets) and cannot use attribute binding (i.e., $=).
The correct usage should be something like this:
<input placeholder="Search for someone new"
value="{{userLookup::change}}" />
demo
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