Mapping hierarchical JSON in TypeScript -KnockoutJS typed object

Let's start with thanks in advance :)

OK, so I'm trying to load / display hierarchical TypeScript / KnockoutJS typed classes from the corresponding JSON data using the knockout.mapping plugin, the hierarchy can be in the Nth degree.

I know that I can do the following to display / load the top level class from JSON data.

var qry = ko.mapping.fromJS(jsData, {}, new Query()); 

However, I cannot figure out how to map / load complex, Nth degree, hierarchical JSON data into a TypeScript / KnockoutJS class set and build parent / child relationships.

I read countless articles, but all of them are not suitable when it comes to hierarchical relationships outside of simple parent / child examples, and I cannot find them using the knockout.mapping plugin.

Here are my abbreviated TypeScript class definitions that I want to display / load. I am a C ++ / C # developer, so JavaScript of this type is very new to me.

TypeScript Objects

 module ViewModel { export class QueryModuleViewModel { public QueryObj: KnockoutObservable<Query>; constructor() { this.QueryObj = ko.observable<Query>(); } public Initialize() { $.getJSON("/api/query/2", null, d => { var qry = ko.mapping.fromJS(d, {}, new Query()); this.QueryObj(qry); }); } } export class Query { public ID: KnockoutObservable<number>; public Name: KnockoutObservable<string>; public RootTargetID: KnockoutObservable<number>; public RootTarget: KnockoutObservable<QueryTarget>; constructor() { this.ID = ko.observable<number>(0); this.Name = ko.observable<string>(); this.RootTargetID = ko.observable<number>(); this.RootTarget = ko.observable<QueryTarget>(); } } export class QueryTarget { public ID: KnockoutObservable<number>; public Name: KnockoutObservable<string>; public ParentID: KnockoutObservable<number>; public Children: KnockoutObservableArray<QueryTarget>; public Parent: KnockoutObservable<QueryTarget>; public Selects: KnockoutObservableArray<QuerySelect>; public FilterID: KnockoutObservable<number>; public Filter: KnockoutObservable<FilterClause>; constructor() { this.ID = ko.observable<number>(0); this.Name = ko.observable<string>(); this.ParentID = ko.observable<number>(0); this.Children = ko.observableArray<QueryTarget>(); this.Parent = ko.observable<QueryTarget>(); this.Selects = ko.observableArray<QuerySelect>(); this.FilterID = ko.observable<number>(0); this.Filter = ko.observable<FilterClause>(); } } export class QuerySelect { public ID: KnockoutObservable<number>; public Name: KnockoutObservable<string>; public Aggregation: KnockoutObservable<string>; public TargetID: KnockoutObservable<number>; public Target: KnockoutObservable<QueryTarget>; constructor() { this.ID = ko.observable<number>(); this.Name = ko.observable<string>(); this.Aggregation = ko.observable<string>(); this.TargetID = ko.observable<number>(); this.Target = ko.observable<QueryTarget>(); } } export class FilterClause { public FilterClauseID: KnockoutObservable<number>; public Type: KnockoutObservable<string>; public Left: KnockoutObservable<string>; public Right: KnockoutObservable<string>; public ParentID: KnockoutObservable<number>; public Parent: KnockoutObservable<FilterClause>; public Children: KnockoutObservableArray<FilterClause>; public QueryTargets: KnockoutObservableArray<QueryTarget>; constructor() { this.FilterClauseID = ko.observable<number>(); this.Type = ko.observable<string>(); this.Left = ko.observable<string>(); this.Right = ko.observable<string>(); this.ParentID = ko.observable<number>(); this.Parent = ko.observable<FilterClause>(); this.Children = ko.observableArray<FilterClause>(); } } } 

JSON will look something like this:

 { "ID": 2, "Name": "Northwind 2", "RootTargetID": 2, "RootTarget": { "ID": 2, "Name": "Customers", "ParentID": null, "FilterID": 2, "Queries": [], "Children": [], "Parent": null, "Selects": [ { "ID": 3, "Name": "CompanyName", "Aggregation": "None", "TargetID": 2, "Target": null }, { "ID": 4, "Name": "ContactName", "Aggregation": "None", "TargetID": 2, "Target": null } ], "Filter": { "FilterClauseID": 2, "Type": "AND", "Left": null, "Right": null, "ParentID": null, "QueryTargets": [], "Parent": null, "Children": [ { "FilterClauseID": 3, "Type": "NE", "Left": "Country", "Right": "Germany", "ParentID": 2, "QueryTargets": [], "Parent": null, "Children": [] }, { "FilterClauseID": 4, "Type": "NE", "Left": "Country", "Right": "Mexico", "ParentID": 2, "QueryTargets": [], "Parent": null, "Children": [] } ] } } } 
