Knockoutjs populating data from server with MVC - javascript

I am trying to populate knockoutjs viewmodel with some initial values from the server, I am using ASP.Net MVC so the way I am doing it is passing a mvc viewmodel to the view:
public ActionResult Edit(int cvId)
{
CV cv = repository.FindCV(cvId);
//auto mapper mapping
Mapper.CreateMap<CV, MyCVViewModel>();
Mapper.CreateMap<Company, MyCompanyViewModel>();
Mapper.CreateMap<Education, MyEducationViewModel>();
Mapper.CreateMap<Reference, MyReferenceViewModel>();
var model = Mapper.Map<CV, MyCVViewModel>(cv);
return View(model);
}
Inside the view I convert the viewmodel into json string and bind it to knockoutjs viewmodel, so it gets populated with data:
//mvc viewmodel
#model Taw.WebUI.Models.MyCVViewModel
//convert
#{
var json = #Html.Raw(Model.ToJson());
}
//lastly bind
<script type="text/javascript">
// Activate knockout binding
var viewModel = new CVViewModel(#json);
ko.applyBindings(viewModel);
</script>
and inside my knockout javascript, i populate knockout viewmodel with the data:
var CVViewModel = function (data) {
var self = this;
//list view model
self.title = ko.observable(data.title);
self.statement = ko.observable(data.statement);
self.reference = ko.observable(data.reference);
self.companies = ko.observableArray(data.companies);
self.educations = ko.observableArray(data.educations);
self.references = ko.observableArray(data.references);
}
Everything gets populated at this stage:
and the resulting json string is:
Questions:
1. The problem is that some values don't bind when I change them, only title and statement changes:
Resulting json, as you can see, only title and statement changes, and values inside company don't change
2. When saving this data again, how can I let server side know what has been edited and what has been deleted, how to keep track of them using MVC and entity framework, and change database accordingly
Update
My knockout javascript, I already have these observables defined, how do define them in the observablearray
function Company() {
this.companyName = ko.observable();
this.jobTitle = ko.observable();
this.description = ko.observable();
this.startDate = ko.observable();
this.endDate = ko.observable();
}

For your first question:
The problem is you need to use ko.observable for each array item.
For example: jsfiddle
function CVViewModel(data) {
var self = this;
//list view model
self.title = ko.observable(data.title);
self.companies = ko.observableArray(data.companies.map(Company));
}
function Company(data) {
if (!(this instanceof Company)){
return new Company(data);
}
this.companyName = ko.observable(data.companyName || '');
this.jobTitle = ko.observable(data.jobTitle || '');
this.description = ko.observable(data.description || '');
this.startDate = ko.observable(new Date(data.startDate) || '');
this.endDate = ko.observable(new Date(data.endDate) || '');
}
Now when you bind the company observables to the UI each array element on the viewmodel will stay synced.
Regarding your second question I recommend using an ORM like breeze.js which does the change tracking for you. Breeze.js has a tutorial that uses knockout.js.

The problem is that you are trying to update Items in the ObservableArray. where all ObservableArray does is to maintain the array model for you meaning if you add or remove something in your companies observable it will be reflected in the array.
In order to make the changes in the array items, you need to make each item in your ObservableArray as Observable.
take a look at this post here:
https://www.airpair.com/knockout/posts/top-10-mistakes-knockoutjs#8-observable-arrays-don-t-automatically-have-observable-members

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

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

Using jQuery dynatree with Knockout and Breeze

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

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