Change js object to knockout js view model with nested observable arrays - javascript

I get a specific JSON from a server and want to be able to add/edit/delete items in nested arrays (variant lists, variants and columns) but I can't figure out how to do that with knockout.js.
I know that I need to change the properties in that JSON object to observables and I do it with the mapping plugin like shown under "Binding" and all values has been bind correctly - but if I change a value in an input field, the model/view is not updated automatically.
Why? Am I missing something?
So are nested arrays supported native by knockout.js without the need to write own code? How can I get this JSON to a full working knockout.js view model?
I use the current available versions (knockout: v2.1.0, mapping plugin: v2.3.2).
JSON
{
"VariantList": [
{
"ColumnCount": 1,
"Variants": [
{
"Title": "One column 100%",
"Columns": [
"100 %"
]
}
]
},
{
"ColumnCount": 2,
"Variants": [
{
"Title": "Two columns 50%/50%",
"Columns": [
"50%",
"50%"
]
},
{
"Title": "Two columns 75%/25%",
"Columns": [
"75%",
"25%"
]
}
]
}
]
}
HTML
<div data-bind="foreach: VariantList">
<h2 data-bind="text: ColumnCount"></h2>
<div data-bind="foreach: Variants">
<h3 data-bind="text: Title"></h3>
<table style="width:500px">
<tr>
<!-- ko foreach: Columns -->
<th><input data-bind="value: $data"/></th>
<!-- /ko -->
</tr>
<tr>
<!-- ko foreach: Columns -->
<td data-bind="style: {width:$data}, text:$data"></td>
<!-- /ko -->
</tr>
</table>
</div>
</div>
Binding
var viewModel;
$(function(){
viewModel = ko.mapping.fromJS(myJson);
ko.applyBindings(viewModel);
});

The issue is that the mapping plugin does not turn primitive values in an array into observables, by default. Even it it did, by the time that you bind $data against your input, you would have the unwrapped value of it rather than the observable.
The easiest way to make this work is to structure your data something like:
var data = {
"VariantList": [
{
"ColumnCount": 1,
"Variants": [
{
"Title": "One column 100%",
"Columns": [
{ value: "100 %" }
]
}
]
},
{
"ColumnCount": 2,
"Variants": [
{
"Title": "Two columns 50%/50%",
"Columns": [
{ value: "50%" },
{ value: "50%" }
]
},
{
"Title": "Two columns 75%/25%",
"Columns": [
{ value: "75%" },
{ value: "25%" }
]
}
]
}
]
};
Then you would bind against value in your loop on Columns. Here is a sample: http://jsfiddle.net/rniemeyer/MCnMX/
If you are not able to pull your data in this structure, then you can consider using the mapping options, to turn it into a structure like this. Here is a sample: http://jsfiddle.net/rniemeyer/sH3r2/
var mappingOptions = {
Columns: {
create: function(options) {
return { value: ko.observable(options.data) };
}
}
};
var viewModel = ko.mapping.fromJS(data, mappingOptions);
If you need to send your JSON back to the server in the same format that you received it, then there are a few options. I kind of like to do it like this: http://www.knockmeout.net/2011/04/controlling-how-object-is-converted-to.html. Here is a sample with your data: http://jsfiddle.net/rniemeyer/Eed2R/
var Value = function(val) {
this.value = ko.observable(val);
};
Value.prototype.toJSON = function() {
return ko.utils.unwrapObservable(this.value);
};
var mappingOptions = {
Columns: {
create: function(options) {
return new Value(options.data);
}
}
};
var viewModel = ko.mapping.fromJS(data, mappingOptions);

Related

How can i grab an element from a row on a datatable?

