This should be really simple.
I have an associative observable array with a name and a boolean value.
this.items = ko.observableArray([
{ name: "name1", boolVal: true },
{ name: "name2", boolVal: true },
]);
Then a simple function to change boolVal.
this.changeValue = function (item) {
item.boolVal = false;
};
When I call the changeValue function, boolVal does change (see console.log(data) in my jsfiddle) but the view doesn't update. The value on the screen remains "true". I must be making an incorrect assumption regarding how KnockoutJS works.
JS Fiddle Link
In order to the KO update UI you need to have observable properties:
this.items = ko.observableArray([
{ name: "name1", boolVal: ko.observable(true) },
{ name: "name2", boolVal: ko.observable(true) },
]);
And set it with:
this.changeValue = function (item) {
item.boolVal(false);
};
The ko.observableArray only tracks item addition and removal. So it won't notify the UI if one of its items changed. For that you need to have ko.observable on the items.
Demo JSFiddle.
Related
I have no idea if what I'm doing is correct or not, but here's a simplified version of what I'm trying to do:
I want to have 3 file inputs, with the 2nd and 3rd disabled until the 1st one has had a file selected.
I've tried to do is set the Vuex state variable to whatever the first file input is has selected, but upon doing that the other 2 inputs don't update their disabled state.
I have some file inputs that are created dynamically, like so:
Vue.component('file-input', {
props: ['items'],
template: `<div><input type="file" v-on:change="fileSelect(item)" v-bind:id="item.id" v-bind:disabled="disabledState"></div>`,
methods: {
fileSelect: function(item) {
store.commit('fileSelect', file);
}
},
computed: {
disabledState: function (item) {
return {
disabled: item.dependsOn && store.getters.getStateValue(item.dependsOn)
}
}
}
}
The data for the component is from the instance:
var vm = new Vue({
data: {
items: [
{ text: "One", id: "selectOne" },
{ text: "Two", id: "selectTwo", dependsOn: "fileOne" },
{ text: "Three", id: "selectThree", dependsOn: "fileOne" }
}
});
Now, notice the "dependsOn". In the Vuex store, I have a corresponding state item:
const store = new Vuex.Store({
state: {
files: [
{
fileOne: null
}
]
},
mutations: {
fileSelect(state, file) {
state.files.fileOne = file;
}
},
getters: {
getStateValue: (state) => (stateObject) => {
return state.files.findIndex(x => x[stateObject] === null) === 0 ? true : false;
}
}
});
Now, the above works when everything is first initialized. But once the first input has something selected, the other two inputs don't change.
I'm not sure how to update the bindings once a mutation of the state occurs.
I think you need to refactor your mutation to make the state property mutable, like this:
fileSelect(state, file) {
Vue.set(state.files[0].fileOne, file);
}
Well, I figured it out...
Because my state object is an array of objects, I can't just change one of the property's values with state.files.fileOne. I needed to do state.files[0].fileOne.
I have to add extra rows forcing Vue to recompute computed prop, specifically:
var foo = this.groups;
this.groups = {};
this.groups = foo;
as can be seen in this fiddle: https://jsfiddle.net/8bqv29dg/. Without these, available_groups is not updated.
Why is that and what is the clean way to have available_groups updating with groups?
Have tried adding groups to "deep-watched", but it did not help.
Use $set to add new property for data object:
methods: {
add_group: function(key, name) {
this.$set(this.groups, key, {key, name});
},
}
Here described vue reactivity
Vue doesn't track new elements added to an object:
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
One solution is to use Vue.set or reassign the object, like the example below:
new Vue({
el: "#app",
data: {
groups: {1: {key: 1, label: 'Those guys'}},
},
computed: {
available_groups: function() {
return [{value: 0, label: 'Anyone'}].concat(Object.values(this.groups));
},
},
methods: {
add_group: function(key, name) {
Vue.set(this.groups, key, {key: key, name: name})
},
}
})
I've made my filters section using vue.js. I inject all the components through ajax and they response dynamically to those filters. Components in my case represent cars, they have price, marks, etc...
Now I'd like to add two more filters that allow me to sort them by some field (price, for instance). I've been reading and it's quite easy to sort lists specifying a field and a order...
How should I proceed to create that list and so, being able to sort it.
Here I made a little fiddle, very simple, in which I'd to sort the cars by prize once I click the filter.
var Car = Vue.extend({
template: '#template_box_car',
props: {
show: {
default: true
},
code: {
default: ""
},
prize: {
default: 0
},
description: {
default: "No comment"
}
}
});
//register component
Vue.component('box_car',Car);
//create a root instance
var vm = new Vue({
el: 'body',
methods: {
sortBy: function(field, order){
}
}
});
First, store the data for each car component in a data property in the parent component:
data: function () {
return {
cars: [
{ code: '11A', prize: 5.00, description: 'Ford Ka' },
{ code: '11B', prize: 3.00, description: 'Kia ceed' },
{ code: '11C', prize: 6.00, description: 'Ford Ka' },
{ code: '13A', prize: 45.00, description: 'Mercedes A' },
{ code: '17B', prize: 20.00, description: 'Seat Leon' },
]
}
},
Then, use the v-for directive to create a box_carcomponent for each of the objects in your cars data property:
// In your version of Vue.js it would look like this:
<box_car
v-for="car in cars"
:code="car.code"
:prize="car.prize"
:description="car.description"
:track-by="code"
></box_car>
// In newer versions of Vue.js, you can pass each object to the `v-bind` directive
// so you don't need to explicitly set each property:
<box_car v-for="car in cars" v-bind="car" :key="car.code"></box_car>
Then, in your sortBy method, simply sort the cars array:
// I used lodash, but you can sort it however you want:
methods: {
sortBy: function(field, order) {
this.cars = _.orderBy(this.cars, field, order);
}
}
Here's a working fiddle.
This question already has answers here:
Knockout JS - How to correctly bind an observableArray
(2 answers)
Closed 9 years ago.
I have been looking into an Knockout for dynamic data-bind and I have a situation where I need an observable array to contain multiple observable objects.
This is my code:
<ul data-bind="foreach: { data: categories, as: 'category' }">
<li>
<ul data-bind="foreach: { data: items, as: 'item' }">
<li>
<span data-bind="text: category.name"></span>:
<span data-bind="text: item"></span>
<input type="text" data-bind="value: item"/>
</li>
</ul>
</li>
</ul>
$(document).ready(function () {
ko.applyBindings(viewModel);
});
var viewModel = {
categories: ko.observableArray([
{ name: 'Fruit', items: [ko.observable('Apple'), ko.observable('Orange'), ko.observable('Banana')] },
{ name: 'Vegetables', items: [ko.observable('Celery'), ko.observable('Corn'), ko.observable('Spinach')] }
])
};
When working with oject observables usually I could modify a value of an input text box and that value is set to the entire page where that property was used to be displayed.
In my current example I tried to do the same with my input box , but after I modified the values in the text box the span did not to the curent value.
How can I make the observable objects inside the observableArray behave as they would have if they were stand alone observable objects?
when i encounter these issues, i like to break them down into sub vms, which allow me better control over what is happening at each level of context that im in. for your issues above, i would do something like this:
var produceVM = function (data) {
var self = this;
self.item = ko.observable(data);
}
var categoryVM = function (data) {
var self = this;
self.name = ko.observable(data.name);
self.items = ko.observableArray();
var items = ko.utils.arrayMap(data.items, function (item) {
return new produceVM(item);
});
self.items(items);
}
var viewModel = function (data) {
var self = this;
self.categories = ko.observableArray();
var categories = ko.utils.arrayMap(data, function (category) {
return new categoryVM(category);
});
self.categories(categories);
}
var data = [
{ name: 'Fruit', items: [ 'Apple', 'Orange', 'Banana' ] },
{ name: 'Vegetables', items: ['Celery', 'Corn', 'Spinach' ]}
];
ko.applyBindings(new viewModel(data));
I believe the ko mapping plugin achieves something similar to this without having to write all of the above code. you could pass it the data model and it would construct the observables for each item.
As #nemesv pointed, answer lies right under the corner.
Simply wrap every array item into object and it will work flawlessly.
This is how your view model should look, and here is a working jsfiddle
var viewModel = {
categories: ko.observableArray([{
name: 'Fruit',
items: [
{name: ko.observable('Apple')},
{name: ko.observable('Orange')},
{name: ko.observable('Banana')}
]
}, {
name: 'Vegetables',
items: [
{name: ko.observable('Celery')},
{name: ko.observable('Corn')},
{name: ko.observable('Spinach')}
]
}])
};
I have to say from my own experience, that usually you'll have objects inside array anyway, not claiming that there isn't other use cases, just saying that it is very useful to have objects in array and have ability to change them dynamically and not to worry about anything else.
I receive a complex JSON from the server. Let it be next:
var data = [{
name: "name1",
items:[
{
name:"name11",
subItems:[{
name:"name111",
children[
{id:1,name:"child1111",status:"good"},
{id:2,name:"child1112",status:"bad"},
{id:3,name:"child1113",status:"good"}
]},
{
name:"name112",
children[
{id:4,name:"child1121",status:"good"}]
}]
},
{
name:"name12",
subItems:[{
name:"name121",
children[
{id:5,name:"child1211",status:"bad"}]
}]
}]
},
{
name: "name2",
items:[
{
name:"name21",
subItems:[{
name:"name111",
children[
{id:7,name:"child2111",status:"good"}
]}]
}]
}];
So I have the list of objects each one contains name and items properties. Items is property of the similar list of objects each one contains name and subItems properties. subItems property same to previous and has name and children properties. children is list of my entities. I use mapping for filling my ViewModel.
First of all I can't image how to set as key id in my entity. I am wondering how to "dive" to it. Moreover, I need to extend my entity. Add the compute property like next example:
computProp: ko.computed(function() {return name+status;})
I don't want to create js classes, because I don't see benefits of mapping on this case. I can implement manual mapping in this case. It would be more clear for me.
So any idea, suggesting or critics are welcome.
PS: I have searched/read similar topics
You must explicit declare the children viewmodel to get this behaviour, but you still benefit from getting all the mapping done
http://jsfiddle.net/uXMhA/
ChildViewModel = function(data) {
ko.mapping.fromJS(data, {}, this);
this.computProp = ko.computed(function() {
return this.name() + this.status();
}, this);
};