I'm trying to change the view model which is bound to some part of a DOM template (instead of changing the values of the view model) but I just can't figure out how or if it's even possible
Here's the scenario:
Create a new View Model object
Bind it (e.g. applyBindings(myViewModel)
Create another View Model object
Bind the new object to the same part of the DOM so all elements are now bound to the new object.
I want to do the equivalent of changing the value of DataContext of a control in WPF (on which KO's MVVM pattern is based)
The reason for this is that I'm trying to use the same view model for both the representation of the object in a list and the representation of the object in its own view, so I already have a view model for all objects being shown in the list.
There are other workarounds but I think this would be the most elegant way to do it.
There are two way of working with multiple viewmodel. The first way is to do multiple binding like #nathan gonzalez said. You should do binding up your viewmodels. However this complicates things a bit. Therefore difficult to manage.
The second way is to use master viewmodel. I would recommend this.
http://jsfiddle.net/sinanakyazici/e29EZ/10/
<div data-bind="with: mainvm">
<span data-bind="text: prop, click : action"></span>
</div>
var vm = function(value)
{
this.prop = ko.observable(value);
var self = this;
this.action = function() {
console.log("clicked: " + self.prop());
}
}
var master = {
mainvm : ko.observable(null)
}
master.mainvm(new vm('viewmodel 1'));
master.mainvm(new vm('viewmodel 2'));
ko.applyBindings(master);
so ko.applyBindings() should cover this for you. you can pass in a 2nd parameter that tells which top level element to apply the bindings to, like so:
ko.applyBindings(myExistingViewModel, $('#someElementId')[0]);
you may want to clean the elements first though, like this:
ko.cleanNode($('#someElementId')[0]);
this completely removes the bindings and clears the in memory data for that element and its children bindings.
Related
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
how can i implement multiple custom knockout bindings that can be declared in an object literal?
basically instead of doing this:
<input data-bind="customBinding1:observable1, customBinding2: observable2 }" />
I would like to be able to do this:
<input data-bind="customBinding0: { customBinding1: observable1, customBinding2: observable2 }" />
thanks in advance.
With Knockout, bindings are specified in name/value pairs, where name is the name of the binding, and value is the value that will be retrieved within the binding using valueAccessor().
It sounds like you may be wanting to pass multiple values in to a binding, like your kendo example shows. Although each binding can only have a single value, that value can be anything. This means you can pass in an object literal as the value, and the object can have as many properties as you want. These properties can also be of any type that you want - you'll just need to handle them correctly inside your binding.
Here's a simple example:
View
<div data-bind="myBinding: {setting1: viewModelProperty1,
setting2, viewModelProperty2}"></div>
Binding
ko.bindingHandlers.myBinding = {
init: function(element, valueAccessor) {
var options = valueAccessor() || {};
//this gives you the value of setting1
var setting1 = ko.utils.unwrapObservable(options.setting1);
//if you need to do something just when `setting1` changes,
// add a subscription to the value like so:
if( ko.isObservable(options.setting1) ){
options.setting1.subscribe(function(newValue){
//do something because the setting1 value changed
});
}
}
};
You also have the option of specifying multiple bindings, each with its own value, and referencing the additional bindings and values from within the first one. Knockout's options and optionsText bindings are an example of this approach.
Excellent Resource: http://www.knockmeout.net/2011/07/another-look-at-custom-bindings-for.html
Take some time to study and experiment with the examples at the link above. There's a lot of good information here that covers what you need to do.
Say I have a backend call which returns a list of objects (and properties), and I use ng-option to stick one of the properties (the name) into the dropdown list, which is using ng-model to attach itself to my model object.
My problem comes in when I need to access other properties of the selected object. ng-option lets me bind objects to that dropdown, which is great. However, if I pull the name out to bind that to my model:
<select ng-model="myModel.name" ng-options="fieldlist.fields.name as fieldlist.fields.name for fieldlist in metrics">
I lose reference to the rest of the object's properties. I need to use another property of the selected object, say fieldlist.fields.location, to perform some other action in an ng-change function. So
Is this possible? Is my Angular naiveté showing too much?
I believe you can do:
ng-model="myModel" ng-options="fieldlist.fields as fieldlist.fields.name for fieldlist in metrics"
Assuming you want myModel to contain all the fields in the selected fieldlist
Per your comment - and this may not be best practice, just my initial thought on how I would do it, you could do something like:
ng-model="selectedItem" ng-change="setSelectedItem()" ng-options="fieldlist.fields.name as fieldlist.fields.name for fieldlist in metrics"
and then in your controller
$scope.setSelectedItem() = function() { $scope.myModel.name = selectedItem.name; };
or remove the ng-change and do
$scope.watch(selectedItem, function() { $scope.myModel.name = $scope.selectedItem.name }
And then you can get whatever property you need from $scope.selectedItem
A little late, but good information i found if anyone could use it. Its still a little odd, but it makes it a little cleaner.
Angular released a ngModelOptions directive which, among other useful things, allows you to define the ngModel as a getter/setter function. Using this, you can remove that pesky ng-change or watch (and possibly a $Digest?). Simply place your update function as the ng-model and set your items that way.
<select ng-model="updatefielset" ng-model-options="{getterSetter:true}" ng-options="fieldlist.fields as fieldlist.fields.name for fieldlist in metrics">
$scope.updatefielset(item) = function()
{
if(angular.isDefined(item)
{
$scope.myModel.A = item.A;
$scope.myModel.B = item.B;
$scope.Val = item;
}
return $scope.Val;
};
https://docs.angularjs.org/api/ng/directive/ngModelOptions
Currently i have a setup that looks like this:
ko.applyBindings(viewModel);
$(".removeButton").live("click", function() {
viewModel.ProductCategories.destroy(ko.dataFor(this));
});
$(".renameButton").live("click", function() {
ko.dataFor(this).Name("Renamed Successfully!");
});
This is working fine for me until i introduce the concept of child elements. At that point the remove event no longer works for those items.
Is there a more generic way of "destroying" an element?
KO obviously knows the element i am clicking (as i am able to rename).
[{"Id":1,"Name":"Bikes","Parent":null,"Children":[{"Id":5,"Name":"Mountain Bikes","Parent":1,"Children":null},{"Id":6,"Name":"Road Bikes","Parent":1,"Children":null},{"Id":7,"Name":"Touring Bikes","Parent":1,"Children":null}]},{"Id":2,"Name":"Components","Parent":null,"Children":[{"Id":8,"Name":"Handlebars","Parent":2,"Children":null},{"Id":9,"Name":"Bottom Brackets","Parent":2,"Children":null},{"Id":10,"Name":"Brakes","Parent":2,"Children":null},{"Id":11,"Name":"Chains","Parent":2,"Children":null]}]
The events above will all work on any element (child or otherwise) except for remove which only works on root elements.
Can i call remove on an element itself or will I have to add some way of working out where it lives inside the array and destroying it like that?
for example; this is preferable:
$(".removeButton").live("click", function() {
ko.dataFor(this).destroy();
});
to this:
$(".removeButton").live("click", function() {
viewModel.ProductCategories[someindex].Children.destroy(ko.dataFor(this));
});
Thanks,
Kohan
The main issue is determining who the parent array is when trying to destroy an item.
Several options:
Rather than ko.dataFor, you can use ko.contextFor which will return an object that includes properties like $data, $parent, $parents and $root.
If your arrays have the same name, then you could do something like: http://jsfiddle.net/rniemeyer/xJjK8/
If your arrays have different names, then you could add a hint on the button element to understand the name of the parent like: http://jsfiddle.net/rniemeyer/arpNx/
Otherwise, if you really wanted it to be generic, then you could use the with binding to force a scope block, which would allow you to access the parent array through $parent. However, this will be the unwrapped array and we really would want the observableArray. With some extra work, you could loop through the properties of the parent's parent and compare the underlying array with your unwrapped array to locate the actual observableArray that you would want to call destroy with your item. Like this: http://jsfiddle.net/rniemeyer/bBVrE/
Finally, if you take care in the way that your objects are created you can push the destroy functionality to the item itself rather than needing access directly to the parent. Here is a sample that shows adding a destroyMe method to an object that uses the parent that was passed to the constructor function: http://jsfiddle.net/rniemeyer/Eeryh/
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.