+7
typescript knockout-mapping-plugin knockout-mvc
source share
2 answers

OK, so I'm a little further in a straight line, after many hair extensions and numerous tests.

Below is a practically working example of what I'm trying to achieve, the only problem with this is that it does not look right, although going through the code seems to suggest the correct download. Only when I use it with my bindings does it generate a zero raw binding in RootTaget.Filter.Type, which should be filled with a value.

I'm still trying to figure out why, but I will welcome suggestions as to what might be wrong. :)

NOW FIXED AND WORK

semi-working typescript

 ///<reference path="Scripts/typings/jquery/jquery.d.ts"/> ///<reference path="Scripts/typings/knockout/knockout.d.ts"/> ///<reference path="Scripts/typings/knockout.mapping/knockout.mapping.d.ts"/> module ViewModel { export class Query { public ID: KnockoutObservable<number>; public Name: KnockoutObservable<string>; public RootTargetID: KnockoutObservable<number>; public RootTarget: KnockoutObservable<QueryTarget>; constructor(json: any) { this.ID = ko.observable<number>(0); this.Name = ko.observable<string>(); this.RootTargetID = ko.observable<number>(); this.RootTarget = ko.observable<QueryTarget>(); var mapping = { 'RootTarget': { create: function (args) { return new QueryTarget(args.data, null); } } }; ko.mapping.fromJS(json, mapping, this); } } export class QueryTarget { public ID: KnockoutObservable<number>; public Name: KnockoutObservable<string>; public ParentID: KnockoutObservable<number>; public Children: KnockoutObservableArray<QueryTarget>; public Parent: KnockoutObservable<QueryTarget>; public Selects: KnockoutObservableArray<QuerySelect>; public FilterID: KnockoutObservable<number>; public Filter: KnockoutObservable<FilterClause>; constructor(json: any, parent: QueryTarget) { this.ID = ko.observable<number>(0); this.Name = ko.observable<string>(); this.ParentID = ko.observable<number>(0); this.Children = ko.observableArray<QueryTarget>(); this.Parent = ko.observable<QueryTarget>(parent); this.Selects = ko.observableArray<QuerySelect>(); this.FilterID = ko.observable<number>(0); this.Filter = ko.observable<FilterClause>(); var mapping = { 'Children': { create: function (args) { return new QueryTarget(args.data, this); } }, 'Selects': { create: function (args) { return new QuerySelect(args.data, this); } }, 'Filter': { create: function (args) { return new FilterClause(args.data, null); } } }; ko.mapping.fromJS(json, mapping, this); } } export class QuerySelect { public ID: KnockoutObservable<number>; public Name: KnockoutObservable<string>; public Aggregation: KnockoutObservable<string>; public TargetID: KnockoutObservable<number>; public Target: KnockoutObservable<QueryTarget>; constructor(json: any, parent: QueryTarget) { this.ID = ko.observable<number>(); this.Name = ko.observable<string>(); this.Aggregation = ko.observable<string>(); this.TargetID = ko.observable<number>(); this.Target = ko.observable<QueryTarget>(parent); ko.mapping.fromJS(json, {}, this); } } export class FilterClause { public FilterClauseID: KnockoutObservable<number>; public Type: KnockoutObservable<string>; public Left: KnockoutObservable<string>; public Right: KnockoutObservable<string>; public ParentID: KnockoutObservable<number>; public Parent: KnockoutObservable<FilterClause>; public Children: KnockoutObservableArray<FilterClause>; constructor(json: any, parent: FilterClause) { this.FilterClauseID = ko.observable<number>(); this.Type = ko.observable<string>(); this.Left = ko.observable<string>(); this.Right = ko.observable<string>(); this.ParentID = ko.observable<number>(); this.Parent = ko.observable<FilterClause>(parent); this.Children = ko.observableArray<FilterClause>(); var mapping = { 'Children': { create: function (args) { return new FilterClause(args.data, this); } } }; ko.mapping.fromJS(json, mapping, this); } } export class QueryModuleViewModel { public QueryObj: Query; constructor() { var json = { "ID": 2, "Name": "Northwind 2", "RootTargetID": 2, "RootTarget": { "ID": 2, "Name": "Customers", "ParentID": null, "FilterID": 2, "Queries": [], "Children": [], "Parent": null, "Selects": [ { "ID": 3, "Name": "CompanyName", "Aggregation": "None", "TargetID": 2, "Target": null }, { "ID": 4, "Name": "ContactName", "Aggregation": "None", "TargetID": 2, "Target": null } ], "Filter": { "FilterClauseID": 2, "Type": "AND", "Left": null, "Right": null, "ParentID": null, "QueryTargets": [], "Parent": null, "Children": [ { "FilterClauseID": 3, "Type": "NE", "Left": "Country", "Right": "Germany", "ParentID": 2, "QueryTargets": [], "Parent": null, "Children": [] }, { "FilterClauseID": 4, "Type": "NE", "Left": "Country", "Right": "Mexico", "ParentID": 2, "QueryTargets": [], "Parent": null, "Children": [] } ] } } } //$.getJSON("/api/query/2", null, // d => { // this.QueryObj = new Query(d); // }) this.QueryObj = new Query(json); } } } window.onload = () => { ko.applyBindings(new ViewModel.QueryModuleViewModel()); }; 

