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
Related
I want to push data from ajax to knockout observableArray , but it give me an error:
The argument passed when initializing an observable array must be an
array, or null, or undefined.
efine(['uiComponent', 'ko', 'jquery'], function (Component, ko, jquery) {
return Component.extend({
initialize: function () {
this._super();
/* State and cities */
this.selectCity();
},
selectCity: function () {
var myViewModel = {};
state = ko.observableArray([]);
jquery.ajax({
url: 'http://127.0.0.1/magento/hamechio/region.php',
type: "GET",
dataType: "json",
success: function(data) {
myViewModel = data;
state.push(data);
}
});
console.log(state);
}
});
});
this line should be change as per my knowledge.
state = ko.observableArray([]);
to this
var state = ko.observableArray();
This is a ajax scope ques.
you can use 'var'.
like this:
var state = ko.observableArray([]);
Can I suggest looking through the Documentation that has lots of working examples: https://knockoutjs.com/documentation/observableArrays.html
Firstly, your View Model is the object on which all of your programme is built on. This object contains all of the data to show (as knockout observable property "methods") and commands to receive (functions). Therefore you need to define the view model to contain everything your application needs to do:
var viewModel = {
//Bindings
state = ko.observableArray();
}
Now you can write to the viewModel.state():
if data is an array and you don't want to track changes to data's items:
viewModel.state(data);
or push them one at a time:
data.foreach(function(el){ viewModel.state.push(el); });
If you want to track changes to each individual item's properties, you will need to use the second method and convert each element into an object composed of ko.observables.
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
I have a tree view in my Backbone app, I use nested collections and models:
Collection:
define(function(require) {
var Backbone = require('backbone')
, UserListModel = require('app/models/userList');
return Backbone.Collection.extend({
model: UserListModel,
url: '/api/lists',
});
});
Model:
define(function(require) {
var Backbone = require('backbone');
return Backbone.Model.extend({
constructor: function(data, opts) {
opts = _.extend({}, opts, {parse: true});
var UserLists = require('app/collections/userLists');
this.children = new UserLists();
Backbone.Model.call(this, data, opts);
},
parse: function(data) {
if (_.isArray(data.children))
this.children.set(data.children);
return _.omit(data, 'chilren');
}
});
});
Part of The View: (full views here: http://laravel.io/bin/O9oYX)
var UserListTreeItemView = Backbone.View.extend({
render: function() {
var data = this.model.toJSON();
data.hasChildren = !!this.model.get('isFolder');
this.$el.html(this.template(data));
if( this.model.get('isFolder') ) {
var list = new UserListTreeView({
collection: this.model.children
});
this.$el.append(list.render().el);
}
return this;
}
});
And I use two Views to render my collection as a tree view. I want to add a search feature to my tree view, I can’t figure out how. It should be able to search name attributes on all models and their nested ones.
Any ideas?
If you have already the models you want on your collection, just use the inherited Underscore method filter() on the collection itself. It will return an Array of models, not a Backbone Collection, though.
http://underscorejs.org/#filter
Supposing filtering by attribute name:
var nameToSearch = "whatever";
var itemsByName = this.model.children.filter(function(item){
return item.get("name").indexOf(nameToSearch) >=0;
}
What I would do is isolate your getData method to cover both cases: filtering on/off.
You didn't specify how do you search, but I'll suppose you have a text input around and you want to use that value. Will that search in the top items only? A search-in-depth would be a little more complicated, involving each parent item to look for the name on its children. For the simple case that you'll be searching for files in every folder, keep the search filter in you parent View state. For that, I normally use a plain vanilla Backbone Model, just to leverage events.
var MySearchView = Backbone.View.extend({
initialize: function(options){
//I like the idea of having a ViewModel to keep state
this.viewState = new Backbone.Model({
searchQuery: ""
});
//whenever the search query is changed, re-render
this.listenTo(this.viewState, "change:searchQuery", this.render);
},
events: {
"click .js-search-button": "doSearch"
},
doSearch: function(e){
e.preventDefault();
var query = this.$(".js-search-input").val();
this.viewState.set("seachQuery", query);
},
render: function(){
var data = this.model.toJSON();
data.hasChildren = !!this.model.get('isFolder');
this.$el.html(this.template(data));
if( this.model.get('isFolder') ) {
//be careful with this, you're not removing your child views ever
if(this._listView) {
this._listView.remove();
}
this._listView = new UserListTreeView({
collection: this.model.children,
**searchQuery: this.viewState.get("searchQuery")**
});
this.$el.append(this._listView.render().el);
}
return this;
}
});
Now in your UserListTreeView, abstract the data-feeding for the template into a method that takes into account the search query:
var UserListTreeView = Backbone.View.extend({
initialize: function(options){
this.searchQuery = options.searchQuery || "";
},
...
getData: function(){
//filter your collection if needed
var query = this.searchQuery;
if(query !== ""){
return this.collection.filter(function(file){
return file.get("name").indexOf(query) >= 0;
}
else {
return this.collection.toJSON();
}
},
render: function() {
var items = this.getData(),
template = this.template(items);
this.$el.empty().append(template);
return this;
}
});
Voilá, the same view will render either the full collection or a filtered version whose items contain the searchQuery in their name. You can adjust the search method just by changing the comparison inside the filter call: you could do RegExp, search only for files starting with (indexOf(searchQuery) == 0), and so on.
Took it longer than expected, hope it helps. Another option would be to implement this in the collection itself, you can override its toJSON() method to return either all, or some items on it. If you find yourself writing another view that needs filterint, then probably it's a better idea to create a SearchableCollection and inherit both from there. Keep it DRY. :)
As a side note: you should have a look at MarionetteJS or build your own specialized views (Collection, and so on) just to save from typing the same over and over again.
I’m not sure I’ve totally understood your app, but here’s how I’ve done something similar before:
In your model add this:
matches: function(search) {
// a very simple and basic implementation
return this.get('name').indexOf(search) != -1;
}
And use it in UserListTreeView’s render:
render: function() {
var search = $someElement.val();
var _this = this;
_.each(this.collection.models, function(model) {
if (model.matches(search)) {
_this.addItem(model);
}
});
return this;
}
Very simple, yet effective. This is actually the most basic version to transfer the idea. You can improve this approach by extending it to other models and collections, checking for some edge cases, and improving its performance by simple optimizations.
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);
}
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));
});
};