How to bind/referece grand parent property to/inside child knockout - data-binding

Hi guys looking for some basic advice.
I have four models: BoardViewModel, List, Card, Member
var Member = function (id, name, avatar) {
var self = this;
self.id = id;
self.name = name;
self.avatar = avatar;
self.isChecked = ko.observable(false);
};
I am instantiating members property inside BoardViewModel. But I want to use a copy of this model inside each Card model to instantiate a list of assigned members.
Each card stores comma separated list of member references like
",1,2,4,5"
I am writing a loop to BoardViewModel.members and mark members as checked if id references match bore I assign it as Card.members.
The last piece of the puzzle I am missing is reference to the BoardViwModel.members.
I have a lovely example fiddler that would somewhat help to build a picture of what I am talking about.
Just bear in mind that once I have this working properly I want to replace view() binding
foreach: $root.members
with
foreach: members
If at all possible I would like to avoid passing BoardViewModel.members as parameter into List and then into Card.
Update 1
As suggested by #Jeroen here's a simplified version of my fiddler.
The top view() model which encompases a concept of lists:
var BoardViewModel = function (lists, members) {
var self = this;
// in reality members are fetched via ajax call to the server
// and instantiate as a ko.observableArray()
self.groupMembers = ko.observableArray(members);
self.lists = ko.observableArray(lists);
...
}
In reality this has a signature like this:
var boardViewModel = function (initialData)
moving on.
The child List model which encompases a concept of cards:
var List = function (id, name, cards, sortOrder, groupMembers) {
var self = this;
self.cards = ko.observableArray(cards);
...
}
in reality:
var list = function (item, groupMembers)
nothing special there really.
The child Card model which encompases the concept of card items (but lets not go there yet):
var Card = function (id, title, description, contentItemId, members, groupMembers) {
var self = this;
self.members = ko.observableArray(parseMembers(members));
// now remember each card has a members property
// which stores comma separated list ",1,4"
function (members) {
var memberList = groupMembers;
var memberRefList = members.split(',');
ko.utils.arrayForEach(memberList, function(member){
ko.utils.arrayForEach(memberRefList, function(memberId){
if(member.id === meberId) {
member.isChecked(true);
}
});
});
return memberList;
}
...
}
in reality:
var card = function (item, groupMembers)
nothing too fancy there either.
I currently have something like this working on my dev environment.
Problem:
Those with keen eyes probably noticed the way I was passing groupMembers all the way up. I am not particularly hyped about the idea.
Anyone know a better way of implementing this? i.e. why can't I just do something like
var memberList = self.parent.parent.groupMembers;
for instance.

As per me, the better way to do is to have the child viewmodels inside the parent view-model. like this where you can access the parent data members as well as methods directly.
ViewModel
var BoardViewModel = function(){
var self = this;
this.members = ko.observableArray();
this.lists = ko.observableArray();
// Child View Models
this.Member = function(member){
this.id = member.id;
this.name = member.name;
this.avatar = member.avatar;
this.isChecked = ko.observable(false);
}
this.List = function(list){
// same for this
};
this.Card = function(card){
// same for this
};
// a Method to bind the data with the observables and arrays
// Assuming data is a json object having Members, List objects
this.applyData = function(data){
self.members(jQuery.map(data.Members, function(item){
return new self.Member(item);
}));
self.lists(jQuery.map(data.Lists, function(item){
return new self.List(item);
}));
}
}
onDom ready
// json object holding your data
var data = {
"Members" : [
],
"Lists" : [
],
"Cards" : [
]
};
var vm = new BoardViewModel();
$(function(){
ko.applyBindings(vm, document.getElementById('myModule'));
vm.applyData(data);
});

Related

How to send an array using Url.Action