I have a simple datatable that shows some JSON data, received from an API endpoint.
I added a column that will hold a button on each row of the table. This button, when hit, will fire an AJAX request with the value of id for that specific row.
This actual code works, but now, instead of only sending the value of id, i would also like to edit the table so that, when the button is hit, it will send the values of id and item for that row. Can someone give me some piece of advice on how to do that?
On another question, i've been told to use Data Attributes, but i don't really know how would i integrate this into my current code. Any advice is appreciated.
$(document).ready(function() {
$(document).on('click', '.btnClick', function() {
var statusVal = $(this).data("status");
console.log(statusVal)
callAJAX("/request_handler", {
"X-CSRFToken": getCookie("csrftoken")
}, parameters = {
'orderid': statusVal
}, 'post', function(data) {
console.log(data)
}, null, null);
return false;
});
let orderstable = $('#mytalbe').DataTable({
"ajax": "/myview",
"dataType": 'json',
"dataSrc": '',
"columns": [{
"data": "item"
}, {
"data": "price"
}, {
"data": "id"
},],
"columnDefs": [{
"targets": [2],
"searchable": false,
"orderable": false,
"render": function(data, type, full) {
return '<button type="button" class="btnClick sellbtn" data-status="replace">Submit</button>'.replace("replace", data);
}
}]
});
});
You could use the full parameter of the DataTables render function to store the values of the current seleceted row. In this way:
return '<button type="button" class="btnClick sellbtn" data-status="' + btoa(JSON.stringify(full)) + '">Submit</button>';
In the above code, the data-status data attribute will contains the stringified version of the current object value in base64 by using btoa(). In base64 because for some reason we cannot directly store the stringified version of the object in the button's data attribute.
Then, in the button's click event, you have to do:
Decode the stringified object by using atob().
Parse into object by using JSON.parse().
Something like this:
$(document).on('click', '.btnClick', function() {
var statusVal = $(this).data("status");
// Decode the stringified object.
statusVal = atob(statusVal);
// Parse into object.
statusVal = JSON.parse(statusVal);
// This object contains the data of the selected row through the button.
console.log(statusVal);
return false;
});
Then, when you click in the button you will see this:
So, now you can use this object to send in your callAJAX() function.
See in this example:
$(function() {
$(document).on('click', '.btnClick', function() {
var statusVal = $(this).data("status");
// Decode the stringified object.
statusVal = atob(statusVal);
// Parse into object.
statusVal = JSON.parse(statusVal);
// This object contains the data of the selected row through the button.
console.log(statusVal);
return false;
});
let dataSet = [{
"id": 1,
"item": "Item 1",
"price": 223.22
},
{
"id": 2,
"item": "Item 2",
"price": 243.22
},
{
"id": 3,
"item": "Item 3",
"price": 143.43
},
];
let orderstable = $('#myTable').DataTable({
"data": dataSet,
"columns": [{
"data": "item"
}, {
"data": "price"
}, {
"data": "id"
}, ],
"columnDefs": [{
"targets": [2],
"searchable": false,
"orderable": false,
"render": function(data, type, full) {
// Encode the stringified object into base64.
return '<button type="button" class="btnClick sellbtn" data-status="' + btoa(JSON.stringify(full)) + '">Submit</button>';
}
}]
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="//cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js"></script>
<link href="//cdn.datatables.net/1.10.20/css/jquery.dataTables.min.css" rel="stylesheet" />
<table id="myTable" class="display" width="100%"></table>
Hope this helps!

Angular call function in ng-click defined in object

Got a question about Angularjs and it's ng-repeat/ng-click workings.
So on this system that I'm working on there is a lot of code re-used for datatables, and I'm trying to make a generic template/service to remedy this. Now I'm running into a problem where we have multiple buttons with their own function calls on being clicked.
I've so far got this setup:
My column object defined as so:
var columns = [
{
identifier: "id",
type: "text"
},
{
identifier: "type",
type: "text"
},
{
identifier: "label",
type: "text"
},
{
identifier: "actions",
type: "button",
multi: true,
content: [
{
icon: "fa-globe",
events: {
click: $scope.openMapModal
}
},
{
icon: "fa-list",
events: {
click: $scope.openGroupModal
}
}
]
}
];
And my HTML as following:
<tr ng-repeat="row in table.data" ng-model-instant>
<td ng-repeat="column in table.columns" ng-if="column.type === 'text'">
{{TableService.getByString(row, column.identifier)}}
</td>
<td ng-repeat="column in table.columns" ng-if="column.type === 'button' && column.multi">
<a ng-repeat="button in column.content" class="btn fa {{button.icon}}" ng-click="button.events.click(row)"></a>
</td>
</tr>
Just for completeness, my TableService.getByString and a small table data set:
(note that columns defined above is set by a function into the table object, and I did not include it in the object).
var table = {
data: [
{
id: 0,
label: "foo",
type: "bar"
},
{
id: 1,
label: "one",
type: "bar"
},
{
id: 2,
label: "foo",
type: "two"
}
]
}
function getKeyObj(obj, key) {
var retVal = {
"key": key,
"obj": obj
};
if (retVal.key.indexOf('.') > -1) {
var keyParts = retVal.key.split('.');
retVal.key = keyParts.pop();
while (keyParts.length && (obj = obj[keyParts.shift()])) ;
retVal.obj = obj;
}
return retVal;
}
function getByString(obj, key) {
var ret = getKeyObj(obj, key);
return ret.obj[ret.key];
}
Now the problem that I'm encountering is that my functions are not being called in my ng-click's whenever I click on the buttons.
I've also tried it with settings the function as a string in my column object, but it didn't work either.
Am I going in the right direction with this or do I need to rethink my generalization, and ifso, what alternative is there?
Are you sure $scope.openMapModal is defined at the time you define the columns array? Could it be that you are assigning undefined to the click property instead of a reference to the function?

How to make dojo treeGrid categorized by two columns?

I have a simple dojo treeGrid that is categorized just by first column. But how to make it categorized/collapsible by second as well? Note the treeGrid has totals shown in each category. Also, is there a way to move totals to the category level but not to the bottom?
var layout = [
{ cells: [
[ {field: "year", name: "Year"},
{field: "childItems",
children: [ { field: "unid", name: "unid", hidden: true},
{ field: "geography", name: "Geography"},
{ field: "country", name: "Coungtry"},
{ field: "status", name: "Status"},
{ field: "credit", name: "Credit"},
{ field: "debit", name: "Debit"}
],
aggregate: "sum"
}
]] } ]
var jsonStore = new dojo.data.ItemFileWriteStore({ url: <...............>});
var grid = new dojox.grid.TreeGrid({
structure: layout,
store: jsonStore,
query: {type: 'year'},
queryOptions: {deep: true},
rowSelector: true,
openAtLevels: [false],
autoWidth: true,
autoHeight: true
},
dojo.byId("treeGrid"));
grid.startup();
dojo.connect(window, "onresize", grid, "resize");
sample JSON store:
{
"identifier": "id",
"label": "name",
"items": [
{
"id": "2018",
"type": "year",
"year": "2018",
"childItems": [
{
"id": "id0",
"geography": "Asia Pacific",
"country": "Australia",
"programname": "Program 1",
"totalPlanned": 0,
"totalForecasted": 0
},
{
.....
}
]
},
{
.....
}
]
}
You can find completely working example over here:
Now, let me try to explain it:
Data
First of all to support multiple levels in the grid you must have your data in the same format. For tree with n levels, you need to have n-1 level grouping in your data itself.
For example, JSON object below have 2 levels of grouping (year, geography) to support tree with 3 levels (root, parent, and child).
{
"identifier":"id",
"label":"name",
"items":[
{
"id":"2018",
"type":"year",
"year":"2018",
"geography":[
{
"id":"id1",
"geography":"Asia Pacific",
"childItems":[
{
"id":"ci1",
"country":"Australia",
"programname":"Program 1",
"credit":100,
"debit":50
}
]
}
]
}
]
}
Layout
To render a tree with n-levels you have to make sure layout of the tree is properly configured with same nesting as your data. To support data structure from JSON object above you need to set layout to:
[
{
cells:
[
[
{ field:"year", name:"Year" },
{
field:"geography",
children:
[
{ field:"geography", name:"Geography" },
{
field:"childItems",
children:[
{ field:"unid", name:"unid", hidden:true },
{ field:"country", name:"Country" },
{ field:"programname", name:"Program" },
{ field:"credit", name:"Credit" },
{ field:"debit", name:"Debit" }
],
aggregate:"sum",
},
]
}
]
]
}
]
You can see that, for each child level(s) you have to add a group (as I would like to call it) field and set first field within that group to your actual group field.
I hope this example will clear your doubt.
PS: In the jsfiddle version I have used formatters just to hide aggregate values for string fields.

Looping through the json service response in Angular JS

I am working on a angular project. i have a scenario where i need to list some details in a page of the application.I have a service call in the page which returns the following json structure. i want to loop through this json structure to list few of the data in the response.
[
{
"ProductDetails": [
{
"ProductType": "Application1",
"Name": "Product1",
"New": false,
"Category": "product",
"Country": "India",
"description": "Description for Product1",
"Favourite": false,
"settings": {
"WebsiteFlag": true,
"SmsFlag": false,
"EmailFlag": true
}
}
]
},
{
"ProductDetails": [
{
"ProductType": "Application2",
"Name": "Product2",
"New": true,
"Category": "product",
"Country": "India",
"description": "Description for Product2",
"Favourite": true,
"settings": {
"WebsiteFlag": false,
"SmsFlag": false,
"EmailFlag": true
}
}
]
}
]
JS
$ctrl.getSettings = function () {
var url = "http://localhost:3000/json/settings-updated.json";
rsicontext.getData(url).then(function (response) {
$ctrl.Settings = response.data;
});
}
HTML
<tbody>
<tr data-ng-repeat="app in $ctrl.Settings" class="content-box">
<td data-ng-bind="app.ProductDetails.ProductType"></td>
<td data-ng-bind="app.ProductDetails.Name"></td>
<td><ng-checkbox data-checked="app.SmsFlag" rounded="true"></ng-checkbox></td>
<td><ng-checkbox data-checked="app.EmailFlag" rounded="true"></ng-checkbox></td>
</tr>
</tbody>
I am trying to list the Product Type, Name, EmailFlag and SmsFlag. How can i loop through the json structure to list the data.
Simply go over the response and build a new array from the fields you need. Like so:
var d = [];
for( vari=0;i< response.data.ProductDetails.length; ++i) {
var curr= {
ProductType: response.data.ProductDetails[i].ProductType,
Name: response.data.ProductDetails[i].Name
}
d.push(curr);
}
$ctrl.Settings = d;
To manipulate object or array I always use a library called underscore.js
This can help you to do what you want.
var plucked=_.pluck($ctrl.Settings, 'ProductDetails');
This function will return an array of object. Then you can loop it.
https://jsfiddle.net/wz2njukj/
You can achieve this throw ng-repeat
<div ng-repeat="d in data[0].ProductDetails[0]">
{{ d.SmsFlag }} {{ d.WebsiteFlag}} {{d.EmailFlag}}
</div>
Look this and get data as you want.
PlunkerHere
ProductDetails contains an array, so you would have to nest ngRepeats
<span data-ng-repeat="detail in app.ProductDetails">
If you know that ProductDetails will only have one element it would be best to change the structure that is being generated if you can. If not you can access it in markup
<tbody>
<tr data-ng-repeat="app in $ctrl.Settings" class="content-box">
<td data-ng-bind="app.ProductDetails[0].ProductType"></td>
<td data-ng-bind="app.ProductDetails[0].Name"></td>
<td><ng-checkbox data-checked="app.ProductDetails[0].settings.SmsFlag" rounded="true"></ng-checkbox></td>
<td><ng-checkbox data-checked="app.ProductDetails[0].settings.EmailFlag" rounded="true"></ng-checkbox></td>
</tr>
</tbody>
Or you can massage the data in your controller before passing it off to the view.
$ctrl.Settings = response.data.map(products=>products.ProductDetails[0])

Kendo excel export - how do I export columns with a custom template?

I have a kendo grid that I define declaritively.
I enable the excel export toolbar via data-toolbar='["excel"]'
The problem is that this only exports the fields that do not have a template defined. (the first 3 in the grid below: Checkpoint, Location, Patrolled By), the other columns show up in the excel document, but the cells of those columns are all empty.
How can I get the values to show up in the excel export? I'm guessing it will require pre-processing of some sort before the excel gets exported, as the excel export function doesn't interpret my custom field html templates.
<div id="Checkpoints">
<div
...
data-toolbar='["excel"]'
data-excel='{ "fileName": "CheckpointExceptionExport.xlsx", "allPages": "true" }'
...
data-columns='[
{
"field": "checkpoint_name",
"title": "Checkpoint",
"filterable": { "cell": { "operator": "contains"}}},
{
"field": "location_name",
"title": "Location",
"filterable": { "cell": { "operator": "contains"}}
},
{
"field": "patrolled_by",
"title": "Patrolled By",
"filterable": { "cell": { "operator": "contains"}}
},
{
"field": "geotag",
"title": "GeoTag",
"template": kendo.template($("#geotagTemplate").html())
},
{
"field": "geofence",
"title": "GeoFence",
"template": kendo.template($("#geofenceTemplate").html())
},
{
"field": "completed",
"title": "Completed",
"template": kendo.template($("#completedTemplate").html())
},
{
"field": "gps",
"title": "GPS",
"template": kendo.template($("#gpsTemplate").html())
}
]'>
</div>
</div>
I've came across this snippet for handling the excel export event however I don't see a way to use this event handler in the way that I've defined the grid.
<script>
$("#grid").kendoGrid({
excelExport: function(e) {
...
},
});
</script>
Check http://docs.telerik.com/kendo-ui/controls/data-management/grid/excel-export#limitations, which explains why this happens and shows how to proceed.
The Grid does not use column templates during the Excel export—it exports only the data. The reason for this behavior is that a column template might contain arbitrary HTML which cannot be converted to Excel column values. For more information on how to use a column template that does not contain HTML, refer to this column template example.
Update
In order to attach a Kendo UI event handler when using declarative widget initialization, use the data-bind HTML attribute and event binding:
<div
data-role="grid"
data-bind="events: { excelExport: yourExcelExportHandler }">
</div>
Check the Kendo UI Grid MVVM demo for a similar example.
yourExcelExportHandler should be a function defined in the viewModel, similar to onSave in the above example.
The excelExport event can also be attached after widget initialization.
I found this great answer by Telerik on their website: https://docs.telerik.com/kendo-ui/knowledge-base/grid-export-arbitrary-column-templates. Their helper function exports to excel with the exact template text.
$(document).ready(function() {
$("#grid").kendoGrid({
dataSource: {
type: "odata",
transport: {
read: "https://demos.telerik.com/kendo-ui/service/Northwind.svc/Orders"
},
schema: {
model: {
fields: {
OrderDate: {
type: "date"
}
}
}
},
pageSize: 20,
serverPaging: true
},
height: 550,
toolbar: ["excel"],
excel: {
allPages: true
},
excelExport: exportGridWithTemplatesContent,
pageable: true,
columns: [{
field: "Freight",
hidden: true
},
{
field: "OrderID",
filterable: false
},
{
field: "OrderDate",
title: "Order Date",
template: "<em>#:kendo.toString(OrderDate, 'd')#</em>"
}, {
field: "ShipName",
title: "Ship Name",
template: "#:ShipName.toUpperCase()#"
}, {
field: "ShipCity",
title: "Ship City",
template: "<span style='color: green'>#:ShipCity#, #:ShipCountry#</span>"
}
],
columnMenu: true
});
});
function exportGridWithTemplatesContent(e) {
var data = e.data;
var gridColumns = e.sender.columns;
var sheet = e.workbook.sheets[0];
var visibleGridColumns = [];
var columnTemplates = [];
var dataItem;
// Create element to generate templates in.
var elem = document.createElement('div');
// Get a list of visible columns
for (var i = 0; i < gridColumns.length; i++) {
if (!gridColumns[i].hidden) {
visibleGridColumns.push(gridColumns[i]);
}
}
// Create a collection of the column templates, together with the current column index
for (var i = 0; i < visibleGridColumns.length; i++) {
if (visibleGridColumns[i].template) {
columnTemplates.push({
cellIndex: i,
template: kendo.template(visibleGridColumns[i].template)
});
}
}
// Traverse all exported rows.
for (var i = 1; i < sheet.rows.length; i++) {
var row = sheet.rows[i];
// Traverse the column templates and apply them for each row at the stored column position.
// Get the data item corresponding to the current row.
var dataItem = data[i - 1];
for (var j = 0; j < columnTemplates.length; j++) {
var columnTemplate = columnTemplates[j];
// Generate the template content for the current cell.
elem.innerHTML = columnTemplate.template(dataItem);
if (row.cells[columnTemplate.cellIndex] != undefined)
// Output the text content of the templated cell into the exported cell.
row.cells[columnTemplate.cellIndex].value = elem.textContent || elem.innerText || "";
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://kendo.cdn.telerik.com/2022.1.119/js/kendo.all.min.js"></script>
<script src="https://kendo.cdn.telerik.com/2022.1.119/js/jszip.min.js"></script>
<link href="https://kendo.cdn.telerik.com/2022.1.119/styles/kendo.default-v2.min.css" rel="stylesheet" />
<div id="grid"></div>

Categories