html binding check

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>TypeScript Knockout Mapping Query Test</title> <link rel="stylesheet" href="app.css" type="text/css" /> <script src="Scripts/jquery-2.0.2.js" type="text/javascript"></script> <script src="Scripts/knockout-2.2.1.debug.js" type="text/javascript"></script> <script src="Scripts/knockout.mapping-latest.debug.js" type="text/javascript"></script> <script src="query.js"></script> <!--<script src="my_js_query_test_all.js"></script>--> </head> <body> <h1>TypeScript Knockout Mapping Query Test</h1> <div data-bind="with: QueryObj"> <span data-bind="blah: console.log($context)"></span> <p>Query Name: <input data-bind="value: Name" /></p> <hr /> <p>Quick test of RootTarget and Filter data</p> <p>RootTarget.ID: <input data-bind="value: RootTarget().ID" /></p> <p>RootTarget.Name: <input data-bind="value: RootTarget().Name" /></p> <p>TYPE: <input data-bind="value: RootTarget().Filter().Type" /></p> <hr /> <p>RootTarget.FilterClause Hierarcy</p> <div data-bind="with: RootTarget().Filter"> <div data-bind="template: { name: 'QueryListClauseTemplate' }"></div> </div> <hr /> <p>RootTarget.Selects</p> <div data-bind="foreach: { data: RootTarget().Selects }"> <div data-bind="template: { name: 'QueryListSelectsTemplate' }"></div> </div> </div> <script type="text/template" id="QueryListClauseTemplate"> <a title="FilterClause.Type" href="#" data-bind="text: Type" /> <div data-bind="foreach: { data: Children }"> <div data-bind="template: { name: 'QueryListClauseTemplate' }"></div> </div> </script> <script type="text/template" id="QueryListSelectsTemplate"> <a title="Select.Name" href="#" data-bind="text: Name" /> </script> </body> </html> 
+6
source share

Another approach is to create a .d.ts file that defines TypeScript interfaces that describe nested collections of observable types that are generated by the knockout mapping plugin, based on your C # classes.

You will then get the required type checking using the .d.ts file (just as you would use the .d.ts file from a specifically typed github project to get type checking for existing javaScript libraries).

I created a console application to test my C # dll using reflection. I used a special attribute to mark the types for which TypeScript interfaces were to be created. (I also had to create a custom attribute to note which properties were NOT created as observables, since the mapping plugin only makes leaf nodes of your nested collections as observables).

