How to assign a KnockoutJs observable to another observable? - javascript

I have a model defined as:
var TestModel = function () {
var self = this;
self.Item = ko.mapping.fromJS(new Item());
self.ItemList = ko.observableArray();
};
var Model = new TestModel();
ko.applyBindings(Model);
After an AJAX call I fill the array with:
ko.mapping.fromJS(data, {}, Model.ItemList);
The array is displayed in the screen and when the user selects an item, a modal dialog is opened to edit that item. The same modal dialog can be opened to add a new item. That modal has a data-bind="with: Item", so I can do something like this:
function add() {
ko.mapping.fromJS(new Item(), {}, Model.Commission); //Empty object
$('#dialog').dialog('open');
};
function saveAdd() {
//Clone the properties...
Model.ItemList.push(ko.mapping.fromJS(ko.mapping.toJS(Model.Item)));
}
function edit(i) {
//Clone properties!
ko.mapping.fromJS(ko.mapping.fromJS(ko.mapping.toJS(Model.ItemList()[i])), {}, Model.Item);
$('#dialog').dialog('open');
}
function saveEdit(i) {
//Clone properties!!!!
Model.ItemList()[i] = ko.mapping.fromJS(ko.mapping.toJS(Model.Item));
}
I want to avoid cloning the properties every time I need to assign the values of one observable to another one. I understand that if I manage to tell Knockout that the Model.Item is some Model.ItemList()[i], the objects references should make sure the changes in the UI reflect directly to my observableArray item, but I cannot just assing it as Model.Item = Model.ItemList()[i]; because that breaks the binding between the original Model.Item object and the view [*1].
Is there a way to assign an observable to another observable that's binded to the view, and maintain the references?
Failing to do that, is the a better way to do this? Basicaly a page where you store the list of items in an observableArray and then add/edit in another piece of HTML.
Edit: Explanation for [*1]
I cannot find the stackoverflow question where this was explained, but it was something like this:
When we do ko.applyBindings(Model);, the observable objects inside Model get binded to the view. At this point the object referenced by Model.Item is binded, so if I do Model.Item = Model.ItemList()[i]; the new object being referenced by Model.Item is not binded to anything. The original object (in memory) is still the one binded to the view, just that there are no references to it now.

If you make Model.Item an observable, with the mapped object inside of it, then you can set the observable's contents to the new item. The reference to the observable made by knockout remains intact because you're just updating the contents and not the property itself.
var TestModel = function () {
var self = this;
self.Item = ko.observable(ko.mapping.fromJS(new Item()));
self.ItemList = ko.observableArray();
};
...
Model.Item(Model.ItemList()[i]);

Related

Knockout Observable Array of Observable Array

I have passed a model to my View Model from .Net, which is a Object, containing a few fields, and a List, and each of those has another List.
So, an object with a List of Lists.
On my View, I represent this as some data, with a list of Tabs (The first list), and then each tab has a grid of data.
In my Knockout View model, I just have:
self.Name = ko.observable("");
self.Width = ko.observable(0);
self.Height = ko.observable(0);
self.TemplateGroups = ko.observableArray();
So, my main object, with an observable array (the first list). But, the list within that, isn't observable.
So when rendering my tabls, I do:
<div class="tab-content" data-bind="foreach: TemplateGroups">
And that renders each tab. And then within each tab, I do this:
<tbody data-bind="foreach: $data.Components">
<tr style="cursor: pointer" data-bind="click: $parents[1].ClickedIt">
('Components' is the name of the list within the outer list)
Problem is, on my ClickIt function, I am displaying the Idof the row I clicked, and it works. And then I am just trying to change the 'Description' field of this outer list, but ... it's not a function, so, no observed:
self.ClickedIt = function () {
alert(this.Id);
this.Description("Craig");
}
The alert shows the Id, but error on Description("Craig"), as it's not a function.
How can I make the list, within my ObservableArray, observable?
(To be clearer, my model I pass in is an object, which contains a List called TemplateGroups... and then each item in that list, contains a List called 'Components'. It's the Id of one of those Components that I am displaying, but I need to make that list Observable.
Edit: This seems to be what I am looking for (http://jsfiddle.net/rniemeyer/bZhCk/), but ... I am not defining my array the same way. Instead, they're being passed in from a .Net class (So, converted to JSON, I think). And that .Net class contains a List of another .Net class, which has a List).
Note, My load method:
self.loadData = function () {
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Name(data.Description);
self.Width(data.Width);
self.Height(data.Height);
self.TemplateGroups(data.PlateTemplateGroups);
});
}
The content of self.TemplateGroups is data.PlateTemplateGroups, that is an array and its content are not observable properties (they are javascript objects).
If you want that this objects in this array become observables, you could use the Mapping Plugin:
self.loadData = function () {
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Name(data.Description);
self.Width(data.Width);
self.Height(data.Height);
self.TemplateGroups(
ko.mapping.fromJS( // <--- new
data.PlateTemplateGrou‌​ps));
});
}
This way, all properties become observables.
If you need to convert this data to a Json format you could use ko.mapping.toJS():
ko.mapping.toJS(self.TemplateGroups)

