Kendo UI TreeListViews with complex data - javascript

Background Information
In short i'm looking to achieve "mostly" whats shown here ...
http://demos.telerik.com/kendo-ui/treelist/remote-data-binding
... except it's a bit of a mind bender and the data comes from more than base endpoint url in my case.
I am trying to build a generic query building page that allows users to pick a context, then a "type" (or endpoint) and then from there build a custom query on that endpoint.
I have managed to get to the point where I do this for a simple query but now i'm trying to handle more complex scenarios where I retrieve child,or deeper data items from the endpoint in question.
With this in mind ...
The concept
I have many endpoints not all of which OData but follow mostly OData v4 rules, and so I am trying to build a "TreeGrid" based having selected an endpoint that will expose the expansion options available to the query.
All my endpoints have a custom function on it called GetMetadata() which describes the type information for that endpoint, where an endpoint is for the most part basically a REST CRUD<T> implementation which may or may not have some further custom functions on it to handle a few other business scenarios.
So, given a HTTP get request to something like ...
~/SomeContext/SomeType/GetMetadata()
... I would get back an object that looks much like an MVC / WebAPI Metadata container.
that object has a property called "Properties" some of which are scalar and some of which are complex (as defined in the data).
I am trying to build a TreeListDataSource or a HierarchicalDataSource object that I can use to bind to the Kendo treeList control for only the complex properties, that dynamically builds the right get url for the meta and lists out the complex properties for that type based on the property information from the parent type with the root endpoint being defined in other controls on the page.
The Problem
I can't seem to figure out how to configure the kendo datasource object for the TreeGrid to get the desired output, I think for possibly one of two reasons ...
The TreeListDataSource object as per the demo shown here: http://demos.telerik.com/kendo-ui/treelist/local-data-binding seems to imply that the hierarchy based control wants a flat data source.
I can't figure out how to configure the datasource in such a way that I could pass in the parent meta information (data item from the source) in order to build the right endpoint url for the get request.
function getDatasource(rootEndpoint) {
return {
pageSize: 100,
filter: { logic: 'and', filters: [{ /* TODO:possibly filter properties in here? */ }] },
type: 'json',
transport: {
read: {
url: function (data) {
//TODO: figure out how to set this based on parent
var result = my.api.rootUrl + endpoint + "/GetMetadata()";
return result;
},
dataType: 'json',
beforeSend: my.api.beforeSend
}
},
schema: {
model: {
id: 'Name',
fields: {
Type: { field: 'Type', type: 'string' },
Template: { field: 'Template', type: 'string' },
DisplayName: { field: 'DisplayName', type: 'string' },
ShortDisplayName: { field: 'ShortDisplayName', type: 'string' },
Description: { field: 'Description', type: 'string' },
ServerType: { field: 'ServerType', type: 'string' }
}
}
parse: function (data) {
// the object "data" passed in here will be a meta container, a single object that contains a property array.
$.each(data.Properties, function (idx, item) {
item.ParentType = data;
item.Parent = ??? where do I get this ???
});
return data.Properties;
}
}
};
}
Some of my problem may be down to the fact that metadata inherently doesn't have primary keys, I wondered if perhaps using parse to attach a generated guid as the key might be an idea, but then I think Kendo uses the id for the question on the API when asking for children.