This worked for me, as I was able to quickly restore the .d.ts file when my C # model changed. And I was able to check the type for all parts of my ViewModel knockout.

  //the custom attributes to use on your classes public class GenerateTypeScript : Attribute { public override string ToString() { return "TypeScriptKnockout.GenerateTypeScript"; } } public class NotObservable : Attribute { public override string ToString() { return "TypeScriptKnockout.NotObservable"; } } //example of using the attributes namespace JF.Models.Dtos { [TypeScriptKnockout.GenerateTypeScript] public class ForeclosureDetails : IValidatableObject, IQtipErrorBindable { [TypeScriptKnockout.NotObservable] public Foreclosure Foreclosure { get; set; } //strings used for form input and validation public string SaleDateInput { get; set; } public string SaleTimeInput { get; set; } ....etc. //the console app to generate the .d.ts interfaces void Main() { string dllPath = @"binFolder"; string dllFileName = "JF.dll"; Assembly assembly = Assembly.LoadFrom(Path.Combine(dllPath,dllFileName)); List<string> interfacesToIgnore = new List<string>{"IValidatableObject"}; //stuff that won't exist on the client-side, Microsoft Interfaces var types = from t in assembly.GetTypes() where (t.IsClass || t.IsInterface) && t.GetCustomAttributes(true).Any( a => ((Attribute)a).ToString() == "TypeScriptKnockout.GenerateTypeScript") orderby t.IsClass, t.Name select t; Console.WriteLine("/// <reference path=\"..\\Scripts\\typings\\knockout\\knockout.d.ts\" />"); foreach (var t in types) { //type Console.Write("{0} {1}", " interface", t.Name); //base class if(t.BaseType != null && t.BaseType.Name != "Object"){ Console.Write(" extends {0}", t.BaseType.Name); } //interfaces var interfacesImplemented = t.GetInterfaces().Where (i => !interfacesToIgnore.Contains(i.Name) ).ToList(); if(interfacesImplemented.Count() > 0){ Console.Write(" extends"); var icounter = 0; foreach (var i in interfacesImplemented) { if(icounter > 0) Console.Write(","); Console.Write(" {0}", i.Name ); icounter++; } } Console.WriteLine(" {"); //properties foreach (var p in t.GetProperties()) { var NotObservable = p.GetCustomAttributes(true).Any(pa => ((Attribute)pa).ToString() == "TypeScriptKnockout.NotObservable" ); Console.WriteLine(" {0}: {1};", p.Name, GetKnockoutType(p, NotObservable)); } Console.WriteLine(" }\n"); } } public string GetKnockoutType(PropertyInfo p, bool NotObservable){ if(p.PropertyType.Name.StartsWith("ICollection") || p.PropertyType.Name.StartsWith("IEnumerable") || p.PropertyType.Name.StartsWith("Dictionary") || p.PropertyType.Name.StartsWith("List")) { return String.Format("KnockoutObservableArray<{0}>", p.PropertyType.GenericTypeArguments[0].Name); } var typeName = p.PropertyType.Name; if(typeName.StartsWith("Nullable")) typeName = p.PropertyType.GenericTypeArguments[0].Name; switch (typeName) { case "Int32" : case "Decimal" : return NotObservable ? "number" : "KnockoutObservable<number>"; case "String" : return NotObservable ? "string" : "KnockoutObservable<string>"; case "DateTime" : return NotObservable ? "Date" : "KnockoutObservable<Date>"; case "Boolean": return NotObservable ? "boolean" : "KnockoutObservable<boolean>"; case "Byte[]": return NotObservable ? "any" : String.Format("KnockoutObservableAny; //{0}", typeName); default: if(NotObservable) return typeName; bool isObservableObject = true; var subProperties = p.PropertyType.GetProperties(); foreach (var subProp in subProperties) { if( subProp.PropertyType.IsClass && !subProp.PropertyType.Name.StartsWith("String") && !subProp.PropertyType.Name.StartsWith("ICollection") && !subProp.PropertyType.Name.StartsWith("IEnumerable") && !subProp.PropertyType.Name.StartsWith("Dictionary") && !subProp.PropertyType.Name.StartsWith("List") ) { isObservableObject = false; } } return isObservableObject ? String.Format("KnockoutObservable<{0}>", typeName) : typeName; } } //example of the interfaces generated interface ForeclosureDetails extends IQtipErrorBindable { Foreclosure: Foreclosure; SaleDateInput: KnockoutObservable<string>; SaleTimeInput: KnockoutObservable<string>; ...etc. 
+1
source share

All Articles