Mapping nested KO ViewModels - javascript

I'm trying to map a nested ViewModel (three levels of depth) with Knockout's mapping plugin. When running this code, only the first Level will be mapped correctly. What am I doing wrong here?
Thanks in advance
Here is my Code:
var mapping = {
create: function (options) {
var levelOneItems = new levelOneModel(options.data)
//Some computed observables for level one here...
return levelOneItems;
},
'levelTwoItemList': {
create: function (options) {
var levelTwoItems = new levelTwoModel(options.data)
//Some computed observables for level two here...
return levelTwoItems;
},
'levelThreeItemList': {
create: function (options) {
var levelThreeItems = new levelThreeModel(options.data)
//Some computed observables for level three here...
return levelThreeItems;
}
}
}
}
var levelOneModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelTwoModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelThreeModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var data = [
{
'LevelOneName': 'Apple1',
'levelTwoItemList': [
{
'LevelTwoName': 'Apple2.1',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.1' },
{ 'LevelThreeItemName': 'Apple3.2' }
]
}, {
'LevelTwoName': 'Apple2.2',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.3' },
{ 'LevelThreeItemName': 'Apple3.4' }
]
},
]
}
]
var viewModel = ko.mapping.fromJS(data, mapping);

I've just figured it out myself while playing around with this objects. I hope this helps someone who got into the same trouble.
Here the code:
var mapping1 = {
create: function (options) {
var levelOneItems = ko.mapping.fromJS(options.data, mapping2)
//Some computed observables for level one here...
return levelOneItems;
}
}
var mapping2 = {
'levelTwoItemList': {
create: function (options) {
var levelTwoItems = ko.mapping.fromJS(options.data, mapping3)
//Some computed observables for level two here...
return levelTwoItems;
}
}
}
var mapping3 = {
'levelThreeItemList': {
create: function (options) {
var levelThreeItems = new levelThreeModel(options.data)
//Some computed observables for level three here...
return levelThreeItems;
}
}
}
var levelOneModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelTwoModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelThreeModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var data = [
{
'LevelOneName': 'Apple1',
'levelTwoItemList': [
{
'LevelTwoName': 'Apple2.1',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.1' },
{ 'LevelThreeItemName': 'Apple3.2' }
]
}, {
'LevelTwoName': 'Apple2.2',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.3' },
{ 'LevelThreeItemName': 'Apple3.4' }
]
},
]
}
]
var viewModel = ko.mapping.fromJS(data, mapping1)

Related

Angularjs retain local variable

