This may well be a very basic problem for anyone familiar with knockout.js, however it is causing me a problem.
I have a situation where I have a model containing an array of items that is dynamically added to and displayed in the view.
So far no problem, I can add entries into the model and the view is updated appropriately.
However. each item in the array itself has an array as a property, this is an array of object, and when I update the properties on these objects the view is not updated.
It is difficult to demonstrate this is a short code snippet so I have created a JsFiddle to show the problem:
https://jsfiddle.net/mikewardle/t0nvwqvL/1/
I have tries making the properties generated by calling
ko.observable()
rather than initializing them directly, but to no avail.
clicking the add button adds items to the array on the model itself.
either of the change... buttons alters the properties of the objects in the inner array.
As Ko2r stated your properties are not declared as observables and therefore updates will not be noticed by knockout.
To fix your changecolors() function you just need to change your linePusher function to create the color as an observable:
var linePusher = function (color, name) {
self.lines.push({ color: ko.observable(color), name: name, current:0 });
};
and then update usages of the color property to box/unbox the observable instead of replacing its value with the standard assignment operator, "="
for (i=0;i<counters.length;i++){
var lines = counters[i].lines();
for (j=0;j<lines.length;j++){
//lines[j].color = color;
lines[j].color(color); //sets the existing observable to the new value
}
}
Unfortunately I can't seem to make sense of your code enough to figure out what the increment() function is supposed to be doing so I can't tell you how to fix it, but hopefully the fixes to changecolors() put you on the right track.
You might want to read up on working with observables
Related
I want to use knockout observables without the viewmodel. I simply want to use one observableArray as a data source for a DevExtreme data grid. So for now, my idea was fairly simple: I just declared a variable (shuttleList) as empty ko.observableArray. Later, I fill that up through an ajax request. My grid is set to that variable as data source.
However, nothing happens, as I change the array. Still, I have to manually replace the dataSource of the grid using its option method. What am I doing wrong?
shuttleList = ko.observableArray([]);
$.getJSON('http://someCall?ID=' + id, function (e) {
shuttleList(e.tourenList.find(x => x.title == 'Base').shuttleList);
});
var grid = $("#gridContainer").dxDataGrid({
dataSource: shuttleList,
…
});
I know that this is not the way knockout is supposed to be used, but can I somehow make this work automatically – without doing some manual grid refreshing in the shuttleList.subscribe event?
By the way, just calling the grid's refresh()/repaint() methods doesn't help either. I haven't found a way around resetting its dataSource option yet.
grid.option('dataSource', shuttleList);
Thank you very much in advance for your help!
I think all you're missing is to initialize the knockout bindings with ko.applyBindings, and you can use a specific element when calling that function. The "view-model" can be as simple as object-brackets with your variable inside:
ko.applyBindings({shuttleList}, document.getElementById("gridContainer"));
var shuttleList = ko.observableArray([]);
setTimeout(function(){shuttleList.push("success!")}, 1000);
ko.applyBindings({shuttleList}, document.getElementById("gridContainer"));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<span>waiting for update...</span><br/>
<span id="gridContainer" data-bind="text: shuttleList"></span>
I know that this is not the way knockout is supposed to be used, but
can I somehow make this work automatically – without doing some manual
grid refreshing in the shuttleList.subscribe event?
The only way to make KO to update something automagically is creating dependency. So in your case you can create anonymous computed that will depend on shuttleList observable array:
ko.computed(function(){
grid.option('dataSource', shuttleList());
});
As you can see the observable array is called there. That's what creates dependency. Now any change made within shuttleList array will re-evaluate the function passed to computed and grid's dataSource will be updated.
However it doesn't seem optimal solution because pushing even single value will cause whole grid to render. But if you data doesn't contain too many rows then this approach will work good.
Furthermore there is another drawback - since the computed creates one-way dependency you have no way to reflect changes on the shuttleList array by using grid's API methods.
When I do a push or pop operation on my observable array, it is reflected in the ui. However other operations on the array won't change anything in the UI. Here's an example of my case:
<ul data-bind="foreach: addresses">
<!-- ko template: {name: 'AddressItemTemplate', data: {address: $data, page: 'update-page'} }-->
<!-- /ko -->
</ul>
I use my template in two different pages and thats the reason I am using the template data like that.
<script type="text/html" id="AddressItemTemplate">
<p data-bind="text: (page == 'update-page') ? 'updating' : 'declined'"</p>
<p data-bind="text: address.title"></p>
</script>
Now on js side, ofc I declared the addresses as an observable array
this.addresses = ko.observableArray([addresObject1, addressObject2, ...])
Somewhere on the page, I edit the address values. To have UI reflecting the changes, I do the following:
//suppose we know that the first address is being edited
var tmp_addresses = addresses();
tmp_addresses[0].title = 'blabla';
addresses(tmp_addresses);
And there it is, in the viewModel, I can see that the content of the addresses has been updated, but not in the UI??
addresses.push(someAddressObject);
or
addresses.pop();
works (updates the UI with the new/removed element). But addresses.splice(0, 1, newAddressObject) does not do anything in the UI again.
What am I missing here? How can push pop work and not the others??
Am I experiencing a bug in knockout framework?
UPDATE
I found out a way to do it, but there's something wrong. I'll come to that but first:
I am well aware that if I use observable objects in the observable array, the changes would be reflected in UI. However that is exactly the thing I want to avoid. It is an overkill.
Observable properties should be required in cases where properties are really exposed to user interaction. For example, if you have a UI for setting each of the fields of an object, then yes, observable property would be the right call.
However in my case, I dont even have a UI for updating the address field. Moreover, I dont need tinkering and constantly watching all the properties of all the addresses. In my case, every now and then an update occurs from the server and that changes only a single field in a single address field.
On another perspective the way I suggest should work. I simply update the whole array at once, not every element individually. It's the exactly the same logic with:
someObservableObject({newObject: withNewFields, ...});
Thats why I dont need my objects as observables. I simply want to re-declare the array and be done with the change. For example, it is advised that if you are going to make lots of pushes into the observable array, dont use array.push(...) multiple times, instead re-declare the larger array on to the observable array variable in a similar way I do it in my question. Otherwise, I am telling knockout to track every single object and every single field in them, which is hardly what I want.
Now, I finally got it working but the way I do suggests that there is a cleaner way to do it.
I found out that, the items in the observable array are somehow tracked and not updated when you re-declare the array with them. For example the code I gave in the question would not work. However the code below works:
var tmp_addresses = addresses();
var tmp_addr = tmp_addresses[0];
var new_addr = {};
Object.keys(tmp_addr).forEach(function(key){
new_addr[key] = tmp_addr[key];
});
new_addr.title = 'Hey this is something new!'
addresses.splice(0, 1, new_addr);
Not satisfied? The code below is going to work as well, because we are re-defining the array:
var newAddressObject1 = {...}, newAddressObject2 = {...};
addresses([newAddressObject1, newAddressObject2]);
But the following would not work!
var tmp_addresses = addresses();
var tmp_addr = tmp_addresses[0];
tmp_addr.title = 'Hey this address wont update';
addresses.splice(0, 1, tmp_addr);
How come? I think knockout adds an internal property to his items in observableArrays and when I try to reinsert one, it will not update.
My problem has now morphed into creating a new object with the same properties of the desired item in the observable array. The way I coded above is simply very dirty-looking. There's gotta be a better way to do that
You are wrongly assigning value to observable title that is the reason why UI not reflecting its changes (2 way binding broken).
Thumb rule is always use () notation while assigning a value to observable (keeps two way binding intact)
viewModel:
var ViewModel = function () {
var self = this;
self.addresses = ko.observableArray([{
'title': ko.observable('one')
}, {
'title': ko.observable('two')
}])
setTimeout(function () {
var tmp_addresses = self.addresses();
tmp_addresses[0].title('blabla'); //assigning data to observable
self.addresses(tmp_addresses);
}, 2000)
};
ko.applyBindings(new ViewModel());
working sample here
PS: Don't get deceived by seeing the value change in viewModel the moment you done assigning using = two binding is broken UI wont reflect VM'S changes .
when you splice up your observableArray UI takes it changes check here
The problem was exactly as #jason9187 pointed out in the comments: The references of the objects in the observable array does not change when I edit a field of them. Therefore, KO would not interpret my array as changed. If the observableArray had contained simple data types, then the way I suggested could work without a problem. However, I have an Object in the array, therefore although I edit the Object, it's reference (pointer) remains the same, and KO thinks that all Objects are the same as before.
In order to achieve what I wanted, we have to solve the deep cloning problem in javascript like in this post.
Now there's a trade-off there, deep cloning is very simple in vanilla if you don't have a circular architecture or functions in your objects. In my case, there's nothing like that. The data comes from a restful API. If anybody in the future gets hold of this problem, they need to deep-clone their 'hard-to-clone' objects.
Here's my solution:
var tmp_addresses = JSON.parse(JSON.stringify(addresses())); //Creates a new array with new references and data
tmp_addresses[0].title = 'my new title';
addresses(tmp_addresses);
Or, if you can create address objects, following will work as well:
var tmp_addresses = addresses();
tmp_addresses[0] = new randomAddressObject();
addresses(tmp_addresses);
Here is a fiddle that I demonstrate both of the methods in a single example
This JSBin isolates a problem I ran into in my code. I have a hierarchy of embedded models and a computed property (data) that is supposed to fire whenever a value at the very bottom of the chain changes (symbol). The example displays the property directly as well as the result of the computed property. A button changes the value on click. You'll see that it updates the property but the computed property doesn't fire. Why doesn't selectedAllocation.positions.#each.instrument.symbol work to trigger the computation when any instrument.symbol changes?
If the example seems contrived, it's only because I tried to abstract something that is more complex in reality, e.g. there is more than just one object in these arrays and data is necessary because another library expects a simple JS object in a particular format.
Note that #each only works one level deep. You cannot use nested forms
like todos.#each.owner.name or todos.#each.owner.#each.name.
http://emberjs.com/guides/object-model/computed-properties-and-aggregate-data/
You'll need to create an alias to bring symbol up one level (Not a coffeescript guy, hopefully you can read through the hacking, the positions alias below is for kicks and giggles, makes no difference).
App.Position = Ember.Object.extend({
instrumentSymbol: Em.computed.alias('instrument.symbol')
})
App.IndexController = Ember.ArrayController.extend
selectedAllocation: null
positions: Em.computed.alias('selectedAllocation.positions'),
data: (->
#get("positions").map (position) ->
symbol: position.get "instrumentSymbol"
).property "positions.#each.instrumentSymbol"
...
http://jsbin.com/bivoyako/1/edit
I've posted my code here: http://jsfiddle.net/HYDU6/6/
It's a pretty stripped-down version of what I'm actually working with, but captures the essence of my problem. My view model is like so:
var viewModel = {
objects: {
foo: [
{ text: "Foo's initial" },
],
bar: [
{ text: "Bar's initial" },
]
}
}
I'm using the ko.mapping plugin and my create handler for objects instantiates Obj from objects.foo and then objects.bar, returning the resulting two items in an array. This part works fine; I use
var view = {};
ko.mapping.fromJS(viewModel, mapping, view);
My issue is updating based on new data. (i.e., getting data from the server). I have an object of new data and I attempt
ko.mapping.fromJS(new_model, mapping, view);
I suspect this is incorrect but I have not been able to get it working despite extensive searching. (Trust me, it's been days. ): Anyway, thanks for any help.
EDIT: So I've mostly figured it out - I was depending too heavily on mapping.fromJS and certain things were not being wrapped into observables. I also realized that I didn't need the create(), only the update(), as it is called after create() anyway. If you have a similar problem let me know!
John,
When updating your data using ko.mapping be sure you don't create a new item. Your UI is already bound to the existing items, so you just want to update the values of the existing item properties; not create new ones. For the example you posted, you'll want to adjust your "update" method of your map to insert the new values into the correct ko.observable property, rather than creating a new object in it's place. The ko.mapping "update" method has a few different parameter lists depending on usage, with the third parameter being the target object of the map. You would want to update that object's properties.
obj.target[label].items[0].text(obj.data[label][0].text);
But, that's a bit of a mess. You'll probably want to create a second level of mappings (create / update) to handle "deep" object hierarchies like in your fiddle. For example one map for objects at the "foo/bar" level, and another call to ko.fromJS from within "update" with another map for the child Obj() objects.
After fixing that, you'll run into a couple simple binding errors that you can fix using another "with" binding, or a "foreach" binding for the child arrays.
Overall, you've just run into a couple common pitfalls, but nothing too severe. You can learn a bit more about a few of these pitfalls on my blog here : http://ryanrahlf.com/getting-started-with-knockout-js-3-things-to-know-on-day-one/
I hope this helps!
I have this controller with a value.
App.xcontroller = SC.ArrayController.create({
...some code...
array_values = [],
..more code...
})
Now i have somewhere in a view this valueBinding
valueBinding: 'App.xController.array_values',
When I change values in the array the view does not get updated. but when i do
the following in the controller:
var array_values = this.get('array_values');
... adding / removing values to the array....
if (x_values.contains(x)){
x_values.removeObject(x)
} else {
x_values.pushObject(x);
};
this.set('array_values', array_values.copy());
the binding works, the view gets updated. But ONLY with the copy().
I don't want to make a copy of the array, IMHO this is not efficient. I just want to
let the valueBinding know content has changed..
the x values are just a bunch of integers.
The reason i want this: I want to change the value key of a SegmentedItemView. I want to change the active buttons. But I do not know on forehand how many segmentedviews I have
so I thought i bind the value of every generated segemented view to some common array and change that common array to be able to change the active buttons on all of the segmented views. Since each button represents an item with an unique key it works fine. except that i have to copy the array each time.
set the content property of the xcontroller
Bind to the arrangedObjects property of the xcontroller
You need to use KVO compliant methods on the array to get the bindings to fire. The ArrayController itself has an addObject and removeObject methods. Arrays in SC have been augmented with a pushObject method (among others), which is also KVO compliant. So if you use the KVO methods the view should update.
The reason your view does not update is because you are bound to the array, but the array itself did not change. When you do a copy, the array itself changes, so the bindings fire.
You might also want to try
this.notifyPropertyChange('x_values');
in the controller after you make the changes, but that is less preferable to using the built in KVO functionality.