I have an array of integers called data which I would like to send from my View to a specific controller, I could see that i can send integers and strings and it works with the code that I have so far, but when I try to send an array I can get the data correctly.
This is the code that I have in my view, it is something simple just to be in perspective.
function SeeStation() {
var data = [];
var i = 0;
$("input:checkbox:checked").each(function () {
data[i] = $(this).val();
});
window.location.href = "#Url.Action("ExportData", "Dispatch")?id=" + data;
}
and this is the code in the controller. I know it doesn't make much sense but so far I am focused on correctly obtaining the array by parameter.
public ActionResult ExportData(int[] id)
{
var data = cn.ESTACIONDESPACHOes.ToList();
return View(data);
}
In my array data I store something like this [1,2,3] and I would like to get something similar in the controller array id.
It will not bind like that.
To get the id array in your action you need to have the link at the end like this: *Dispatch/ExportData?id=1&id=2&id=3*
Your "#Url.Action("ExportData", "Dispatch")?id=" + data; will not generate that (data will give the numbers separated with commas).
You can just build the query string when you enumerate the checkboxes.
function SeeStation() {
var data = '';
$("input:checkbox:checked").each(function () {
data += 'id='$(this).val() + '&';
});
window.location.href = "#Url.Action("ExportData", "Dispatch")?" + data;
}
You will have a "&" in the end. You can easily remove it, but it will not affect anything.
There may be better ways to do this though, but I just used your function.
try
#Url.Action("ExportData", "Dispatch", new { id= [1,2,3] })
Store the Values in the Hidden Fields
#Html.HiddenFor(m => m.Ids, new { #Value = [1,2,3] })
Then Using the Ajax Get Method Pass the Hidden fields
In the Controller Method Convert the sting to array using string extension method
function SeeStation() {
var data = [];
var i = 0;
$("input:checkbox:checked").each(function () {
data[i] = $(this).val();
});
location.href = '#Url.Action("ExportData", "Dispatch")?id=' + data;
}
Please remove window keyword.

Do not post SelectList with Knockout

I'm running an ASP.net MVC4 application with Knockout.
I have a generic script that posts my Knockout Forms.
I need to optimize the data sent to the server, because when i post my Knockout ViewModel, SelectList with all items are posted too!
Example Server ViewModel :
Public Class FooViewModel
Public Property Bar As String
Public Property Products As List(Of SelectListItem)
End Class
The JS code to convert my Knockout ViewModel to JSON
var data = ko.toJSON(viewModel);
data variable contains all products items and that's not very optimized.
I found this code (which work) :
viewModel.toJSON = function () {
var copy = ko.toJS(this);
// remove any unneeded properties
delete copy.Products;
return copy;
}
But I need a generic solution ... ! And here I don't see how i can make it generic ...
A quick and dirty solution would be to add a suffix on every array properties like "_NoPost" and then loop and delete every property that has this suffix, but it smells ... bad :/
Any thoughts ?
The one option is to separate your form data from your lookup data like the following. This will allow you to get hold of only your form data when you need to post it to the server.
Public Class FormViewModel
Public Property Bar As String
End Class
Public Class FooViewModel
Public Property FormData As FormViewModel
Public Property Products As List(Of SelectListItem)
End Class
Which will allow you to
var data = ko.toJSON(viewModel);
$post(url, data.FormData, function(d){...});
In your HTML you will also have to include the FormData as part of the variable i.e.
<input data-bind="value: FormData.Bar">
EDIT
Based on your feedback you can use the following function to construct a "clean" object for you. The idea is to pass in the original JSON object as well as a mapping object which will indicate which of the properties should be excluded/left behind:
function MapJson(obj, map) {
if (obj == undefined)
return;
map = map || {};
var ret = {};
for (var prop in obj) {
if (map[prop] != undefined && map[prop] == false)
continue;
if (typeof obj[prop] !== "object")
ret[prop] = obj[prop];
else {
if (map.constructor == Array) {
ret[prop] = MapJson(obj[prop], map[0]);
}
else
ret[prop] = MapJson(obj[prop], map[prop]);
}
}
return ret;
}
You can then use it like this - by setting the property's value to false it will be excluded from the data. The sample shows how to block both an array within a child object as well as an array within an array:
var obj = {
Name: "John Doe",
Vehicle: {
Details: {
Make: "Mazda",
Model: 2010
},
Registration: "ABC123",
ServiceDates: ["01 Jan", "23 Feb", "13 March"]
},
WeekDays: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
Children: [{ Name: "Mary", Age: 4, Hobbies: ["Soccer", "Chess", "Swim"] }, { Name: "Jane", Age: 2, Hobbies: ["Tennis", "Movies", "Reading"] }]
};
var map = {
Vehicle: {
ServiceDates: false
},
Children: [{
Hobbies: false,
}]
};
MapJson(obj, map);
Hope it helps.
EDIT 2
Herewith a working sample based on the data you posted in your comment.
var vm = {
"Type":"PropertyTax",
"Label":"d",
"StartDate":"2015-01-01T00:00:00",
"EndDate":"2015-12-31T00:00:00",
"Value":0,
"RegularizationMonth":0,
"TotalConsumption":null,
"UnitPrice":null,
"Active":true,"Products":[{"Selected":false,"Text":"XXX 39","Value":"28"},{"Selected":false,"Text":"ZZZ","Value":"38"}],"ChargeProducts":[{"ProductID":"28","Products":[{"Selected":false,"Text":"XXX 39","Value":"28"},{"Selected":false,"Text":"XXX 41","Value":"8"}]}],
"map":{"Products":false,"ChargeProducts":[{"Products":false}]}
};
var result = MapJson(vm, vm.map);
console.log("Result: ", result);
If you use KO.Mapping, you can choose certain pieces to ignore.
var mapping = { 'ignore': ["SomeFieldProperty"] };
ko.mapping.fromJS(data, mapping, viewModel);
This section in the documentation lists all the ways you can manipulate the bindings coming and going, for ko.mapping:
http://knockoutjs.com/documentation/plugins-mapping.html
Scroll down to the bottom for the bits on ignore as well as topics covering how you can manage bindings and debinding.

