Knockout Observable Array of Observable Array - javascript

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)

Related

How to assign a KnockoutJs observable to another observable?

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]);

Async Angular/Node $scope calls

really new to Angular here and I have a problem that I need some help with. Basically, I need to have a selection of items that user can click on. When an item is clicked, the page needs to show some of the properties that the item has like it's description, etc. The first part is not a problem, but I'm having trouble with the second part, which is displaying the data. So here is what I have:
On the front end, I have an angular ng-click chooseItem(item) function that takes the clicked item as its paramater:
<div ng-repeat="item in items" class="col-xs-2 col-md-1">
<div ng-click="chooseItem(item)" class="thumbnail">
<img src="/images/items/{{item.name}}.png"/>
</div>
</div>
This is then passed on to the items factory through items.getChosenItemData(item) function. Since the real item data is stored in Mongo and not the factory, this function queries the db to retrieve the item data. This retrieved data is stored into the chosenItem object, which is then passed back to the controller as $scope.chosenItem.
app.factory('items', ['$http', function($http){
var objects = {
items: [
// ... more items before these
{name: "Pencil"},
{name: "Pen"}
/* I use these item objects as keys to the items themselves.
The ng-repeat iterates through all of the names for each item
which allows me to display static images for each item to the page.
There aren't many items, about 100, but they have tons of json information
so to hardcode it all into here is not an option
A way to do this without any hardcoding would be nice! */
// more items after these
],
// this is used to store a currently clicked item's values
chosenItem: null
}
objects.getChosenItemData = function(name){
return $http.get('/items/' + name).success(function(data){
// console.log(data);
angular.copy(data, objects.chosenItem);
console.log("Chosen Item: ", objects.chosenItem);
})
}
return objects
}]);
app.controller('MainCtrl', [
'$scope',
'items',
function($scope, items){
$scope.items = items.items;
$scope.chosenItem = null;
$scope.chooseItem = function(item){
items.getChosenItemData(item.name);
$scope.chosenItem = items.chosenItem; //chosen item object attribute in factory
console.log("$scope item: ", $scope.chosenItem);
}
}
});
This almost all works. I can query the data of the clicked item successfully, but returning it is another story. Upon first click, the value of $scope.chosenItem is null. Then upon second click, it stores the value of the click item. This also causes the problem where if I click on n amount of items, the value stored is always the value of the n-1 item, not the current item. I need it to store the value of the clicked item on the first click, not the second.
I have a feeling I need to add a callback somewhere in here to make it work, but I'm new to Angular/JS so I'm not sure where it should even go.
Thanks for any help! Also any tips or leads on Angular design patterns would be much appreciated, since I have the feeling that this is a terrible implementation of something that seems rather simple.
I suggest you to expose the service directly:
$scope.serviceItem = items;
and then you can call it in the view like that:
{{serviceItem.chosenItem}}
It will be always updated to the latest clicked value.
I hope it helps.

Knockout component updating observable not being subscribed to by parent view model?