I have a factory like this:
TestFactory= function () {
var objectName=null;
return {
SetName:function(name) {
objectName = name;
},
GetName:function() {
return objectName;
},
Init:function() {
return angular.copy(this);
}
}
}
A controller like:
TestController = function($scope) {
$scope.TestClick = function () {
var tstA = TestFactory.Init();
var tstB = TestFactory.Init();
tstA.SetName('test A')
tstB.SetName('test B')
console.log('A', tstA.GetName());
console.log('B', tstB.GetName());
}
}
In the console I get Test B for both objects.
How can I make a proper instance of this object?
I would like to use the objectName value in other functions of the factory.
Take into account that in Angular, Factories are singletons, so the instance is always the same.
You can do the following:
TestFactory= function () {
var objectName={};
return {
SetName:function(property,name) {
objectName[property] = name;
},
GetName:function(property) {
return objectName[property];
},
Clear:function(property) {
delete objectName[property]
}
}
}
Then in your controller:
TestController = function($scope, TestFactory) {
$scope.TestClick = function () {
TestFactory.SetName('a','test A')
TestFactory.SetName('b','test B')
console.log('A', TestFactory.GetName('a')); // test A
console.log('B', TestFactory.GetName('b')); // test B
}
}
Couple of issues. First your returning an object rather than a function from your factory.
app.factory('TestFactory', function() {
return function() {
var objectName = null;
var setName = function(name) {
objectName = name;
};
var getName = function() {
return objectName;
};
return {
SetName: setName,
GetName: getName
};
};
});
Then you can just instantiate like this:
var tstA = new TestFactory();
var tstB = new TestFactory();
Services and factories are singletons so I think you can achieve what you want with a more appropriate use of the factory by providing an Init function that returns the common code and unique name like so:
angular.module('app')
.factory('ServiceFactory', serviceFactory);
function serviceFactory() {
return {
Init: function (name) {
return {
objectName: name,
setName: function (name) {
this.objectName = name;
},
getName: function () {
return this.objectName;
}
};
}
};
}
This leaves the possibility to use it as a factory that can initialize many types.
You basically need to create a simple getter/setter.
angular.module('app', [])
.controller('TestController', testController)
.service('serviceFactory', serviceFactory);
testController.$inject = ['serviceFactory'];
function testController(serviceFactory) {
serviceFactory.set('A', {
name: 'test A'
});
serviceFactory.set('B', {
name: 'test B'
});
console.log(serviceFactory.getAll());
console.log(serviceFactory.get('A'));
console.log(serviceFactory.get('B'));
}
function serviceFactory() {
var
_model = {
name: ""
},
_data = {};
return {
set: function(key, data) {
_data[key] = angular.extend({}, _model, data);
},
get: function(key) {
return _data[key];
},
getAll: function() {
return _data;
}
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.22/angular.min.js"></script>
<body ng-app="app" ng-controller="testController"></body>

Alternative to multiple Object.defineProperty

Given a product might have several attributes such as name, price, sku, description and so on - the following will become quite long winded to describe a product model...
function Product(data) {
var productData = data || {};
Object.defineProperty(this, "sku", {
get: function() {
return productData.sku;
}
});
Object.defineProperty(this, "name", {
get: function() {
return productData.name;
}
});
Object.defineProperty(this, "price", {
get: function() {
return productData.price;
}
});
}
module.exports = Product;
What alternatives are there in javascript for this and how is this normally handled?
#Pointy deserves the points here with Object.defineProperties-
function Product(data) {
var productData = data || {};
Object.defineProperties(this, {
"sku": {
get: function() {
return productData.sku;
}
},
"name": {
get: function() {
return productData.name;
}
},
"price": {
get: function() {
return productData.price;
}
}
});
}
module.exports = Product;
Support is near identical to Object.defineProperty so there is no real reason not to use this method when defining multiple properties at the same time.
You can use a single loop to define all the properties:
var self = this;
Object.keys(productData).forEach(function(prop){
Object.defineProperty(self, prop, {
get: function() {
return productData[prop];
}
});
});
Demo

KnockoutJS - ViewModel (Grandparent - Parent - Child) Accessing a Parent Function within Child element

I have a Grandparent, Parent, Child ViewModel relationship setup in knockout and knockout mapping, CustomerViewModel, WorkOrderViewModel, and RepairViewModel.
For each level I flag if the record has been Modified. Then I have a save button that saves the entire Model. The function that Saves the Model is within the Grandparent ViewModel (CustomerViewModel)
Example of a Child level element
<input class="form-control input-sm text-right" name="RepairCost" id="RepairCost" data-bind="value: RepairCost, event: {change: flagRepairAsEdited}" />
Is there a way within the flagRepairAsEdited function I can call the SAVE function within the parent/grandparent?
Thanks so much!
Here is the JS code I'm using (simplified):
var ObjectState = {
Unchanged: 0,
Added: 1,
Modified: 2,
Deleted: 3
};
var workOrderMapping = {
'WorkOrders': {
key: function (workOrders) {
return ko.utils.unwrapObservable(workOrders.WorkOrderId);
},
create: function (options) {
return new WorkOrderViewModel(options.data);
}
},
'Repairs': {
key: function (repairs) {
return ko.utils.unwrapObservable(repairs.RepairId);
},
create: function (options) {
return new RepairViewModel(options.data);
}
}
};
RepairViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, workOrderMapping, self);
self.flagRepairAsEdited = function () {
if (self.ObjectState() != ObjectState.Added) {
self.ObjectState(ObjectState.Modified);
}
//WOULD LOVE TO CALL SAVE FUNCTION HERE
return true;
}
;
}
WorkOrderViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, workOrderMapping, self);
self.flagWorkOrderAsEdited = function () {
if (self.ObjectState() != ObjectState.Added) {
self.ObjectState(ObjectState.Modified);
}
//WOULD LOVE TO CALL SAVE FUNCTION HERE
return true;
}
;
}
CustomerViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, workOrderMapping, self);
self.save = function () {
$.ajax({
url: "/Customers/Save/",
type: "POST",
data: ko.toJSON(self),
contentType: "application/json",
success: function (data) {
ko.mapping.fromJS(data.customerViewModel, workOrderMapping, self);
if (data.newLocation != null)
window.location = data.newLocation;
},
});
},
self.flagCustomerAsEdited = function () {
if (self.ObjectState() != ObjectState.Added) {
self.ObjectState(ObjectState.Modified);
}
return true;
}
;
}
There are 2 ways to do this
a) Pass viewModels as parameters of the child flagRepairAsEdited function:
data-bind="value: RepairCost, event: {change: flagRepairAsEdited.bind($data, $parent, $root)}"
b) Save the link of the parent viewModel inside child viewModel
WorkOrderViewModel = function (data, parent) {
this.parent = parent;
...
}
And use parent.flagWorkOrderAsEdited and parent.parent.flagWorkOrderAsEdited to save parent and grandparent viewmodels