Knockout 3.2 component data context

I am using Knockout 3.2 and the new component system. I am trying to have components that include sub-components.
Home Page (component - with HomePageViewModel)
NewsFeed1 (component with HomePageViewModel.NewsFeedViewModel1)
NewsFeed2 (component with HomePageViewModel.NewsFeedViewModel2)
HomePageViewModel
var viewModel = (function () {
function viewModel() {
this.message = ko.observable("Welcome to DKT!");
this.newsFeedViewModel = new gr.viewModel();
this.newsFeedViewModel2 = new gr.viewModel();
this.newsFeedViewModel.message("Message 1");
this.newsFeedViewModel2.message("Message 2");
}
return viewModel;
})();
NewsFeedViewModel
var viewModel = (function () {
function viewModel() {
this.message = ko.observable("This is the profile!");
}
return viewModel;
})();
As you can see the HomePageViewModel contains both the NewsFeedViewModel. I now want to be able to use these as the DataContext/BindingContext of my two components but this does not seem to work.
Home.html
<news-feed data-bind="newsFeedViewModel"></news-feed>
<news-feed data-bind="newsFeedViewModel2"></news-feed>
Both these components do not use the ViewModels from the HomePageViewModel but uses a new NewsFeedViewModel. How can I make the datacontext of both these components bind to the viewModels stored in the top component (home)?
Generally, you would want to supply your component with any data via params. For example, with your structure, you could create the component like:
ko.components.register("news-feed", {
viewModel: function (params) {
this.vm = params.vm;
},
template: "<h2>News Feed</h2><div data-bind=\"text: vm.message\"></div>"
});
Then, you would define the elements like:
<news-feed params="vm: newsFeedViewModel"></news-feed>
<news-feed params="vm: newsFeedViewModel2"></news-feed>
You could choose to pass the message in directly for each and/or choose whatever names make sense for your params (rather than vm).
Sample: http://jsfiddle.net/rniemeyer/fssXE/

Ember: ContainerView and Child Views binding data not working