Updating variable through model Backbone

I have a base model, that is creating a view with several div's. It is not actually a form; but it is acting as a form. I have variables being set with defaults as well. Here's my model right now:
var BaseModel = require('base-m');
var SomeModel = BaseModel.extend({
defaults: function() {
return {
FirstName : null,
LastName : null,
Age : null,
State : null
};
}
update: function() {
return {
FirstName : $('[name="FirstName]').val()
};
console.log(FirstName);
}
});
I am trying to update the model with the particular value of whatever is entered. Do I need to use an event? I am doing this because I want to retrieve the updated variable for output purposes.
Also; (if it's different), lets say it's a drop down menu like states..? Would I update it similar to a text field like first name?
Thanks
It appears your model is accessing the DOM. Usually, your view would deal with the DOM, extracting information then updating the model.
So for example, create a view with a constructor that:
Creates your input elements and put them in an attribute called $el; then
Adds $el to the DOM; then
Binds event listeners to $el.
These event listeners can update model attributes via a reference to the model, e.g. this.model in the view's context.
The view can also watch the model for changes and update itself accordingly.
For example:
var SomeView = Backbone.View.extend({
// Store HTML of DOM node in template. Easy to change in future.
template: [
'<div class="blah">',
'<input type="text" class="hello" />',
'</div>'
].join(''),
initialize: function() {
// Create DOM node, add to DOM
this.$el = $(_.template(this.template)());
$("body").append(this.$el);
this.hello = this.$el.find('.hello');
// Update model when view changes
this.hello.on('keydown', this.updateModel);
// Update view when model changes
this.model.on('change', this.updateView);
},
updateModel: function(evt) {
this.model.set('hello', this.hello.val());
},
updateView: function() {
this.hello.val(this.model.get('hello'));
}
});
The code that creates your model could also create this view and pass the model reference to the view constructor, e.g.
var myModel = new SomeModel();
var myView = new SomeView({model: myModel});
Of course, all of the specifics will vary according to your situation.
If you would like to use an existing DOM node as $el, remove the first two lines of code that create and append $el. Then instantiate your view like:
var existingJqNode = $('#existing'); // find existing DOM node, wrap in jQuery object
var myView = new SomeView({
model: myModel,
$el: existingJqNode
});
Above all, think about how best to set this up. Does using an already existing DOM element as $el create an advantage? If you want to create more of these views in the future, what code is responsible for creating/adding the $els before each view is instantiated?
Hope that helps.

how to fire function when observable array is modified

is it possible to fire a function when observable array is modified? my goal here is to notify me if my observable array is modified to do some logics on my current application
this is my view model it has a observable array inside it
WT.BM.BarsViewModel = function () {
var self = this;
self.BarsDataHolder = ko.observableArray([]);
};
i just want to fire a function to notify me if self.BarsDataHolder has been modified
any ideas?
You can subscribe to any ko.observable or ko.observable array.
WT.BM.BarsViewModel.BarsDataHolder.subscribe(function(newArray) {
console.dir(newArray);
});
you can find this in the knockout documentation on the observables page

DurandalJS and composing Child Views with Parameters

I'm having some difficulty transitioning from Knockout's template binding to the Durandal compose binding.
In my old project, I have a list of tabs which can be swapped into center stage by placing them into the "selectedTabSection" observable. The templateId was a property of the sub-view. So in my parent view, I created instances of my child models like this:
self.tabSections([
new BasicTabViewModel(self, db),
new BiometricTabViewModel(self, db),
new ActivityTabViewModel(self, db),
new SurveyTabViewModel(self, db),
new CommunicationTabViewModel(self, db),
new ReferralTabViewModel(self, db),
new GoalTabViewModel(self, db),
new NcpTabViewModel(self, db),
new CriticalValuesViewModel(self, db),
new ConditionManagement(self, db)
]);
Then when I wanted to show one, I'd place it into the active observable:
self.selectedTabSection(self.tabSections()[0]);
When I change to the compose binding, it seems that Durandal cannot find the associated Views for my ViewModels because I'm binding instances of the models rather than the constructor of the ViewModel itself. In other words,
self.selectedTabSection(BasicTabViewModel);
Finds the appropriate view, whereas
self.selectedTabSection(new BasicTabViewModel(self, db));
does not.
How can I get the viewLocator to understand that I'm passing an instance rather than the ViewModel constructor itself? If I cannot, how do I pass parameters to my child views since they haven't been initialized until they are composed?
EDIT/UPDATE:
It looks like there's something to do with how I've composed my child ViewModels. Durandal appears to have issue when you return an object from the ViewModel's constructor.
This seems to work as expected:
var viewModel = function (parentVm, db) {
var self = this;
}
Whereas this:
var viewModel = function(parentVm, db){
var self = this;
//public api
return {};
}
Does not. Something about returning an object from the constructor make DurandalJS get lost when trying to locate the View and also makes a mess of various scopes. I'm considering re-writing my scripts to fit, but this pattern of returning a object from a constructor has served me well for many moons (prior to Durandal) Curious...
The reason this works -
var viewModel = function (parentVm, db) {
var self = this;
}
Whereas this does not -
var viewModel = function(parentVm, db){
var self = this;
//public api
return {};
}
Is because you are erasing everything that your function did to create an object.
Consider this -
function newModule (path, params){
var self = this;
self.modulePath = path;
self.activationData = params;
}
Now you can create instances of this anonymous function and pass in the parameters to you want to bind your view / view model to.
var theseChildViewModels = ko.observableArray();
var someData = getDataFromSomewhere();
theseViewModels.push(new newModule('viewmodels/myViewModelOne', { data: someData }));
theseViewModels.push(new newModule('viewmodels/myViewModelTwo', { data: someData }));
Now you can bind to these in your parent view like this -
<ul data-bind="foreach: theseChildViewModels">
<li>
<!-- ko compose: { model: modulePath, activationData: activationData} -->
<!-- /ko -->
</li>
</ul>
And dynamically declare your paths and what data is passed into the activate callback during composition.

Cannot update Knockout UI with fresh data object

I'm currently having problems having the UI refresh when I'm getting new data from the server for a single item which is in an observableArray of wrapper objects which holds an object of several observables.
Consider the following:
var vm = {
....
localEdited: ko.mapping.fromJS(new ItemWrapper(defaultModelSerialised)),
selected: ko.observable(null),
editItem: function(data) {
// clone a temporary copy of data for the dialog when opening (*.localEdited on dialog)
var clonedData = ko.toJS(data);
ko.mapping.fromJS(clonedData, null, this.localEdited);
// selected should now point to the item in the obserable array which will be refreshed
this.selected(data);
// open dialog...
},
submitDialog: function(data) {
// submit data to server...
// (1) commit the data back to UI (new item is return in resp.entity from server)
vm.selected(new ItemWrapper(resp.entity));
// at this point the UI isn't showing the updated value
// (2) however if I do this it reflects the data change in the UI
this.selected().Name("changed"); // updates the UI.
}
Can someone explain why passing in the ItemWrapper into vm.selected isn't updating the UI whereas in (2) it works. I don't want to have to set-up each property like in (2) for every property.
ItemWrapper looks like so:
function PoolWrapper(pool) {
this.Name = ko.observable(pool.Name);
// more properties...
}
OK- the issue is that your clones end up with mapping meta-data on them and eventually this causes recursion when trying calling ko.mapping.fromJS.
The solution is to create your clones using ko.mapping.toJS instead of ko.toJS, so that you get a clean clone (without mapping meta-data).
Here is an updated fiddle: http://jsfiddle.net/rniemeyer/tDDBp/
Something I also stumbled upon today that I thought I'd share:
If you clone using:
var clone = ko.mapping.fromJS(ko.mapping.toJS(itemToClone));
Then the clone will be stripped of any computed observables. They will exist as the last value of the function, but no longer function as a computed observable.
If your item is a complex model with computed observables that you would like to keep on your clone you can do the following:
var clone = ko.mapping.fromJS(ko.mapping.toJS(itemToClone), null, new itemModel());
Where itemModel is your complex model for your item containing your computed observables.

Categories

Resources