I've written a component called Upload which allows users to upload files and then report back with a JSON object with these files. In this particular instance, the Upload component has a parameter which comes from a parent view model:
<upload params="dropzoneId: 'uploadFilesDropzone', postLocation: '/create/upload', uploadedFiles: uploadedFiles"></upload>
The one of importance is called uploadedFiles. The parameter binding here means I can reference params.uploadedFiles on my component and .push() new objects onto it as they get uploaded. The data being passed, also called uploadedFiles, is an observableArray on my parent view model:
var UploadViewModel = function () {
// Files ready to be submitted to the queue.
self.uploadedFiles = ko.observableArray([]);
};
I can indeed confirm that on my component, params.uploadedFiles is an observableArray, as it has a push method. After altering this value on the component, I can console.log() it to see that it has actually changed:
params.uploadedFiles.push(object);
console.log(params.uploadedFiles().length); // was 0, now returns 1
The problem is that this change does not seem to be reflected on my parent viewmodel. self.uploadedFiles() does not change and still reports a length of 0.
No matter if I add a self.uploadedFiles.subscribe(function(newValue) {}); subscription in my parent viewmodel.
No matter if I also add a params.uploadedFiles.valueHasMutated() method onto my component after the change.
How can I get the changes from my array on my component to be reflected in the array on my parent view model?
Why do you create a new observable array when the source already is one? You can't expect a new object to have the same reference as another one: simply pass it to your component viewModel as this.uploads = params.uploads. In the below trimmed-down version of your example, you'll see upon clicking the Add button that both arrays (well the same array referenced in different contexts) stay in sync.
ko.components.register('upload', {
viewModel: function(params) {
this.uploads = params.uploads;
this.addUpload = function() { this.uploads.push('item'); }.bind(this);
},
template: [
'<div><button type="button" data-bind="click: addUpload">Add upload</button>',
'<span data-bind="text: uploads().length + \' - \' + $root.uploads().length"></span></div>'].join('')
});
var app = {
uploads: ko.observableArray([])
};
ko.applyBindings(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="component: {name: 'upload', params: {uploads: uploads}}"></div>
It is only in case your source array is not observable that things get a little more complicated and you need to have a manual subscription to update the source, eg. you would insert the following in the viewModel:
this.uploads.subscribe(function(newValue) { params.uploads = newValue; });
Additionally the output in the text binding would not be updated for the source because it is not observable. If for some reason that I cannot conceive of you would want to have 2 different observableArrays (1 source & 1 component), you should still be able to do with the line above, but replace the function code with params.uploads(newValue)
The problem may be related to this bug (to be confirmed): https://github.com/knockout/knockout/issues/1863
Edit 1: So this was not a bug. You have to unwrap the raw param to access the original observable. In your case, it would be:
params.$raw.uploadedFiles() //this would give you access to the original observableArray and from there, you can "push", "remove", etc.
The problem is that when you pass a param to a component, it gets wrapped in a computed observable and when you unwrap it, you don't have the original observableArray.
Reference: http://knockoutjs.com/documentation/component-custom-elements.html#advanced-accessing-raw-parameters
While Binding Property that involves Parent --> Child Relation
Use Binding in this way
If You want to bind data to Child Property
data-bind='BindingName : ParentViewmodel.ChildViewModel.ObservableProperty'
Here it seems you want to subscibe to a function when any data is pushed in Array for that you can write subscribe on Length of Observable array which can help you capture event that you want.
This should solve your problem.

Knockout - added observable not updating on new objects

For UI purposes, when I load the array my viewModel is based on I add a new property to each object based on some other properties:
item.forEach(function (party) {
if (party.AcknowledgementDate() === null) {
party.Agreed = ko.observable(false);
}
else {
party.Agreed = ko.observable(true);
}
vm.Parties.push(party);
});
"Parties" is defined as ko.observableArray when the page starts.
The items in this array are edited in a separate UI window. When those changes are saved and the window closed, I call this function to update those values:
function updateAgreed() {
vm.Parties().forEach(function (i) {
if (i.AcknowledgementDate() === null) {
i.Agreed(false);
}
else {
i.Agreed(true);
}
});
}
This all works fine, and makes me very happy. The problem arrives when users create a new party item. We're using Breeze too, so we go off to the data service which requests entity framework create a new object of the appropriate type, then add an observable:
var lp = manager.createEntity('Party_dto'. { [an array of initial values] });
lp.Agreed = ko.observable('');
return lp;
Thanks to Breeze, this adds itself to the Parties observableArray because it's related to the same parent object. I can then call updateAgreed again to populate the Agreed observable with the appropriate value.
Logically, this work as expected - you can step through it and watch the Agreed observable of the new item be added and populated with the expected values. The problem comes in the UI - it doesn't update as having changed. Yet running the same code against an already-loaded object does cause the UI to update.
I'm stumped by this. I can't replicate it in Fiddle because we create objects in Breeze and not on the fly - and making a mock version without Breeze works perfectly. Why do my observables update on already loaded objects, but the same observable not update on a new object?
There are a few things that I see that need to be addressed. One, since you are using Breeze, take advantage of the model constructors and initializers. Wherever you are defining properties for your models, add the following code -
metadataStore.registerEntityTypeCtor(
'Party', null, partyinitializer);
function partyinitializer(party) {
party.Agreed = ko.observable(false);
}
Now all of your party entities have an agreed property that you can access. Next, make sure you aren't setting the Party's parent navigation property in the createEntity method, as that will break your binding.
var lp = manager.createEntity('Party'. { [an array of initial values] });
lp.parentParty(something); // Set the parent here
return lp;
This will make sure that before the party is bound back to the parent and shown in the view, all of the properties will be set. Then when you set the navigation property, it will show up in your view all happy-like.

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