I'm trying to use Ember to dynamically create child views in a ContainerView.
The problem is those child views need data bindings to a value from an attribute given to the container view.
Here is a bit of code showing roughly what I'm doing:
ReferenceCat.EditorView = Ember.ContainerView.extend({
init: function(){
this._super();
if(this.get('model.type') != undefined){
this.modelTypeChanges();
}
},
modelTypeChanges : function(){
// 1st step: Remove all children of my view
this.removeAllChildren();
var model = this.get('model');
// 2nd step is to run through all of the type information
// generating the views that we need to actually
// fill in this reference
var tI = model.get('typeInfo');
var self = this;
tI.forEach(function(field){
// Now we have a field
switch(field.type){
case "string":
// add new child view here with data binding to data.<field.name>
break;
}
});
}
});
And this class is referenced like this:
{{view ReferenceCat.EditorView
modelBinding=model}}
In your modelTypeChanges function instead of using a switch statement to create the different types of childViews you need to override the ContainerView's createChildView function (http://emberjs.com/api/classes/Ember.ContainerView.html#method_createChildView). The createChildView function itself will return the instantiated childView and in that overidded function itself you can place your switch statement. So it will look something like this...
createChildView: function(view, attrs) {
switch(attr.type) {
case "string":
view = yourview //your view class here, it is overriding the default
break;
}
.....
...
//Once you place all your case statements above
//make sure to call the parents createChildView function.
//This line is actually creating the view you want with the
//properties that are defined in the object attrs
return this._super(view, attrs);
}
So make sure when you call your overridden createChildView function to pass it the object you want bounded in the childView as a property of the object you pass as its second parameter...
var self = this,
tempviewarray = [];
tI.forEach(function(field){
var view = self.createChildView(null, field);
tempviewarray.push(view);
});
// add all the instantiated views to the ContainerView as children views
this.pushObjects(tempviewarray);

Best way to typecast deserialised JSON

I think I've established that in as3corelib JSON.decode I have no choice but to deserialise to a plain old flex object.
var data:Object = JSON.decode(json);
If I then want to get the data contained in the object into another type I can't use type casting. I have to instantiate a new instance and add the properties manually.
var data:Object = JSON.decode(json);
var model:Model = new Model();
model.name = data.name;
model.notes = data.notes;
A pain and a bit ugly, but I'm guessing this is the price to be paid for going from untyped json to a flex type. My first question is whether my assumption is correct and there is no prettier way to create my model instance with the data contained within the json?
My second question, if so then before I write my own method to do this, is there anything inside the flex api that will take the data object and mixin it's values to my model instance?
Cheers,
Chris
the approach I've always used proved to be part of the AMF3 serialization mechanism in ActionScript.
have a look at IExternalizable and registerClassAlias.
now what I use is the following:
interface ISerializable {
public function getRawData():Object;
public function setRawData(param:Object):void;
}
function registerType(id:String, type:Class):void {
//implementation
}
function getTypeByID(id:String):Class {
//implementation
}
function getTypeID(type:Class):String {
//implementation
}
and to the decoder/encoder you register a class alias.
serialization of an object works as follows:
var raw:Object = model.getRawData();
raw[" type"] = getTypeID(model);
var encoded:String = JSON.encode(raw);
decoding works as follows:
var raw:Object = JSON.decode(raw);
var cl:Class = getTypeByID(raw[" type"]);
if (cl == null) throw new Error("no class registered for type: "+raw[" type"]);
delete raw[" type"];
var model:ISerializable = new cl();
model.setRawData(raw);
you will need to do this recursively on the whole deserialized JSON tree, starting at the leafs.
For cyclic reference, you'll need a trick.
I had an implementation of this somewhere, but I can't find it.
You can loop within the field of you json decoded object and assign them into your model:
function json2model(json:String):Model{
var data:Object = JSON.decode(json);
var m:Model=new Model();
for (var field:String in data) {
if (m.hasOwnProperty(field)) {
m[field] = data[field];
}
}
return m;
}
var model:Model=json2model(json)
or add a static function within your Model if you preffer:
public class Model {
//...
public static function fromJSon(json:String):Model {
var data:Object = JSON.decode(json);
var m:Model=new Model();
for (var field:String in data) {
if (m.hasOwnProperty(field)) {
m[field] = data[field];
}
}
return m;
}
}
}
var model:Model=Model.fromJSon(json);

Resources