Binding to nested array values in KendoUI TreeView - mvvm

We are evaluating KendoUI web for use in all of our projects and I am running into an issue (or lack of understanding) with the TreeView Widget.
I am trying to use a MVVM TreeView and I am having issues when binding to nested array values from my template. I am dynamically building the tree view, by adding a node and its children with a button click. I want to bind each leaf(child) node to a text value on that node in the MVVM. This seems to work great on the initial load of the tree, but after adding more nodes to the viewModel, the bindings seem to quit working.
This seems to be a lot harder than I feel it should, which makes me think I am missing something.
Can anyone help shed some light on this?
Thanks,
Jason
JSFiddle
I created a JSFiddle example for our use in this discussion: http://jsfiddle.net/jsonsee/9B9pT/
HTML
<script id="treeTemplate" type="text/x-kendo-template">
<span>
#: item.name #
# if (!item.hasChilren) { #
<input type = 'text' data-bind = "value: treeData[#: item.parentIndex #].items[#: item.childIndex #].displayValue"/>
# } #
</span>
</script>
<div id="content">
<div id="form">
<label>Item Name:
<input type="text" data-bind="value: form.itemName" />
</label>
<button data-bind="click: form.onAddItem">Add Item</button>
<div class="console">
<h2>Output:</h2>
<div id="myConsole"/>
</div>
</div>
<h2>TreeView</h2>
<div data-template="treeTemplate" data-role="treeview" data-bind="source: treeData" />
</div>
Javascript
(function ($) {
var viewModel = kendo.observable({
itemIndex: 0,
form: {
itemName: '',
onAddItem: function (e) {
e.preventDefault();
var itemToAdd = {
name: viewModel.get('form.itemName'),
hasChilren: true,
items: (function () {
var children = [];
for (var i = 0; i < 3; i++) {
children.push({
name: viewModel.get('form.itemName') + '-child-' + i,
parentIndex: viewModel.get('itemIndex'),
childIndex: i,
displayValue: '',
hasChilren: false
});
}
return children;
})()
}
if (viewModel.get('treeData')) {
viewModel.get('treeData').push(itemToAdd);
} else {
viewModel.set('treeData', kendo.observableHierarchy([itemToAdd]));
}
viewModel.set('form.itemName', '');
viewModel.set('itemIndex', viewModel.get('itemIndex') + 1);
}
}
});
$(document).ready(function () {
kendo.bind(document.body, viewModel);
viewModel.bind('change', function (e) {
console.log('viewModel changed!');
var output = JSON.stringify(viewModel, undefined, 2);
console.log('----------------------------------------------------');
console.log(output);
console.log('----------------------------------------------------');
$("#myConsole").html("<pre>" + output + "</pre>");
});
});
})(jQuery);
Edit
Addressing a comment:
In my application, I am dragging elements from a tool palette on the left hand side of the page to a center pane. On dropping the item, I build a tree view. Each node has a set of children, those children do not have children. So the tree is one level deep (two if you count the parent).
Each node in the tree view is rendered as such (Starting with the parent)
* parentNode.name
--->* childNode.name [textbox bound to this node's displayValue property in viewModel]
--->* childNode.name [textbox bound to this node's displayValue property in viewModel]
--->* childNode.name [textbox bound to this node's displayValue property in viewModel]
--->* childNode.name [textbox bound to this node's displayValue property in viewModel]
Problem
The part I have a problem with is binding the child nodes text box to the displayValue property of the corresponding viewModel field in the hierarchical dataSource. I have tried many different methods, but the one that "sort-of" works is accessing the array elements directly from the template binding via: data-bind="value: treeData[#: item.parentIndex #].items[#: item.childIndex #].displayValue"
Note that the above binding doesn't work after I add the second node. That is the problem. I agree that the code is complex, that is part of the reason for the question... something seems wrong to me. So, either the kendo MVVM doesn't support direct binding to nested array elements, or I am doing something completely wrong.
Updated Output
I updated the JSFiddle and am posting the output to better illustrate the problem. Notice how the second set of child elements on the viewModel has a blank displayValue property, even after I changed them in the view. The bindings aren't working for some reason after I add the first node.
New JSFiddle: http://jsfiddle.net/jsonsee/9B9pT/
Printout of viewModel
{
"itemIndex": 2,
"form": {
"itemName": ""
},
"treeData": [
{
"name": "a",
"hasChilren": true,
"items": [
{
"name": "a-child-0",
"parentIndex": 0,
"childIndex": 0,
"displayValue": "1",
"hasChilren": false,
"items": [],
"index": 0
},
{
"name": "a-child-1",
"parentIndex": 0,
"childIndex": 1,
"displayValue": "2",
"hasChilren": false,
"items": [],
"index": 1
},
{
"name": "a-child-2",
"parentIndex": 0,
"childIndex": 2,
"displayValue": "3",
"hasChilren": false,
"items": [],
"index": 2
}
],
"index": 0,
"expanded": true
},
{
"name": "b",
"hasChilren": true,
"items": [
{
"name": "b-child-0",
"parentIndex": 1,
"childIndex": 0,
"displayValue": "",
"hasChilren": false,
"index": 0
},
{
"name": "b-child-1",
"parentIndex": 1,
"childIndex": 1,
"displayValue": "",
"hasChilren": false,
"index": 1,
"selected": true
},
{
"name": "b-child-2",
"parentIndex": 1,
"childIndex": 2,
"displayValue": "",
"hasChilren": false,
"index": 2
}
],
"index": 1,
"expanded": true
}
]
}

Related

How to i trigger an update in Tabulator when using a select editor

I'm trying to get the row ID of a select editor in Tabulator.
I have a few hundred rows of values, some i want to have a value of yes, some of no. When I change the value I then need to know which row it is for.
Here is my table.
adtable = new Tabulator("#adtable", {
ajaxURL: "getinfo.php",
layout: "fitColumns",
pagination:"local",
placeholder: "No Data Set available",
paginationSize: 40,
paginationSizeSelector: [100, 200, 500, 1000],
columns: [{
title: "Key Name",
field: "keyname",
formatter: "textarea",
sorter: "string",
headerFilter: "input"
},
{
title: "Active",
field: "display",
editor: "select",
editorParams: {
values: ["Yes", "No"],
sortValuesList: "asc",
defaultValue: "Yes",
elementAttributes: {
maxlength: "10",
},
},
],
});
});
Where can I put a command which will give me the row number.?
I need to update a db record. basiclaly, set display = either yes or no.
My preference was a tickCross but couldnt work that out either.
Any assistance is greatly appreciated.
using Tabulator 4.9
After a bit more thinking... it was actually blindingly simple.
{
title: "Active",
field: "display",
editor: "select",
editorParams: {
values: ["Yes", "No"], //create list of values from all values contained in this column
sortValuesList: "asc", //if creating a list of values from values:true then choose how it should be sorted
defaultValue: "Yes", //set the value that should be selected by default if the cells value is undefined
elementAttributes: {
maxlength: "10", //set the maximum character length of the input element to 10 characters
},
},
cellEdited: function(row) {
let thisrow = row.getData()
alert(thisrow.id + "is now " + thisrow.display)
}
},
Added here to help others who might be banging their head against a wall. I was obviously thinking of something more complicated.

How can I implement a linear regression line in a Vizframe Chart?

I have a scatter plot Vizframe chart in which I need to include a linear regression/trend line. Any idea how this can be done? It appears this is not something offered by vizframe 'out of box'? I can't find a solution for this!
Question:
Any suggestions on a feasible way to implement a regression line on a Scatter Plot Vizframe chart?
Here is the code I have for the setup. The scatter plot opens in a dialog/modal when a button is pressed.
sap.ui.define([
'jquery.sap.global',
'vizConcept/controller/BaseController',
'sap/ui/model/json/JSONModel',
'vizConcept/model/viewControls',
'sap/m/Button',
'sap/m/Dialog',
],
function (jQuery, BaseController, JSONModel, viewControls, Button, Dialog) {
"use strict";
var controls;
var mainController = BaseController.extend("vizConcept.controller.Main", {
onInit: function(oEvent) {
// Access/expose the defined model(s) configured in the Component.js or Manifest.json within the controller.
this.getView().setModel(this.getOwnerComponent().getModel("products"), "products");
var oModel = this.getView().getModel("products");
this.getView().setModel(oModel);
var sUrl = "#" + this.getOwnerComponent().getRouter().getURL("page2");
$(function() {
var dataset = new sap.viz.ui5.data.FlattenedDataset({
dimensions : [
{
axis : 1,
name : 'Award Date',
value : "{AwdDate}"
}
],
measures : [
{
group: 1,
name : 'Award Date',
value : '{Hist}'
},
{
group: 2,
name : 'Current PPI',
value : '{Current}'
}
],
data : {
path : "/ProductCollection"
}
});
var scatterViz = new sap.viz.ui5.Scatter({
id : "idscatter",
width : "1000px",
height : "400px",
title : {
text : 'Pricing Tool Scatter Plot Example'
},
xAxis : {
title : {
visible : true
}
},
yAxis : {
title : {
visible : true
}
},
dataset : dataset
});
scatterViz.setModel(sap.ui.getCore().getModel());
scatterViz.setModel(oModel);
var dlg = new sap.m.Dialog({
id: 'vizModal',
title: 'Scatter Plot Example Viz',
width : "1800px",
height : "600px",
content : [scatterViz],
beginButton: new Button({
text: 'Close',
press: function () {
dlg.close();
}
})
});
(new sap.m.Button({
text: 'open',
type: 'Accept',
press: function() {
dlg.open();
scatterViz.invalidate();
}
})).placeAt('content');
});
},
onToPage2 : function () {
this.getOwnerComponent().getRouter().navTo("page2");
},
});
return mainController;
});
Edit
Here is the 'products' model that is outputted on the vizframe chart. I have the products model defined in the manifest.json but the connection there is fine:
products model
{
"ProductCollection": [
{
"Item": "1",
"AwdDate": "20160715",
"Hist": 171.9,
"Current": 183
},
{
"Item": "2",
"AwdDate": "20160701",
"Hist" : 144.3,
"Current": 158.6
},
{
"Item": "3",
"AwdDate": "20150701",
"Hist": 160,
"Current": 165
},
{
"Item": "1",
"AwdDate": "20160715",
"Hist": 201,
"Current": 167
},
{
"Item": "2",
"AwdDate": "20160801",
"Hist" : 175.3,
"Current": 178.2
},
{
"Item": "3",
"AwdDate": "20150721",
"Hist": 160,
"Current": 147
},
{
"Item": "1",
"AwdDate": "20160715",
"Hist": 175.9,
"Current": 185.2
},
{
"Item": "2",
"AwdDate": "20161101",
"Hist" : 165.3,
"Current": 158.2
},
{
"Item": "3",
"AwdDate": "201700101",
"Hist": 160,
"Current": 165
},
{
"Item": "4",
"AwdDate": "201600401",
"Hist": 173,
"Current": 177
}
]
};
Edit 2
Here is my attempt at the solution offered here. But nothing appears after this is included in the onInit() function of the controller.
var regressionData = [];
for (var i = 0; i < 10; i++) {
regressionData[i] = [oData.ProductCollection[i].Current, oData.ProductCollection[i].Hist];
};
regression('linear', regressionData);
Unfortunately, you have some limitations inherent to the viz charts. Namely:
You cannot add "oblique" reference lines (= the trend line) to the chart. You can only add vertical / horizontal ones via the Reference Line feature. Check out the (non-intuitive) documentation on this: Viz Charts Documentation (look at the plotArea.referenceLine.line; it is just a number = will be a horizontal / vertical line depending on your chart type or orientation).
You cannot combine a scatter plot with a line chart. If you look in the same Viz Charts Documentation, you will see that in the Combination "chapter" on the left hand side, there is no "Line - Scatter" combination.
Also, your X values (AwDate) are ABAP-style dates as string. It is more appropriate to use a date / time chart to correctly display them. Anyway, regression makes a lot more sense when you use dates. Otherwise your data is "categorical" and the only way you can make regression is by sorting them and considering them equidistant on the X axis - not what you would normally want if you have non-equidistant data (like the one in your example).
My proposal would be to:
Use a line chart instead of a scatter chart (such that you can also display the trend line).
Convert the ABAP-style strings into dates, such that you can use a timeseries chart. You have some "wrong" dates in your example data btw: "201600401".
Do the regression for whatever you want and add it as a separate "trend" series. You will need to compute a point on the "trend" series for each point on your other series (basically, you will have to add a "trend" attribute to each line in your ProductCollection). If you are using an OData model, then you will need to either switch to an JSON client model or do some ugly workarounds with formatters.
As requested, I made an example implementation here: https://jsfiddle.net/93mx0yvt/23/. The regression algorithm I borrowed from here: https://dracoblue.net/dev/linear-least-squares-in-javascript/. The main points of the code are:
// format for parsing the ABAP-style dates
var oFormat = DateFormat.getDateInstance({
pattern: "yyyyMMdd"
});
// maps an ProductCollection entry to a new object
// which has the parsed date (and only the needed attributes)
var fnMapData = function(oEntry) {
return {
date: oFormat.parse(oEntry.AwdDate),
current: oEntry.Current,
historical: oEntry.Hist
};
};
var fnProcessData = function(oD) {
var aEntries = oD.ProductCollection.map(fnMapData),
aXs = aEntries.map(function(oE) { // get the Xs
// we take the millis to be able to do arithmetics
return oE.date.getTime();
}),
aYs = aEntries.map(function(oE) { // get the Ys (hist)
return oE.historical;
}),
//changed the function to only return only result Ys
aRs = findLineByLeastSquares(aXs, aYs);
//save the Ys into the result
for (var i = 0; i < aEntries.length; ++i) {
aEntries[i].trend = aRs[i];
}
return {
data: aEntries
};
};
You can use then the data returned by the fnProcessData function inside a JSONModel and then build a simple multi-series date/time line chart based on it.

Meteor Collection 2 Re-Use Array Items From Same Collection for AutoForm

I have a collection with a few arrays. I am using autoform to display various forms.
I need to pull in the existing data from the same _id into a different form field.
Here is my document with data:
{
"_id": "ERjvecg2GmSBktmn5",
"clientName": "General Motors",
"clientInfo": [
{
"fullname": "John Smith",
"email": "jsmith#gmail.com",
"phone": 2485551212,
"phone2": 7345551212
},
{
"fullname": "Billy Bob",
"email": "bbob#gmail.com",
"phone": 3135551212,
"phone2": 7346661212
}
],
"facilityList": [
{
"name": "GM # Ann Arbor",
"id": "1234",
"facilityType": "option1",
"address": "12345 Main St Ann Arbor, MI 48888",
"contacts": [
{
"contactName": "Jill Sutton",
"contactEmail": "jsutton#gmail.com",
"contactPhone1": 7348881212,
"contactPhone2": 3132224444
}
],
"special": "gsgfsdgdsgdsgdgsdgsdgdgdgggdsgfdgdfgdgsdgsdg",
"_id": "accde038-775d-4d11-b8fd-d0cc3b63c869"
}
],
"TasksWithData": [
{
"inspectionDetails": [
{
"inspector": "cHsSP6jadYa8wAeTi",
"inspectorName": "Joe Reporter",
"projectType": "inspection",
"startDate": "2017-01-13T05:30:00.000Z",
"desc": "fgfsdgsdgsdgdsgsdgdggsdgdg",
"activeTask": false
}
],
"TaskItem": [
{
"DataFacilitySection": "dsfgsdfgds",
"DataItemDescription": "item 1",
"DataItemSection": "dfgdfgdf",
"DataItemCode": "dfgdfgdf",
"DataItemPass": null
},
{
"DataFacilitySection": "ftydftuydrtuy",
"DataItemDescription": "item 2",
"DataItemSection": "dfgdgdf",
"DataItemCode": "dfgdfgdf",
"DataItemPass": null
}
],
"author": "Ss6TjGGzqWJRZYStx"
}
]
}
Really what I need to do is get the "facilityList" objects to appear as options in a select.
I have managed to do this with data from other collections like so:
'TasksWithData.$.inspectionDetails.$.inspector': {
type: String,
label: "Inspector",
optional: true,
autoform: {
firstOption: 'Choose an Inspector',
options: function() {
return Meteor.users.find({}, {
sort: {
profile: 1,
firstName: 1
}
}).map( function ( c ){
return{
label: c.profile.firstName + " " + c.profile.lastName,
value: c._id
};
} );
}
}
},
but it seems a bit different when doing it with items from the same _id.
Here is my autoForm:
{{#autoForm collection="ClientData" id=UpdateForm type="update-pushArray" scope="TasksWithData" doc=this}}
{{> afQuickField name="inspectionDetails.0.TaskFacility"}}
{{> afQuickField name="inspectionDetails.0.inspector"}}
{{> afQuickField name="inspectionDetails.0.inspectorName"}}
{{> afQuickField name="inspectionDetails.0.projectType"}}
{{> afQuickField name="inspectionDetails.0.startDate"}}
{{> afQuickField name="inspectionDetails.0.desc"}}
<div class="panel panel-default">
{{> afArrayField name="TaskItem"}}
</div>
<input class="btn btn-primary btn-block" type="submit" value="Add Task">
{{/autoForm}}
I am a little unsure if this would get done on the autoform side or with the schema. Here is an example of the schema I am using to attempt to pull in the data:
'TasksWithData.$.inspectionDetails.$.TaskFacility': {
type: String,
label: "Choose a Facility",
optional: true,
autoform: {
firstOption: 'Choose a Facility',
options: facilityList
},
Clearly its wrong, but I would REALLY appreciate suggestions on this.

How to group items by month in ionic 2?

I have a data collection that i want to group by month for a calendar list view with dividers like the attached image. How to do this in ionic 2?
{
"data": [
{
"id": "75",
"title": "Oudergesprekken",
"startDate": "18-01-2017",
},
{
"id": "76",
"title": "Talentmiddag",te ontdekken.</p>",
"startDate": "25-01-2017",
},
{
"id": "77",
"title": "Studiedag team, alle kinderen vrij!",
"startDate": "06-02-2017",
},
{
"id": "79",
"title": "Letterfeest groep 3",
"startDate": "14-02-2017",
},
{
"id": "78",
"title": "Voorjaarsvakantie",
"startDate": "24-02-2017",
}
]
At the moment i have an filter pipe
#Pipe({
name: "filter",
pure: false
})
export class ArrayFilterPipe implements PipeTransform {
transform(items: Array<any>, conditions: {[field: string]: any}): Array<any> {
return items.filter(item => {
for (let field in conditions) {
if (item[field] !== conditions[field]) {
return false;
}
}
return true;
});
}
}
and in the template ill do this.
<ion-item-group *ngFor="let maand of maanden" >
<ion-item-divider sticky>{{maand.title}}</ion-item-divider>
<ion-item *ngFor="let item of agendaItems | filter:{ maandNum:maand.id }" >
{{ item.title }}
</ion-item>
</ion-item-group>
The pipe groups the items with an months array that's works fine but shows dividers of months with no have any data and if there are items that are in 2 different years in the same month this group by not working propaly.
I'd like to have usefull data when programming in front-end. There are a few ways to handle the data you recieved. In my opinion it's the easiest way to manipulate the data you've got, so you are able to iterate through that month array, like following :
Your component
export class YourComponent {
public months: any = [];
public data: any = [];
constructor() {
this.data = [
{
'id': '75',
'title': 'Oudergesprekken',
'startDate': '18-01-2017',
},
{
'id': '76',
'title': 'Talentmiddag,te ontdekken.</p>',
'startDate': '25-01-2017',
},
{
'id': '77',
'title': 'Studiedag team, alle kinderen vrij!',
'startDate': '06-02-2017',
},
{
'id': '79',
'title': 'Letterfeest groep 3',
'startDate': '14-02-2017',
},
{
'id': '78',
'title': 'Voorjaarsvakantie',
'startDate': '24-02-2017',
}
];
}
ngOnInit() {
this.data.forEach( (event) => {
let monthNumber = this.getMonthNumber(event);
if (this.months[monthNumber] === null||undefined ) this.months[monthNumber].events = [];
this.months[monthNumber].events.push(event);
});
}
private getMonthNumber(event: any): number {
return event.startDate.split('-')[1];
}
public getMonthName(monthNumber: any): number {
let maanden = [ {"id": 1, "title": "Januari"}, {"id": 2, "title": "Februari"}, {"id": 3, "title": "Maart"}, {"id": 4, "title": "April"}, {"id": 5, "title": "Mei"}, {"id": 6, "title": "Juni"}, {"id": 7, "title": "Juli"}, ...... ];
maanden.forEach((maand) => {
if ( maand.id === monthNumber) return maand.title;
});
}
}
Your HTML
<ion-item-group *ngFor="let month of months" >
<ion-item-divider sticky *ngIf='month.events.length > 0'>{{getMonthName()}}</ion-item-divider>
<ion-item *ngFor="let event of month.events | filter:{ maandNum:months.indexOf(month) }" >
{{ item.title }}
</ion-item>
</ion-item-group>
I did not test this, so I'll keep an eye on this one. How this make some clear.

Attributes are removed in HTML code

I use wysihtml5.
When you insert an image
<img alt src="src" media_img_id="123" data-title="title" data-author="author" />
The result is
<img alt src="src" />
Rules for img
"img": {
"remove": 0,
"check_attributes": {
"width": "numbers",
"alt": "alt",
"src": "url", // if you compiled master manually then change this from 'url' to 'src'
"height": "numbers",
"media_img_id": "numbers"
},
"add_class": {
"align": "align_img"
}
},
How to make the attributes generally not removed?
I have the same task today to extend abilities of this editor.
You should add your attributes in special object:
I'm using additionaly bootstrap3-wysihtml5 - https://github.com/schnawel007/bootstrap3-wysihtml5 . The object that should be added with new attributes for element:
var defaultOptions = $.fn.wysihtml5.defaultOptions = {
/../
"img": {
"check_attributes":
{
"width": "numbers",
"alt": "alt",
"data-encid": "alt", <<-here is my custom attribute
"src": "url",
"height": "numbers"
}
},
/../
}
and in wysihtml5.js you should add condition in which your src attribute is differs from classical source (that this plugin expected) "http://example.png".
line 4922:
if (checkAttributes) {
for (attributeName in checkAttributes) {
method = attributeCheckMethods[checkAttributes[attributeName]];
if (!method) {
continue;
}
newAttributeValue = method(_getAttribute(oldNode, attributeName));
if (typeof(newAttributeValue) === "string") {
attributes[attributeName] = newAttributeValue;
}
}
}
replace with:
if (checkAttributes) {
for (attributeName in checkAttributes) {
method = attributeCheckMethods[checkAttributes[attributeName]];
if (!method) {
continue;
}
newAttributeValue = (attributeName == "src" && checkAttributes["data-encid"])
? oldNode.src
: method(_getAttribute(oldNode, attributeName));
if (typeof(newAttributeValue) === "string") {
attributes[attributeName] = newAttributeValue;
}
}
}
Here I just copy the src attribute value without checking through wysihtml5.js core.
Hope this helps!