Animating knockout template views

Is there a way to have transitional animations between "page" changes while using knockout for changing templates? I'm looking for something similar to Knockback-Navigators. I cant figure out a way to do this? Is there a package I can use to make this easier? Here is a JSFiddle with the same type of binding my project uses. And a sample of my javascript here:
var View = function (title, templateName, data) {
var self = this;
this.title = title;
this.templateName = templateName;
this.data = data;
this.url = ko.observable('#' + templateName);
};
var test1View = {
test: ko.observable("TEST1")
};
var test2View = {
test: ko.observable("TEST2")
};
var viewModel = {
views: ko.observableArray([
new View("Test 1", "test1", test1View),
new View("Test 2", "test2", test2View)]),
selectedView: ko.observable(),
}
//Apply knockout bindings
ko.applyBindings(viewModel);
//Set up sammy url routes
Sammy(function () {
//Handles only groups basically
this.get('#:view', function () {
var viewName = this.params.view;
var tempViewObj = ko.utils.arrayFirst(viewModel.views(), function (item) {
return item.templateName === viewName;
});
//set selectedView
viewModel.selectedView(tempViewObj);
});
}).run('#test1');
There are plenty of ways of doing this, here is one
ko.bindingHandlers.withFade = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var $element = $(element);
var observable = valueAccessor();
var wrapper = ko.observable(observable());
observable.subscribe(function(value) {
var current = wrapper();
fadeIn = function() {
wrapper(value);
$element.fadeIn();
};
if(current) {
$element.fadeOut(fadeIn);
} else {
$element.hide();
fadeIn();
}
});
ko.applyBindingsToNode(element, { with: wrapper }, bindingContext);
return { controlsDescendantBindings: true };
}
};
http://jsfiddle.net/7E84t/19/
You can abstract the effect like this
ko.transitions = {
fade: {
out: function(element, callback) {
element.fadeOut(callback);
},
in: function(element) {
element.fadeIn();
}
},
slide: {
out: function(element, callback) {
element.slideUp(callback);
},
in: function(element) {
element.slideDown();
}
}
};
html
<div data-bind="withFade: { data: selectedView, transition: ko.transitions.slide }">
http://jsfiddle.net/7E84t/23/

Mustache and nested templates

