I am trying to implement a grid with tree data using ag-grid. I am using the Enterprise Row Model. The problem is that when hard coding the data and setting it through setRowData the grid displays perfectly. However, when data is loaded through the enterprise row model, the grid does not render as a tree. In fact, the getDataPath callback is not even being called.
Did anyone manage to use the tree data feature with an enterprise data source as this does not seem to be documented?
Thanks
I am assuming that by Enterprise row model, you mean Serverside row model so you expect tree structured data from server. In that case, I have been able to combine following features in Ag-grid : Infinite scrolling + Tree data + server side row model.
I have even implemented custom filtering and it's working as expected.
Data flow:
We have to enable the server side row model on ag-grid using its configuration.
Implement a fake server and proxy data source objects in JavaScript. The object of data source must contain a method named "getRows" that ag-grid can call. Ag-grid will call this method every time user performs actions such as: scroll, filter, sorting, expanding a parent row to see child rows etc.
Implement a method called onGridReady() which will be called by ag-grid every time it's trying to render the grid first time, and then pass the server and dataSource objects to ag-grid's internal API inside onGridReady().
Implementation (using combination of ReactJS and plain JavaScript):
Enable serverSide row model in ag-grid.
<AgGridReact
columnDefs={this.columnDefs}
rowModelType={this.rowModelType}
treeData={true}
isServerSideGroup={this.isServerSideGroup}
getServerSideGroupKey={this.getServerSideGroupKey}
onGridReady={this.onGridReady}
cacheBlockSize={50}
/>
Implement a fake server and proxy data source objects in JavaScript.
To work with server side row model, you need to supply the data in an instance of ServerSideDataSource in JavaScript. Instance of ServerSideDataSource must have a method called getRows() which will be called by ag-grid every time user scrolls down to get next set of data or a row is expanded for retrieving its children records in Tree structure.
The constructor for ServerSideDataSource accepts a proxy data container, typcailly used in ag-grid example: a fakeServer instance. A singleton instance of fakeServer holds the data that was received from the real server and keeps it for following usage:
a) when ag-grid wants to display child records, it calls getRows. Because we supply a custom implementation of this ServerSideDataSource, we can write logic inside getRows to extract data from this fakeServer instance.
b) When ag-grid tries to display next set of data in infinite scrolling or paging, it checks how much data was last retrieved to ask for next block in infinite scrolling (using startRow and endRow variable).
Defining fake server and server side data source:
function createFakeServer(fakeServerData) {
function FakeServer(allData) {
this.data = allData;
}
FakeServer.prototype.getData = function(request) {
function extractRowsFromData(groupKeys, data) {
if (groupKeys.length === 0) {
return data; //child records are returned from here.
}
var key = groupKeys[0];
for (var i = 0; i < data.length; i++) {
if (data[i].employeeId === key) {
return extractRowsFromData(groupKeys.slice(1), data[i].children.slice());
}
}
}
return extractRowsFromData(request.groupKeys, this.data);
};
return new FakeServer(fakeServerData);
}
function createServerSideDatasource(fakeServer) {
function ServerSideDatasource(fakeServer) {
this.fakeServer = fakeServer;
}
ServerSideDatasource.prototype.getRows = function(params) {
console.log("ServerSideDatasource.getRows: params = ", params);
var rows = this.fakeServer.getData(params.request);
setTimeout(function() {
params.successCallback(rows, rows.length);
}, 200);
};
return new ServerSideDatasource(fakeServer);
}
Implement onGridReady()
Once this dataSource is ready, you have to supply this to ag-grid by calling its API method: params.api.setServerSideDataSource(). This API is available inside onGridReady() method that must be passed to Ag-grid as well. This method is mandatorily required if you're using serverSide row model.
onGridReady = params => {
...
var fakeServer = createFakeServer(jsonDataFromServer);
var dataSource = createServerSideDatasource(fakeServer);
params.api.setServerSideDatasource(dataSource);
}
Providing a key property that help ag-grid identify parent-child relationship. You have to supply these parameters to grid. Check the point (1) in Implementation that has HTML syntax and shows how to supply these methods to ag-grid.
var rowModelType = "serverSide";
var isServerSideGroup = function (dataItem) {
return !!dataItem.children;
};
var getServerSideGroupKey = function (dataItem) {
return dataItem.employeeId;
};
Notice that in getServerSideGroup(), we are returning a boolean value which checks whether children property of current row (i.e. dataItem) has any children or not.
I would request you to separately look through documents for server side row model for each feature and that means Tree data (client model) and Tree data (server side model) have two different approaches. We can't setup one model and expect it to work with data of other model.
Documentation for server side row model : https://www.ag-grid.com/javascript-grid-server-side-model/
Please try this and let me know. I had these requirements a month ago, so I contacted them for their help on Trial Support and they have prepared a plunker for this problem statement:
Working example for Tree data from server side with infinite scroll. https://next.plnkr.co/edit/XON5qvh93CpURbOJ?preview
Note:
Since the tree data is being retrieved from server, you can't use getDataPath.
Tree data would be in nested hierarchy per row unlike the client-side tree model. So unique column names are not encapsulated in an array.
Wrong :
var rowData = [
{orgHierarchy: ['Erica'], jobTitle: "CEO", employmentType: "Permanent"},
{orgHierarchy: ['Erica', 'Malcolm'], jobTitle: "VP", employmentType: "Permanent"}
...
]
Right :
[{
"employeeId": 101,
...
"children": [
{
"employeeId": 102,
...
"children": [
{
"employeeId": 103,
...
},
{
"employeeId": 104,
...
}
]},
]}
}]
There was one point when the grid was not being rendered at all in initial phase when I was just setting up the grid with Enterprise features and so it's their recommendation to use px instead of % for height and width of the wrapper DIV element that contains your Ag-grid element.
Edit:
If you prefer to fetch children record in separate API calls to save initial load time then you can make these API calls inside getRows() method. API call will have success and error callbacks. Inside the success callback method, once you receive the children data, you can pass them to ag-grid using:
params.successCallback(childrenData, childrenData.length);
Example: When a parent row is expanded, it sends a unique key of that parent row (which you must have configured already) through params.request.groupKeys. In this example, I am using JavaScript's fetch() to represent API calling. This method accepts ApiUrl, and optional request parameters' object in case of POST/PUT requests.
ServerSideDatasource.prototype.getRows = function(params) {
//get children data based on unique value of parent row from groupKeys
if(params.request.groupKeys.length > 0) {
fetch(API_URL, {...<required parameters>...})
.then(response => response.json(), error => console.log(error))
.then((childrenData) => {
params.successCallback(childrenData, childrenData.length);
});
}
else {
//this blocks means - get the parent data as usual.
params.successCallback(this.fakeServer.data, this.fakeServer.data.length);
}
};
Adding import 'ag-grid-enterprise' followed by initializing the enterprise key resolved the issue of getDataPath not working correctly.
Using example provided by Akshay Raut above, I was inspired to combine server side pagination, and client side grouping. Following his steps, with a slight change. On the getServerSideDatasource function, you can check if the parent node have the children, and rather than calling the server, just return the children directly. That way, you will be able to display the already loaded children as client side. Here is a sample code:
getServerSideDatasource(): IServerSideDatasource {
return {
getRows: (params) => {
if (params.request.groupKeys.length > 0) {
params.success({
rowData: params.parentNode.data.sales,
rowCount: params.parentNode.data.sales.length,
});
} else {
// Your regular server code to get next page
}
Here is a stackblitz:
https://stackblitz.com/edit/ag-grid-angular-hello-world-1gs4jx?file=src%2Fapp%2Fapp.component.ts
Make sure you are using gridOptions.treeData = true An example is here.
Hierarchy of data should be properly set when you implement the gridOptions.getDataPath(data)
Make sure you have implemented Enterperise.getRows Read more
If the above things don't work, share your code here to understand the overall picture better.
Infinite Scrolling or Enterprise/Serverside datasources are not compatible with Tree Data
https://www.ag-grid.com/javascript-grid-row-models/
So you have to either change your code to use Client Side Row Model or use Row Grouping (only available in enterprise)
Related
I have developed a Master-Detail app. In the Detail view, I am using a DynamicPage where within content, I am using an IconTabBar Element with 3 items. Each item is a different Fragment with a Smartform.
Master view is loading the data from an EntitySet (MasterEntity)
Detail view with a is using some fields from the Entity MasterEntity
Fragment 1: is using DetailEntitySet
Fragment 2: is using DetailEntitySet
Fragment 3: is using DetailEntitySet
Basically, when Detail View Controller is detecting the RouteMatch, I am receiving the selected line on Master View and I bind it to the View Detail. Once it is done, I am checking if the view was generated and then, calling a method to bind the Expanded entity to the iconTabBar Element which contains the 3 Fragments. The code is as follows:
function _onRoutePatternMatched(event) {
if (event.getParameter("name") === "detail") {
var path = event.getParameter("arguments").contextPath;
if (path !== " ") {
var path2 = "/" + path;
view.bindElement(path2);
if (view) {
this._setBindingToIconTab("Master2Detail");
}
} else {
view.unbindElement();
}
}
}
The _setBindingToIconTab function is as follows:
_setBindingToIconTab: function (sAssociation) {
view.byId("iconTabBar").bindElement(sAssociation);
}
iconTabBar is the ID I have assigned within the Detail view to the IconTabBar Element.
The problem is, when I execute it and it loads the first Fragment, all is ok. With the 2nd and 3rd, I got errors (but the values are displayed...). I am loading the Fragments when they are picked on the screen. If they were not generated, I instantiate and store them in an array. The errors I got are:
Assertion failed: The EDM property "DateFrom" was not found in the "ZZODATA_TEST_SRV.Master" entity type. -
sap.ui.comp.smartfield.ODataControlFactory
It is complaining about Fields from DetailEntitySet are not in MasterEntitySet.
Could you please give me a hand with this?
As there is no XML, running example etc. i can't tell you why this error occurs but..
Best practise is to biind also in the detail view the selected entity with expand to the detail and so on
MasterEntitySet->DetailEntitySet
As bindings are propagated to children there is no need to bind the iconTabBar again. Data is already there. Again i don't know your case, but most services look like this
MasterEntitySet->DetailEntitySet->DataVariantA(ForIconTab1)
->DataVariantB(ForIconTab2)
->DataVariantC(ForIconTab3)
In V4 your detail code looks like this
oView.bindObject({
path: "/MasterEntity(" + this._args.ID + ")",
parameters: {
$expand:`DetailEntityNavPath($expand=DataVariantANavPath()...`
},
events: {
dataReceived: (oEvent) => {...
In the affected application is a responsive table whose ColumnListItems are added via JavaScript code. Now the lines should be highlighted by the highlighting mechanism depending on their state. The first idea was to control the whole thing via a normal controller function. I quickly discarded the idea, since the formatter is intended for such cases. So I created the appropriate Formatter function and referenced it in the JavaScript code. The call seems to work without errors, because the "console.log" is triggered in each case. Also the transfer of fixed values is possible without problems. However, the values I would have to transfer are located within customData of each line...
No matter how I try to form the path I get an "undefined" or "null" output.
I have already tried the following paths:
"/edited"
"/customData/edited"
"mAggregations/customData/0/mProperties/value"
"/mAggregations/items/0/mAggregations/customData/0/mProperties/value"
The code from Controller.js (with consciously differently indicated paths):
var colListItem = new sap.m.ColumnListItem({
highlight: {
parts: [{
path: "/mAggregations/items/0/mAggregations/customData/0/mProperties/value"
}, {
path: "/edited"
}],
formatter: Formatter.setIndication
},
cells: [oItems]
});
// first parameter to pass while runtime to the formatter
colListItem.data("editable", false);
// second paramter for the formatter function
colListItem.data("edited", false);
oTable.addItem(colListItem);
The code from Formatter.js:
setIndication: function (bEditable, bEdited) {
var sReturn;
if (bEditable && bEdited) {
// list item is in edit mode and edited
sReturn = "Error";
} else if (bEditable || bEdited) {
// list item is in edit mode or edited
sReturn = "Success";
} else {
sReturn = "None";
}
return sReturn;
}
The goal would also be for the formatter to automatically use the value of the model in order to avoid its own implementation of a listener, etc.
I hope one of you has a good/new idea that might bring me a solution :)
Many thanks in advance!
You cannot bind against the customData. Because the customData is located in the element, it is like a property.
Thats why you defined it here on colListItem: colListItem.data("key", value)
You only can bind against a model.
So I see three solutions
Store the information in a separate local JSON model whereof you can speficy your binding path to supply the values to your formatter
Do not supply the information via a binding path to the formatter, but read a model/object/array from a global variable in the controller holding the information via this (=controller) in formatter function
Store the information in the customData of each element and access the element reference in the formatter function via this(=ColumnListItem).data().
Passing the context to the formatter similar to this formatter: [Formatter.setIndication, colListItem]
Cons of 1. and 2: you need a key for a respective lookup in the other model or object.
From what I understand I would solve it with solution 3.
I have a user form that is created in extjs framework. The user form has many user fields which are being passed as part of request payload to the REST controller.
I am trying to add a grid panel(most likely in a tabular format with multiple rows & columns) to the user form.
But I am not sure how to pass the grid panel data as part of request payload to the REST controller.
I will post more code if any more details are needed.
Any help would be appreciated. Thanks.
Ext.define('soylentgreen.view.admin.UserForm', {
extend : 'Ext.form.Panel',
alias : 'widget.userform',
bodyStyle : 'padding:5px 5px 0',
// some userform elements like firstname,lastname, go here.....
name : 'userTeamGrid',
xtype : 'gridpanel',
id : 'userTeamGrid',
itemId : 'userTeamGrid',
multiSelect : true,
selModel : Ext.create(
'Ext.selection.CheckboxModel',
{
injectCheckbox : 'first',
mode : 'MULTI',
checkOnly : false
}),
anchor : '100%',
width : '700px',
height : 250,
flex : 1,
store : 'userTeamStore',
var user = form.getRecord();
form.updateRecord(user);
user.save({
callback : function(records, operation){
//reset the (static) proxy extraParams object
user.getProxy().extraParams = {
requestType: 'standard'
}
if(!operation.wasSuccessful()){
var error = operation.getError();
IMO, That's one of the most complicated yet common issues we face in ExtJS. It has to be solved often, but most solutions have to be slightly different depending on the exact requirements.
Grids are bound to a full store, not to a single record. So if you want to get data from a record (e.g. as an array) into a grid and vice versa, you have to have a mapping between the data from the store and the value in a single field of a record. To e.g. get all data from the store, the generic solution is
store.getRange().map(function(record) { return record.getData(); });
If you need only the grid selection (maybe you want to use a checkboxselection model or similar) and/or only certain fields of the records, you have to change this code to your needs, e.g.
grid.getSelectionModel().getSelection().map(function(record) {return record.get('Id'); });
So, your record is bound to the form, and a form consists of fields. How do you get the grid data to be accepted as part of the form?
You have to add a hiddenfield to your form and, depending on your requirement, overwrite some of the four functions setValue, getValue, getModelData, getSubmitValue. Usually, the hiddenfield tries to convert its value to a string, but obviously, you need to allow for arrays there; and you want to modify the grid whenever setValue is called, or read the value from the grid whenever one of the getters is called. The four functions are used for the following:
setValue should be used to modify the grid based on the value you find in the record.
getValue is used in comparison operations (check whether the value has changed, which means the form is dirty and has to be submitted, etc.) and if you call form.getValues. You should return some value based on the grid state in it.
getModelData is used by the form.updateRecord method; it should return a value that the model field's convert method can work with.
getSubmitValue is used by the form.submit method, it should return a value that can be safely transmitted to the server (has to be a string if the form doesn't have sendAsJson:true set)
I cannot give you an exact recipe for these implementations, as they are specific to your requirements.
I am looking at a trial of the Ajax Browser Control by ItHit. So far it seems to be pretty responsive when it comes to pulling files across http protocol.
What I want to do at this point is have the details view pull custom properties from my excel workbooks. What is the most efficient way to connect my C# code that gets the custom properties to the Ajax control to display the correct values?
The easiest way to create a custom column is to return custom property from a WebDAV server. In the example below the server returns price in PricesNs:RetailPrice property.
On a client side you will define a custom column and specify custom property name and namespace:
{
Id: 'MyColumn1',
CustomPropertyName: 'RetailPrice',
CustomPropertyNamespace: 'PricesNs',
Text: 'Retail Price',
Width: '150px'
}
Another approach is to return an HTML from a Formatter function specified for column. You have a full control over what is being displayed in this case.
You can find more details and an example in this article: http://www.webdavsystem.com/ajaxfilebrowser/programming/grids_customization/
In case your WebDAV server is running on IT Hit WebDAV Server Engine, to return the requested property, you must implement IHierarchyItem.GetProperties method (or its asynchronous counterpart):
public IEnumerable<PropertyValue> GetProperties(IList<PropertyName> names, bool allprop)
{
if (allprop)
{
return getPropertyValues();
}
List<PropertyValue> propVals = new List<PropertyValue>();
foreach(PropertyName propName in names)
{
if( (propName.Namespace == "PricesNs") && (propName.Name == "RetailPrice") )
{
// Depending on the item you will return a different price,
// but here for the sake of simplicity we return one price regerdless of the item
propVals.Add(new PropertyValue(propName, "100"));
}
else
{
...
}
}
return propVals;
}
I am using mvc 4, i have a form in a view (not binded to a model), its using standard html elements.
I want to populate a dropdownlist from a list value (i.e return from controller action)
also based on the selection of a value from the first dropdownlist i want to populate a second dropdownlist
can someone please guide
for the first dropdownlist you loop thru all the available options and add in <option> tags inside <select> for the second dropdownlist you need to either make bunch drop downs and hide/show them, or you create one big list and remove invalid entries base on the first list's selection. You would definitely need to do javascript for 2nd list.
If you don't want to use a Model (you should though), you will have to add the items to ViewData
I'll stub something out for you and you can complete the rest.
Inside your controller, create a list object of what you need. If you are using EntityFrameWork this will look familiar.
var list = context.Table.ToList();
List<System.Web.Mvc.SelectListItem> ddlItems = new List<System.Web.Mvc.SelectListItem>();
foreach (var item in list)
{
ddlItems.Add(new SelectListItem(){ Text = item.Text, Value = item.Value.ToString()});
}
ViewData["DDLItems"] = ddlItems;
#Html.DropDownListFor(x => x.LeagueId,
new SelectList((System.Collections.IEnumerable)ViewData["DDLItems"], "Value", "Text")
, "--Select League--", new { id = "league" })
You can define your second dropdownlist with just a placeholder until the cascade effect happens.
#Html.DropDownListFor(x => x.DivisionId, Enumerable.Empty<SelectListItem>(),
"--Select Division--", new { id = "ddlDivision" })
Your going to need to use JQuery and fire and event when the dropdown changes, and then use Ajax to make a call back to the controller. Theres 2348239 examples online about making Ajax calls, know how to do that because it's done all the time in MVC.
I'll let you figure that part out. One hint, inside the Ajax call you can pass data to the controller. something like this data: { leagueId: value } where value is the value of the dropdownlist you want to cascade off of. leagueId must match type and name of the parameter your controller will expect.
Return a Json object from your controller like so...
public JsonResult GetDivisions(int leagueId)
{
var division = //similar to before, fill a list.
return Json(divisions, JsonRequestBehavior.AllowGet);
}
And then in the success function of your Ajax call, you will populate the Second dropdownlist.
success: function (data) {
$.each(data, function (index, item)
$('#ddlDivision')
.append($('<option></option>')
.val(item.Value)
.html(item.Text))
item.Value and item.Text can be anything, just as long as the Json you return as the properties of Text and Value
IE...
var divisions = (from x in context.Division
select new
{
Text = league + " " + x.Region,
Value = x.DivisionId
}).ToList();