I am working in an MVC 5 / Knockout environment. My View is loaded initially from the ViewModel supplied by a standard MVC 5 Controller. I have a knockout grid as part of this view. The grid is correctly populated based on the ViewModel contents.
On a user button push, a Web API call is made and returns successfully. I want to use the data returned from the API call to update the Knockout observable array which is bound to the knockout grid. I can see the knockout observable array is correcty updated, but the knockout grid is never refreshed. The knockout observalbe array I write to with the results of the API call is the same array that gets written to when the View is first loaded.
This is teh code at the top of the .cshtml view:
<script>
$(function () {
var inputData = ko.mapping.toJS(#Html.Raw(HelperFunctions.ToJson(Model)));
var dom = document.getElementById("AWBSNode");
var obj = new AreasModel(inputData);
ko.applyBindings(obj, dom);
});
</script>
AWBSNode is the topmost div in my cshtml. Below is the html for my Grid
<div id="koGridPanel" class="panel panel-default top-buffer">
<div class="panel-heading">AWBS - Areas</div>
<div class="koGrid" data-bind="koGrid: gridOptions"></div>
</div>
Below is the relevant JS of my View Model / grid config / binding
var AreasModel = function(data) {
var self = this;
var apiCall = new APICaller();
.
.
.
self.AllAWBSAreas = ko.observableArray(
ko.utils.arrayMap(data.Areas, function(x) {
return new NewAWBSAreaModel(x);
})
);
.
.
.
this.gridOptions = {
footerRowHeight: 35,
beforeSelectionChange: function (rowItem) {
self.selectAreaToEdit(rowItem);
},
disableTextSelection: false,
keepLastSelected: false,
multiSelect: false,
displaySelectionCheckbox: false,
data: self.AllAWBSAreas,
columnDefs: [
{ field: 'Name', displayName: 'Name' },
{ field: 'Description', displayName: 'Description' }
]
};
.
.
.
Below is the API Call I make (I am using a wrapper function but that part works fine as I do come in to the success path and the data is there
apiCall.CallAPI(
url,
dom,
true,
null,
function (successData) {
self.AllAWBSAreas(ko.utils.arrayMap(successData.Items, function (x) {
return new NewAWBSAreaModel(x);
});
);
I also tried the alternative approach of
apiCall.CallAPI(
url,
dom,
true,
null,
function (successData) {
self.AllAWBSAreas(successData.Items);
.
.
.
.
I have tried calling valueHasMutated() on the AllAWBSAreas entity. There is no problem with the API call and its return data as this pattern works fine in all other aspects of the project. The symptom I see is that the grid in the UI is "affected" but not updated. The tidy blue banner at the top goes blank but the data remains unchanged.
The answer turned out to be a problem with the ladda spinner. The knockout grid would not update if the spinner was attached to the knockout grid. I moved the spinner to nearby piece of UI and all worked. Thanks for those who took the time to look at / help with this issue.
Related
I am rendering a component into a grid rowExpander plugin rowBodyTpl:
plugins: [{
ptype: 'rowexpander',
rowBodyTpl : [
'<div id="contact-row-{Id}"> </div>'
]
}],
var row = 'contact-row-' + record.get('Id'),
grid = Ext.getCmp(gridId),
if(!grid) me.createSubGrid(record,gridId).render(row);
This works fine. But when the grid receives an update event for a record with certain modifiedFields, that record's rowExpander is rerendered using the rowTpl only, and the inner grid is dangling somewhere. I can access the grid using Ext.getCmp, and grid.rendered is true, but it is not shown. So I guess I have to reattach the grid after the row has been rerendered.
I think I can attach to the update event, and find out whether the rowTpl has been rerendered without the child grid using
if(!grid.container.contains(grid.el))
But is there a way to put the grid back into the dom?
I have tried
grid.container.insertFirst(grid.el);
but this does not work. The element is inserted but the grid is not displayed.
Any ideas?
The way you are doing leads to memory leaks (because your component is not destroy when the grid is re-render).
I suggest to check at : Component Template (http://skirtlesden.com/ux/ctemplate) or if you try to render a nested grid, there was several attempt to build something correct and I think a working one is the following : http://blogs.walkingtree.in/2015/06/30/nested-grid-in-sencha-ext-js/
With my trials, I already was on the right track, and the error was somewhere else entirely. The problem was that my update event fn which did the reattach was executed before the update event fn that broke everything, because the fn was fired on the same event and had the same priority, but was attached to the store earlier. Changing the priority was one option, but I just put the creation into the boxready event, which is executed after the view has attached its events to the store:
xtype:'grid',
listeners:{
boxready:function(grid) {
var store = grid.getStore();
...
store.on({
update:function(store, record) {
me.restoreSubgridDom(record);
},
beforeload:function(store) {
var records = store.getRange();
Ext.each(records,me.destroySubgrid);
}
});
}
}
with the two functions:
restoreSubgridDom: function(record) {
var row = 'contact-row-' + record.get('Id'),
gridId = 'ContactRow'+record.get('Id')+'Grid',
grid = Ext.getCmp(gridId);
if(grid && !grid.container.contains(grid.el))
{
grid.container.insertFirst(grid.el);
}
},
destroySubgrid: function(record) {
var gridId = 'ContactRow'+record.get('Id')+'Grid',
grid = Ext.getCmp(gridId);
if(grid) grid.destroy();
}
I want to implement Master / Detail Functionality in kogrid. So that when I select any field of first grid table all the other details will be shown in next detail grid.
Can anybody have any hint how to implement this.
A create a master/detail grid set you need to use a ko.computed to select/filter the detail.
You need 3 observable arrays and 1 computed.
var model = {
parent: ko.observableArray(),
details: ko.observableArray(),
selectedParents: ko.observableArray()
};
model.selectedDetails = ko.computed( function() {
var selectedParent = this.selectedParents()[0];
if ( !selectedParent ) {
return [];
}
return this.details().filter( function(item) {
// define filter comparison here
} );
}, model );
In your markup, define 2 koGrids
<div data-bind="koGrid: {
data: parent,
multiSelect: false,
selectedItems: selectParents }"></div>
<div data-bind="koGrid: { data: selectedDetails } "></div>
koGrid will populate the selectedItems observableArray with Zero or One item. The computed will execute a filter over the children which the second grid is bound to.
I have omitted the columnDef data bind part from breivity.
If you have an AJAX query to run for the details array, then use a subscription against the "parent" observable, change selectDetails to a straight observableArray and set its contents in the AJAX callback
See this jsFiddle for a basic example
I have created a igComboBox with it's datasource set as an observableArray. When I add items to the array, I would like the datasource of the combobox to automatically pull in the new values, without having to set the self.datasource:sourceListArray() explicitly. How can I achieve this?
self.sourceListArray = ko.observableArray();
$("#dataSource").igCombo({
allowCustomValue: false,
showDropDownButton: true,
enableClearButton: false,
dataSource: self.sourceListArray(),
nullText: "Select Data Source",
selectionChanged: self.dataSourceChanged
});
function PopulateSourceList(sourceList) {
for (var i = 0; i < sourceList.length; i++) {
self.sourceListArray.push(sourceList[i].ServiceName);
}
$("#dataSource").igCombo({ dataSource: self.sourceListArray() }); //don't want this
}
Your current sample is not really using the Knockout support for the Ignite UI Combo. Have a look at this sample for KnockoutJS Binding, by comparison you are initializing the Combo the default way, not going through the Knockout binding process (where our handler kicks in). The solution is simple - define the combo like this:
<div id="dataSource" data-bind="igCombo: {
allowCustomValue: false,
showDropDownButton: true,
enableClearButton: false,
dataSource: self.sourceListArray(),
nullText: 'Select Data Source',
selectionChanged: self.dataSourceChanged
}"></div>
And then everything will just work - http://jsfiddle.net/damyanpetev/athF2/
I am trying to bind the listview to the ViewModel. I have placed some hard coded data into the code to ensure that it is not a problem with the web services. I am not seeing any console errors so I am at a loss for how to troubleshoot this problem.
Ideally I would want to have as much of the code dealing with getting the data in the ViewModel, and I want to stay as close as possible to the way that you are supposed to use the KendoUI Mobile framework.
Html
<div data-role="view" id="contactView" data-model="ContactViewModel" data-init="dataInit">
<h1 id="ContactHallo">Contact Screen</h1>
<ul id="contactDetailList"
data-role="listview"
data-style="inset"
data-template="contactDetailtemplate"
data-bind="source: rdata">
</ul>
</div>
JavaScript
var ContactViewModel = kendo.observable({
rdata: null,
loadData: function () {
var testData = [
{
AssociatedContactType: "n\/a",
AssociatedProperties: [],
EmailAddress: "n\/a",
FName: "User1",
HomeNumber: "n\/a",
LName: "LastName",
MobileNumber: "+27 21 0823219213",
WorkNumber: "n\/a"
}];
var loadedData = new kendo.data.DataSource.create({ data: testData });
loadedData.read();
this.rdata = loadedData;
}
});
function dataInit() {
ContactViewModel.loadData();
}
var app = new kendo.mobile.Application($(document.body));
Template
<div>
<h4>Added Record</h4>
#:data.MobileNumber#
</div>
It would be interesting to know why someone down-voted the original question..
I cover this in one of my blog posts here: Kendo Mobile Gotchas, Tips, and Tricks.
The MVVM data bind actually happens before the init event, so your ContactViewModel.rdata is still null when the bind happens. However, I think if you properly call .set() when setting rdata, it might fix your issue:
loadData: function () {
...
this.set('rdata', loadedData);
}
The set should trigger the ListView to update since rdata is being set.
If that doesn't work, then you can get really tricky and delay the MVVM data bind until the init event by doing it yourself instead of using data-model declaratively.
To do that, you would remove data-model= attribute from your view, and instead manually call kendo.bind() at the end of your init function, like this:
<div data-role="view" id="contactView" data-init="dataInit">
function dataInit(initEvt) {
ContactViewModel.loadData();
kendo.bind($(initEvt.view.element), ContactViewModel, kendo.mobile.ui);
}
In my MVC application in Controller i have following function to add and focus new tab to TabPanel with DataView inside:
show_gallery: function(view, record, item, index, e, opts) {
var tabb = Ext.ComponentQuery.query('.gallery_panel');
var gallery_view = Ext.widget('gallery_view');
var ImageStore = Ext.create('Gallery.store.Images');
ImageStore.load({url: 'myphoto/index.php/api/feed/json/' + record.data.uuid});
Ext.apply(gallery_view, {
title: record.data.name,
id: record.data.uuid,
closable:true,
store: ImageStore
});
if (tabb[0].down('#' + record.data.uuid)) {
console.log('Entered IF');
//tabb[0].setActiveTab(tabb[0].down('#' + record.data.uuid));
tabb[0].setActiveTab(record.data.uuid);
}else{
console.log('Entered ELSE');
tabb[0].add(gallery_view);
if (Ext.getCmp(record.data.uuid)) {
console.log('THERE IS SUCH UUID');
}
//tabb[0].setActiveTab(gallery_view);
}
},
And the problem is in the last line. When i uncomment tabb[0].setActiveTab(gallery_view) the new tab is focused but empty and if i leave the line commented the new tab with dataView is populated with data but not focused. I really dont have any idea why setActiveTab() causes DataView not to display at all. The gallery_view widget is Ext.view.View extension.
I'm not sure how come you get the data view if there's no setActiveTab, but there seem to be some issue with this code:
var gallery_view = Ext.widget('gallery_view');
var ImageStore = Ext.create('Gallery.store.Images');
ImageStore.load({url: 'myphoto/index.php/api/feed/json/' + record.data.uuid});
Ext.apply(gallery_view, {
title: record.data.name,
id: record.data.uuid,
closable:true,
store: ImageStore
});
First you create a new widget with Ext.widget() and then you override some config options with Ext.apply(). To my understanding, the latter is fine for primitives but not for objects/arrays.
Generally speaking, the configs are there for the purpose of telling the constructor how to initialise a specific instance of the class. A change to an object's title through Ext.apply() could work if the object is not rendered yet, but not a change to a store config (upon construction the component might start listening to various store events, this won't happen by a simple Ext.apply() which only copies configs from one object to another - you've already missed the train for a component that was created as far as listening to store events goes).
Please try this instead:
var ImageStore = Ext.create('Gallery.store.Images');
ImageStore.load({url: 'myphoto/index.php/api/feed/json/' + record.data.uuid});
var gallery_view = Ext.widget('gallery_view', {
title: record.data.name,
id: record.data.uuid,
closable:true,
store: ImageStore
});