I have a tree-like setup, where each node contains its own Mustache template that might be wrapped in a parent node's template.
var templates = {
secondTmpl: Mustache.compile("<i>Yet another template.. Here's some text: {{text}}</i> {{date}}"),
template: Mustache.compile("<b>{{val}}</b><p>{{{outlet}}}</p><ul>{{#list}}<li>{{.}} </li>{{/list}}</ul> {{test}}")
};
var tree = [
{
template: "template",
data: { val: "yup!!", list: [1,2,3,4,5], test: function() { return new Date(); } },
children: [
{
view: "Main",
template: "secondTmpl",
data: { text: "Some Value", date: function() { return new Date(); } }
}
]
}
];
function MainView(options) {
this.template = options.template;
this.data = options.data;
this.childViews = options.childViews;
this.el = document.createElement("div");
}
MainView.prototype.render = function() {
View.prototype.render.call(this);
this.postRender();
return this;
};
MainView.prototype.postRender = function() {
this.el.getElementsByTagName("i")[0].style.border = "1px dotted red";
};
function View(options) {
this.template = options.template;
this.data = options.data;
this.childViews = options.childViews;
this.el = document.createElement("div");
}
View.prototype.render = function(renderChildren) {
if(this.childViews) {
this.data.outlet = "<div class=\"outlet\"></div>";
}
this.el.innerHTML = this.template(this.data);
if(this.childViews) {
this.childViews.forEach(function(view) {
this.el.getElementsByClassName("outlet")[0].appendChild(view.render().el);
}, this);
}
return this;
};
function traverse(node) {
var viewOptions = {
template: templates[node.template],
data: node.data
};
if(node.children) {
viewOptions.childViews = node.children.map(function(n) {
return traverse(n);
});
}
return node.view ? new window[node.view + "View"](viewOptions) : new View(viewOptions);
}
function init() {
tree.forEach(function(node) {
var view = traverse(node);
document.body.appendChild(view.render().el);
});
}
window.onload = init;​
Example here: http://jsfiddle.net/b4fTB/
The reason that I have the data in a tree is because the nested templates varies depending on the users data, and because many templates may be wrapped in different templates.
I don't know if what I'm doing here is stupid, but it allows me to render the templates from C# as well, which is quite nice.
So - the question (comments to the above is of course welcome). When dealing with a template with nested templates, it would be nice to have a simple function that only returns dom elements related to the actual template - and not dom elements from the nested templates. Is this even possible? And is it possible in a way that allows for deeply nested templates and not lose a great deal of performance? In other words, I have two templates where one of them is nested within the other in the jsfiddle. It would be nice not having to worry about nested views in the parent view when dealing with the dom.
Alright, I think I've found a way myself.
Following code requires mustachejs and composejs:
var noop = function() {};
var templates = {
secondTmpl: Mustache.compile("<i>Yet another template.. {{{outlet}}}Here's some text: {{text}}</i> {{date}}"),
template: Mustache.compile("<b>{{val}}</b><p>{{{outlet}}}</p><ul>{{#list}}<li>{{.}}</li>{{/list}}</ul> {{test}}")
};
var tree = [
{
view: "Main",
template: "template",
data: { val: "yup!!", list: [1, 2, 3, "Four", 5], test: function() { return new Date(); } },
children: [
{
template: "secondTmpl",
data: { text: "Some Value", date: function() { return new Date(); } }
},
{
view: "Test",
template: "secondTmpl",
data: { text: "ANOTHER TEMPLATE", date: function() { return new Date(); } },
children: [
{
template: "template",
data: { val: "Pretty nested template", list: [56, 52, 233, "afsdf", 785], test: "no datetime here" }
}
]
}
]
}
];
var View = Compose(function(options) {
Compose.call(this, options);
this.el = document.createElement(this.tag);
}, {
tag: "div",
render: function() {
if(this.childViews) {
this.data.outlet = "<div class=\"outlet\"></div>";
}
this.el.innerHTML = this.template(this.data);
this.didRender();
if(this.childViews) {
var lastEl;
this.childViews.forEach(function(view) {
if(!lastEl) {
var outlet = this.el.getElementsByClassName("outlet")[0];
lastEl = view.render().el;
outlet.parentNode.replaceChild(lastEl, outlet);
} else {
var el = view.render().el;
lastEl.parentNode.insertBefore(el, lastEl.nextSibling);
lastEl = el;
}
}, this);
}
this.didRenderDescendants();
return this;
},
didRender: noop,
didRenderDescendants: noop
});
var TestView = View.extend({
didRender: function() {
var nodes = this.el.querySelectorAll("*");
for(var i = 0; i < nodes.length;i++)
nodes[i].style.border = "2px dotted red";
}
});
var MainView = View.extend({
didRender: function() {
var nodes = this.el.querySelectorAll("*");
for(var i = 0; i < nodes.length;i++)
nodes[i].style.backgroundColor = "lightgray";
}
});
function traverse(node) {
var viewOptions = {
template: templates[node.template],
data: node.data
};
if(node.children) {
viewOptions.childViews = node.children.map(function(n) {
return traverse(n);
});
}
return node.view ? new window[node.view + "View"](viewOptions) : new View(viewOptions);
}
function init() {
tree.forEach(function(node) {
var view = traverse(node);
window["view"] = view;
document.body.appendChild(view.render().el);
});
}
window.onload = init;
The trick is to replace the div.outlet with the first child view instead of appending to it. Then it's a matter of inserting the other child views next to each other.

Categories