Using jQuery dynatree with Knockout and Breeze - javascript

OK, following the suggestion from PW Kad I'm splitting this part of the question off from where it started on question ID 17973991.
I have a viewmodel that utilises a datacontext built around breeze and it fetches the data I want and populates observable arrays. I have a requirement to use data already retrieved by Breeze to populate another (observable) array to use in a treeview.
As the existing data does not have the correct fieldnames, I need to be able to create a new array with correct fieldnames that the dynatree/fancytree plugin can use.
My first attempt: (subsequently shown to not work so don't do this!)
So in my viewmodel I added the following at the top of the .js file:
var treeMaterials = ko.observableArray();
var treeMaterial = function (data) {
var self = this;
self.name = ko.observable(data.name);
self.id = ko.observable(data.id);
self.children = ko.observableArray();
$.each(data.children, function (index, item) {
self.children.push(new Person(item));
});
};
I then added an "asTreeMaterials" method to my module:
var asTreeMaterials = function (treeMatsObservable, matsObservable) {
treeMatsObservable([]); //clear out array as we're rebuilding it in here
var tmpArray = treeMatsObservable(); //create local temp array to avoid ko notifications on each push
$.each(matsObservable, function (index, mat) {
tmpArray.push(new treeMaterial({
id: mat.id,
name: mat.materialName,
children: []
}));
});
treeMatsObservable(tmpArray);
};
(borrowing heavily from John Papa's coding there, thanks John!)
Note: there will be more code going into the "children" bit once I have the basics working
And finally changing the "activate" method to use the new method:
var activate = function () {
// go get local data, if we have it
return datacontext.getMaterialPartials(materials),
asTreeMaterials(treeMaterials, materials);
};
....
and then returning the new array from the module:
var vm = {
activate: activate,
materials: materials,
treeMaterials: treeMaterials,
title: 'My test app page 1',
refresh: refresh
};
means that I don't hit the server again for the treeview version of the data.
Edit 2.
Following the guidance from PW Kad on the other question (will be added to this question shortly) I have modified the "asTreeMaterials" method as follows:
var asTreeMaterials = function () {
treeMaterials([]); //clear out array as we're rebuilding it in here
var matArray = materials().slice();
var tmpArray = [];
$.each(matArray, function (index, mat) {
tmpArray.push(new treeMaterial({
id: mat.id,
name: mat.materialName,
children: []
}));
});
treeMaterials(tmpArray);
};
The reason (I think) I have to create a separate new array is that the existing "materials" observable that I slice does not contain the correct properties. Dynatree/fancytree requires (among other things) an "ID" and a "name". I have the ID, but I have "materialName" in the materials observable hence the "$.each" on the array created by the slicing of the materials observable to push the "materialname" property into the "name" property in my new array (tmpArray). I'm new to all this, I may be miles off the mark here!
Do I actually need an observable array...? I don't think I do if I understand what observable arrays are for... my materials are pretty much set in stone and will change very, very rarely. I presume I can simply leave "treeMaterials" as a standard javascribt object array and return that in the viewmodel instead of making it an observableArray?
Either way, currently the values for materialname and ID are not passed into the relevant properties in the tmpArray I'm making. Instead I'm getting the functions from the materials observable so I think I need to approach this with an "unwrap" of some sort to get at the actual values?

You are not populating the treeMaterials because you don't have any data in materials when you are sending it to asTreeMaterials. I am making some assumptions here but basically it looks like this is what you are trying to do -
At the top of your view model, I assume you have two observableArrays
var treeMaterials = ko.observableArray();
var materials = ko.observableArray();
For your activate method, you need to go get some data, and then when your datacontext returns a promise, go make a tree out of it of some object type -
var activate = function () {
return datacontext.getMaterialPartials(materials).then(
makeMyTree);
};
You don't need to pass treeMaterials or materials because they are within the scope of the view model already, and you are just trying to make a tree of objects out of your materials.
var makeMyTree = function () {
treeMaterials([]);
ko.utils.arrayForEach(materials(), function (mat) {
treeMaterials.push(new treeMaterial(mat));
});
};
This is going to make an observableArray of objects with observable properties, meaning if you are passing them or trying to get their value you would need to use something like treeMaterials()[0].name().
In case your dynatree doesn't take observables, or isn't playing well with them
I am not sure how your dynatree or w/e works with observables, so here is a standard array of non-observable objects instead of an observable array -
var treeMaterials = [];
var makeMyTree = function () {
treeMaterials[];
ko.utils.arrayForEach(materials(), function (mat) {
treeMaterials.push(new treeMaterial(mat));
});
};
var treeMaterial = function (data) {
var self = this;
self.name = data.name;
self.id = data.id;
self.children = [];
$.each(data.children, function (index, item) {
self.children.push(new Person(item));
});
};

Related

Knockout JS Mapping fromJS nested models

I am having trouble understanding how to work with Knockout JS Mapping Plugin. I have some nested models (as seen below) and what I am currently doing is just using the ko.mapping.fromJS() in the parent model. What I am noticing is that computed values are not being ...computed.
I tried to understand the "create":
var mapping = {
'children': {
create: function(options) {
return new myChildModel(options.data);
}
}
}
var viewModel = ko.mapping.fromJS(data, mapping);
But right now in my current scenario I am not sure how to implemented.
Current structure:
var ProductModel = function($name, $price, $quantity) {
var self = this;
self.id = ko.observable();
self.name = ko.observable($name);
self.quantity = ko.observable($quantity);
self.price = ko.observable($price);
self.price.total = ko.computed(function() {
return self.price() * self.quantity();
});
};
var CartModel = function (){
var self = this;
// Model Properties
self.id = ko.observable();
self.products = ko.observableArray();
self.fetch = function() {
$.ajax({
url: "route to get the specific cart",
type: "GET",
success: function(data) {
ko.mapping.fromJS(data, {}, self);
}
});
};
// Convert to mapping format
ko.mapping.fromJS(ko.mapping.toJS(self));
};
var ViewModel = ko.validatedObservable(new CartModel());
ko.applyBindings(new ViewModel());
I am not sure how to get the ProductModel to trigger the computed inside the ProductModel, I am not sure if I have to call the ko.mapping.fromJS inside every single Model (I have many more models, I stripped them out to make it simpler).
After the fetch function I alerted the products().length and it actually contains the quantity that was previously saved. but the computed is not showing. How do I implement the create method of knockout mapping plugin for nested observable(and observablearrays) that are models with computed inside of them.
P.S: The reason I have it self.price.total is in order to avoid the toJS send the total key as well. I can't ignore the properties of nested models using ignore:
See this thread for more: Knockout JS Mapping fromJS nested models
How does your ajax response looks like? Does it returns the whole cart, or just the products?
Anyway, if you want to handle your product list as an observableArray of viewmodels you could do something like this:
ko.utils.arrayForEach(products, function(item, index) {
self.products.push(new ProductModel(item.name, item.price, item.qty);
});
If you want to use the mapping plugin, I haven't made a custom implementation of the "create" method, but I would do something like:
var ProductModel = function(product) {
var self = this;
ko.mapping.fromJS(product, {}, self);
self.total = ko.computed(function() {
return self.price() * self.quantity();
});
};
Edit
I have re-read your question, and I think this answer may be what you're looking for, the only downside of that approach, well for me at least, is that you would need to define a mapping config object for each viewmodel with nested viewmodels in your code

JavaScript knockoutjs, set nested object value doesn't update Html?

I'm using knockout 3.3.0. Assume I have a model as the following:
As you can see There are two nested objects, DeviceStatistics and Product.
At first they are OK and work fine and update HTML.
I assign the new values to these objects as the following:
window.KoEntityModel.EntityModel.DeviceStatistics = ko.mapping.fromJS(newJsonModel);
It's OK without any problems, but knockout doesn't update(change) HTML
Whereas, chrome console shows window.KoEntityModel.EntityModel.DeviceStatistics is observable:
How can I fix it?
well to convert object into function you need to do something like below
viewModel:
var fromserver = {
'device': {
'one': 'onevalue'
},
'product': {
'prod': 'prodone'
},
'name': 'supercool'
};
var ViewModel = function() {
var self = this;
self.device = ko.observable();
self.prod = ko.observable();
ko.mapping.fromJS(fromserver, {}, self); //this converts object into function & keeps the other prop's coming from server intact .
};
var vm = new ViewModel();
ko.applyBindings(vm);
working sample fiddle here shows the modified data .
I found the solution,
We have to update a knockoutJs view model with the following code:
ko.mapping.fromJS(newDataInJson, {}, knocoutJsViewModel);

trying to convert/map the Objects in my JSON array to my KO observable function

I have an JSON array that I read in, now I want to make each object in the array a KO observable so that it can be mapped to the function
function Person(data)
{
this.name = ko.observable(data.name);
this.age = ko.observable(data.age);
this.link = ko.observable(data.link);
}
function ViewModel()
{
var self = this;
self.Persons = ko.observableArray([]);
var JSONdataFromServer;
$.getJSON('http://127.0.0.1:8080', function(data) {
self.Persons(data);
for(var k in self.Persons) {
k = $.map(k, function(item) { return new Person(item) });
}
});
}
However When I run this code(this is only a portion of it) I get the error "Cannot use 'in' operator to search for '0' in G". Again all I want to do is convert the JSON object in the Persons array to a Person KO observable object.
self.Persons is an observableArray, so if you want to look at its contents, you need to invoke it:
for (var k in self.Persons())
Other things you're doing are perplexing me. To convert the JSON object to an observableArray of Persons, you'd likely do something like this (depending on what the data looks like -- you don't say):
$.getJSON('http://127.0.0.1:8080', function(data) {
var arrayOfPersons = ko.utils.arrayMap(data, function (item) {
return new Person(item);
});
self.Persons(arrayOfPersons);
});
Avoid manually mapping incoming data from the server. Use ko mapping plugin for this. Maintaining your client side viewModel in sync with data from the server is very hard as soon as your viewModel starts growing, better to leverage this to ko mapping, which is also very flexible if you need more control over how the mapping should work.

Update KnockoutJS viewModel using mapping and localStorage to maintain persistence

I need the data saved to localStorage, "savedData", to update the currently empty view model when the page is revisted/refreshed, possibly using the KO mapping plug-in in the line:
ko.mapping.fromJS(this, retrievedData);
But of course it's not working!
function viewModel() {
var self = this;
self.employees = ko.observableArray([]);
self.removeEmployee = function (employee) {
self.employees.remove(employee);
};
self.addEmployee = function () {
self.employees.push(new Employee());
};
self.save = function () {
var savedData = ko.toJSON(this);
localStorage.setItem('savedData', savedData);
//console.log('savedData', JSON.parse(savedData))
}
if (localStorage && localStorage.getItem('savedData')) {
var retrievedData = JSON.parse(localStorage.getItem('savedData'));
ko.mapping.fromJS(this, retrievedData);
}
}
var vm = new viewModel();
ko.applyBindings(vm);
Problem is in fromJS parameters. Second parameter is mapping options but you are trying to pass data instead. Proper usage should be following
var viewModel = ko.mapping.fromJS(retrievedData, mappingOptions);
If you don't want map retreivedData to viewModel using mappingOptions
ko.mapping.fromJS(retrievedData, {}, viewModel);
This will convert your data to viewModel without any specific mapping options.
NOTE: If you call mapping.fromJS with two arguments and second argument IS mapping object (__ko_mapping__ property defined) it will treat second argument as a viewModel and you can use function in a different manner:
ko.mapping.fromJS(retrievedData, viewModel);
Basically this scenario is valid when you are not creating viewModel, but instead updating previously created using mapping viewModel with some data.
ANSWER: You should update your code co map properly by passing empty options parameter. An of course data should go first:
if (localStorage && localStorage.getItem('savedData')) {
var retrievedData = JSON.parse(localStorage.getItem('savedData'));
ko.mapping.fromJS(retrievedData, {}, self);
}

Knockout JS initializing observable array from server data using a javascript type

I'm looking for the best way to initialize a knockout observable array from some server data (ViewBag), and I want the array contents to be of a javascript type I have defined. Without the requirement of the JS type I could just use:
materialVarieties: ko.observableArray(#Html.Raw(Json.Encode(ViewBag.Materials)))
but I also have a material JS type that I want to use so I can have some extra ViewModel specific properties and functions i.e.:
var material = function(id, name) {
this.id = id;
this.name = name;
this.selected = ko.observable(false);
this.select = function()
{
jQuery.each(processViewModel.materials(), function(index, item)
{
item.selected(false);
});
this.selected(true);
}
}
And then the required initialization becomes:
materialVarieties: ko.observableArray([new material(1, "Apricot"), .....
Currently I build up a string from the ViewBag data and then render that as the initializer like this:
#{ var items = string.Join(",",
((IEnumerable<MaterialVariety>) ViewBag.Materials)
.Select(m => string.Format("new material({0}, {1})",
Json.Encode(m.Id), Json.Encode(m.Name)))); }
var processViewModel = {
material: ko.observableArray([#Html.Raw(items)])
But I'm wondering if there is a cleaner way than the string.Join bit. I could wrap it up in a Helper. What do you do?
I would typically serialize the array first, then map it when putting it in the view model. Would be like:
var originalVarieties = #Html.Raw(Json.Encode(ViewBag.Materials))
var processViewModel = {
materialVarieties: ko.observableArray(ko.utils.arrayMap(originalVarieties, function(variety) {
return new material(variety.id, variety.name);
}))
}
Requires a minor amount of additional processing on the client-side, but seems cleaner than building strings.

Categories