So it turns out that kendo is just not geared up to do anything more than serve up data from a single endpoint and the kind of thing i'm doing here is a little bit more complex than that, and further more due to the data being "not entity type data" I don't have common things like keys and foreign keys.
With that in mind I chose take the problem away from kendo altogether and simply handle the situation with a bit of a "hack that behaves like a normal kendo expand but not really" ...
In a treegrid, when kendo shows an expandable row it renders something like this in the first cell ...
With no expanded data or a data source that is bound to a server this cell is not rendered.
so I faked it in place and added an extra class to my version .not-loaded.
that meant I could hook up a custom block of js on click of my "fake expand", to build the right url, do my custom stuff, fake / create some id's, then hand the data to the data source.
expandList.on('click', '.k-i-expand.not-loaded', function (e) {
var source = expandList.data("kendoTreeList");
var cell = $(e.currentTarget).closest('td');
var selectedItem = source.dataItem($(e.currentTarget).closest('tr'));
my.type.get(selectedItem.ServerType, ctxList.val(), function (meta) {
var newData = JSLINQ(meta.Properties)
.Select(function (i) {
i.id = selectedItem.id + "/" + i.Name;
i.parentId = selectedItem.id;
i.Selected = my.type.ofProperty.isScalar(i);
i.TemplateSource = buildDefaultTemplateSourceFor(i);
return i;
})
.ToArray();
for (var i in newData) {
source.dataSource.add(newData[i]);
}
$(e.currentTarget).remove();
source.expand(selectedItem);
buildFilterGrid();
generate();
});
});
This way, kendo gets given what it epects for a for a treeviewlist "a flat set with parent child relationships" and I do all the heavy lifting.
I used a bit of JSLINQ magic to make the heavy lifting a bit more "c# like" (i prefer c# after all), but in short all it does is grab the parent item that was expanded on and uses the id from that as the parent then generates a new id for the current item as parent.id + "/" + current.name, this way everything is unique as 2 properties on an object can't have the same name, and where two objects are referenced by the same parent the prefix of the parents property name makes the reference unique.
It's not the ideal solution, but this is how things go with telerik, a hack here, a hack there and usually it's possible to make it work!
Something tells me there's a smarter way to do this though!

Related

How to dynamically build a table from a JSON array?

I've placed an AJAX GET call in an Enyo view. The GET calls a web service which returns an array of records including the column headers.
My aim is to dynamically build a table with this returned array, where a row is created for each index and columns for each header within the index.
What I do know in terms of Enyo is to create one record by mapping the AJAX response headers to component fields:
this.$.actionsTaken.setContent( inResponse.ActionsTaken);
But I'm not sure how to do that dynamically and create the table on the fly.
So for example when I inspect the web service response my index 0 contains the following: (Where ActionsTaken, Application and EM are the col headers.)
{
ActionsTaken: "Tested uptime"
Application: "2011 Hanko"
EM: "EM102 "
}
Question:
How can you dynamically build a table from a JSON array?
The AJAX GET:
fetch: function() {
var ajax = new enyo.Ajax({
url: "http://testservice.net/WebService/GetHistory",
async: false,
handleAs:"json",
xhrFields: {withCredentials: true}
});
ajax.go(this.data);
ajax.response(this, "gotResponse");
ajax.error(this, function(inSender, inError) {
Misc.hideMask();
ViewLibrary.back();
Misc.showToast("Error retrieving data");
});
},
gotResponse: function(inSender, inResponse)
{
var debugVar = inResponse;
this.$.actionsTaken.setContent( inResponse.ActionsTaken);
this.$.configurationItemLogicalName_value.setContent( inResponse.Application);
this.$.eM.setContent( inResponse.EM);
},
The components that hold the data values:
{name:"outage_table", kind: "FittableRows",components:[
{content: "Actions Taken", name: "actionsTaken", },
{content: "Application", name: "application", },
{content: "EM", name: "eM", },
]}
Depending on the complexity of all your data, you might be able to do this fairly simply. Iterate through your array and on each object, you can then iterate through its keys and create each column with its data.
Something like:
for (var k in theObject) {
// make column k
// add theObject[k] to it
}
I think the problem is that you have created named components that match this example data, but it is unknown if those will always be the same keys?
There is a kind (enyo.DataTable, which, surprisingly, I have never used) that you might use instead. It lets you add rows (no headers) so you would make your first row from all the object keys, then the next row would be those keys' data. It is derived from DataRepeater so there may be some implementation to work out, such as using an enyo.Collection to store your data and then set the collection to the DataTable.
The other alternative that matches closer to what you have would be to just dynamically create the components as you need them:
this.$.outage_table.createComponents([{...}]);
this.$.outage_table.render(); // need to re-render when dynamically adding components

Aggregate nested model data in Sails.JS/Waterline

I'm struggling with where to put my business logic in my SailsJS project.
My data model is as follows:
Portfolio (hasMany) -> Positions (hasOne) -> Asset-> (hasMany) Ticks
I'm looking to aggregate data of the children using computed properties on the parent model cascading all the way up to Portfolio. e.G an Asset knows the newest tick (price), the position knows its current price (numberShares * newest Tick of its Asset), etc. Ideally i want to add this data to the JSON so i can move business logic away from the Ember client.
This works fine for one level, but if i try to use a computed property of a child, it's either non defined or when using toJSON() empty
Asset.js:
latestTick: function () {
if (this.ticks.length> 0)
{
this.ticks.sort(function(a, b){
var dateA=new Date(a.date), dateB=new Date(b.date)
return dateB-dateA
})
return this.ticks[Object.keys(this.ticks)[0]].price;
}
return 0;
},
toJSON: function() {
var obj = this.toObject();
if (typeof this.latestTick === 'function')
obj.latestTick = this.latestTick();
return obj;
}
This works (after adding the typeof check as depending on whether it was returned nested or not it didn't work).
Now in Position.js
assetID:{
model: "Asset",
},
i want to calculate currentValue:
currentValue: function () {
return this.assetID.latestTick() * this.numberShares;
},
which does not work because the function is not available (only the hardcoded attributes). I tried using Tick.Find()... but ran into async issues (JSON is returned before data is fetched).
I tried to force embedding / populate the associations in the JSON but it made no difference. As soon as I'm crossing more than one hierarchy level i don't get any data.
Where should/can i do this aggregation? I don't want to use custom controller actions but leverage the REST API.
Waterline does not support deep populates. If you want to implement a deep query like that, you can use native (https://sailsjs.com/documentation/reference/waterline-orm/models/native) if you are using Sails < 1, or manager (https://sailsjs.com/documentation/reference/waterline-orm/datastores/manager) if you are using Sails >= 1
That way you can use whatever your database is capable for building this kind of "more advanced" queries.

Dojo JsonRestStore with array not at root-level of JSON response

Is there a way to configure a JsonRestStore to work with an existing web service that returns an array of objects which is not at the root-level of the JSON response?
My JSON response is currently similar to this:
{
message: "",
success: true,
data: [
{ name: "Bugs Bunny", id: 1 },
{ name: "Daffy Duck", id: 2 }
],
total: 2
}
I need to tell the JsonRestStore that it will find the rows under "data", but I can't see a way to do this from looking at the documentation. Schema seems like a possibility but I can't make sense of it through the docs (or what I find in Google).
My web services return data in a format expected by stores in Ext JS, but I can't refactor years worth of web services now (dealing with pagination via HTTP headers instead of query string values will probably be fun, too, but that's a problem for another day).
Thanks.
While it's only barely called out in the API docs, there is an internal method in dojox/data/JsonRestStore named _processResults that happens to be easily overridable for this purpose. It receives the data returned by the service and the original Deferred from the request, and is expected to return an object containing items and totalCount.
Based on your data above, something like this ought to work:
var CustomRestStore = declare(JsonRestStore, {
_processResults: function (results) {
return {
items: results.data,
totalCount: results.total
};
}
});
The idea with dojo/store is that reference stores are provided, but they are intended to be customized to match whatever data format you want. For example, https://github.com/sitepen/dojo-smore has a few additional stores (e.g. one that handles Csv data). These stores provide good examples for how to handle data that is offered under a different structure.
There's also the new dstore project, http://dstorejs.io/ , which is going to eventually replace dojo/store in Dojo 2, but works today against Dojo 1.x. This might be easier for creating a custom store to match your data structure.

Allow user to specify a Backbone model's ID at creation time

When saving a model, Backbone determines whether to send an HTTP POST or PUT request by whether or not the model's ID attribute is set. If there is an ID, the model is considered to already exist.
For my application, this logic is incorrect because I must allow the user to specify an ID (as I interact with a poorly designed legacy system).
How should I handle this problem? I still would like to use PUT if the model is changed.
I am considering the following options:
Override isNew, which is the Backbone method that simply checks if an ID is present.
Override sync.
Determine if the concept of cid would somehow solve the problem.
One solution is to address the symptoms rather than the cause. Consider adding to your model a new create method:
var FooModel = Backbone.Model.extend({
urlRoot: '/api/foo',
create: function () {
return this.save(null, {
type: 'post', // make it a POST rather than PUT
url: this.urlRoot // send the request to /api/foo rather than /api/foo/:id
});
}
});
This is the solution I use, but I don't consider it ideal because the view logic/caller now needs to call create rather than save when creating (which is rather easy to do). This extended API bothers me for my use-case (despite working and being rather small), but perhaps it'll work for yours.
I'd love to see some additional answers to this question.
So I went down the path of trying to change up isNew.
I came up with new criteria that would answer whether a model is new:
Was the model created via a fetch from a collection? Then it's definitely not new.
Was the model created with an ID attribute? This is a choice I made for my case, see disadvantages below for the effect of doing this, but I wanted to make new Model({ id: 1, name: 'bob' }) not be considered new, while setting the ID later on (new Model({ name:
bob'}).set('id', 1)) would be.
Was the model ever synced? If the model was successfully synced at any point, it's definitely not new because the server knows about it.
Here's what this looks like:
var UserDefinedIDModel = Backbone.Model.extend({
// Properties
_wasCreatedWithID: false,
_wasConstructedByFetch: false,
_wasSynced: false,
// Backbone Overrides
idAttribute: 'some_id',
urlRoot: '/api/foo',
constructor: function (obj, options) {
this._wasCreatedWithID = !!obj[this.idAttribute];
this._wasConstructedByFetch = options && options.xhr && options.xhr.status === 200;
// Preserve default constructor
return Backbone.Model.prototype.constructor.apply(this, arguments);
},
initialize: function () {
this.on('sync', this.onSync.bind(this));
},
isNew: function () {
// We definitely know it's not new
if (this._wasSynced || this._wasConstructedByFetch) return false;
// It might be new based on this. Take your pick as to whether its new or not.
return !this._wasCreatedWithID;
},
// Backbone Events
onSync: function () {
this._wasSynced = true;
}
});
Advantages over the other answers
No logic outside of the backbone model for handling this odd usecase.
No server-side changes to support this
No new pseudo properties
Disadvantages
This is a lot of code when you could just create a new create method as per my other answer.
Currently myCollection.create({ some_id: 'something' }); issues a PUT. I think if you need support for this you'll have to do myCollection.create({ some_id: 'something' }, { url: '/api/foo', type: 'post' }); You can remove the _wasCreatedWithoutID check to fix this, but then any construction of a new model that derives its data from an existing one will be treated as new (in my case, this is undesirable).
Here's another solution :
In your model define an idAttribute that don't exists in your server model/table/... and that wouldn't be displayed to the DOM.
So let's suppose that the JSON that you send to the server is as follows :
{
'id': 1,
'name': 'My name',
'description': 'a description'
}
Your model should look like :
var MyModel = Backbone.Model.extend({
idAttribute: 'fakeId'
});
Now, when you create a new model and try to save it to the server, no one would initialize the fakeId and it would be considered a new object (POST).
When you fetch your model from the server you have to set the fakeId in your model, and your server must duplicate the id in the fakeId like this your model will be considered as an existing (PUT)

Persisting & loading metadata in a backbone.js collection

I have a situation using backbone.js where I have a collection of models, and some additional information about the models. For example, imagine that I'm returning a list of amounts: they have a quantity associated with each model. Assume now that the unit for each of the amounts is always the same: say quarts. Then the json object I get back from my service might be something like:
{
dataPoints: [
{quantity: 5 },
{quantity: 10 },
...
],
unit : quarts
}
Now backbone collections have no real mechanism for natively associating this meta-data with the collection, but it was suggested to me in this question: Setting attributes on a collection - backbone js that I can extend the collection with a .meta(property, [value]) style function - which is a great solution. However, naturally it follows that we'd like to be able to cleanly retrieve this data from a json response like the one we have above.
Backbone.js gives us the parse(response) function, which allows us to specify where to extract the collection's list of models from in combination with the url attribute. There is no way that I'm aware of, however, to make a more intelligent function without overloading fetch() which would remove the partial functionality that is already available.
My question is this: is there a better option than overloading fetch() (and trying it to call it's superclass implementation) to achieve what I want to achieve?
Thanks
Personally, I would wrap the Collection inside another Model, and then override parse, like so:
var DataPointsCollection = Backbone.Collection.extend({ /* etc etc */ });
var CollectionContainer = Backbone.Model.extend({
defaults: {
dataPoints: new DataPointsCollection(),
unit: "quarts"
},
parse: function(obj) {
// update the inner collection
this.get("dataPoints").refresh(obj.dataPoints);
// this mightn't be necessary
delete obj.dataPoints;
return obj;
}
});
The Collection.refresh() call updates the model with new values. Passing in a custom meta value to the Collection as previously suggested might stop you from being able to bind to those meta values.
This meta data does not belong on the collection. It belongs in the name or some other descriptor of the code. Your code should declaratively know that the collection it has is only full of quartz elements. It already does since the url points to quartz elements.
var quartzCollection = new FooCollection();
quartzCollection.url = quartzurl;
quartzCollection.fetch();
If you really need to get this data why don't you just call
_.uniq(quartzCollecion.pluck("unit"))[0];

Categories