PDA

View Full Version : Displaying a hasMany model relationship in a Grid



whirling dervish
15 May 2012, 10:27 AM
Does ExtJS have a mechanism to display a hasMany relationship in a grid?

For instance, if I had 1 to many relationship and had a row represented the 1 side of the relationship and wanted to extend out the columns for each item in a many relationship, would that be possible, or do the columns need to be uniform in a grid?

scottmartin
15 May 2012, 12:00 PM
After looking around, I found this:
http://stackoverflow.com/questions/6265641/how-to-display-nested-json-data-in-extjs-4-grids

Regards,
Scott.

whirling dervish
16 May 2012, 5:57 AM
Thank you Scott for your reply.

While I might be able to use the mapping approach suggested by that thread to accomplish the hasMany grid, I don't think what the StackOverflow poster is asking is the same as what I am looking for.

So this is what I have for my models,



Ext.define('Daughter', {
extend: 'Ext.data.Model',
fields: [
{ name: 'age' },
{ name: 'name' }
]
});

Ext.define('Father', {
extend: 'Ext.data.Model',
fields: [
{ name: 'id' },
{ name: 'married' },
{ name: 'name' },
{ name: 'sons' }
],
hasMany: {
model: 'Daughter',
name: 'daughters'
}
});

I think I have the data modeled correctly. Here is a sample of my JSON.


"fathers" : [
{
"id" : 0,
"married" : false,
"name" : "Kenneth Lee",
"sons" : null,
"daughters" : [
{
"age" : 5,
"name" : "Sarah"
},
{
"age" : 11,
"name" : "Deborah"
}
]
},


So far my approach has been playing with the column mapping to see if I can get it to behavior like I expect.

This what I have for my columns. This code is WIP and does not display correctly.


columns = [
{
xtype: 'rownumberer'
},
{
text: 'name',
dataIndex: 'name'
},
{
text: 'married',
dataIndex: 'married'
},
{
text: 'sons',
dataIndex: 'sons'
},
{
text: 'daughters',
columns: [
{
text: 'daughter',
dataIndex: 'daughters[0].name'
}
]
}
];


Keep in mind that I am new to extJS, so that Stackoverflow answer might have what I am looking for, but I am too dense to see it :)

Connall
17 May 2012, 8:49 AM
See this http://www.sencha.com/forum/showthread.php?201062-Populate-sub-grid-with-data-from-main-grid

whirling dervish
21 May 2012, 7:08 AM
Thanks, but if I understand that thread correctly, he is creating two grids and populating the second grid with whatever was selected in the first. That is not what I am trying to do.

whirling dervish
29 May 2012, 3:01 PM
Anyone have a solution for this?

redraid
30 May 2012, 1:44 AM
Try this plugin https://github.com/mitchellsimoens/Ux.grid.plugin.AssociationRowExpander

whirling dervish
30 May 2012, 7:48 AM
That's interesting. But that plugin displays the hasMany / belongTo on the row when it's expanded, as opposed to display the hasMany as additional columns inline on the row.

sdt6585
30 May 2012, 2:20 PM
Here's what I would try, the grid wouldn't be editable this way though without a fair amount of work:
Create a store of fathers before you create the grid.
Use. the store.each() function to go through each father and find the maximum number of daughters while at the same time building an array of father models, flattening the hasMany daughter relationship along the way (ie father[i].daughter1, father[i].daughter2, ...).
Create a new store copying all the fields (store.fields = []) from the father model and add as many daughter fields as the maximum from the above step and use the array from the above step as the data for the store.
Create your grid with columns for the father properties and as many daughter columns as found in step 2. Use the store created in step 3 as the grid's store.
Good luck!

whirling dervish
31 May 2012, 10:16 AM
That sounds like a workable solution. I have started implementing it, but I can't figure out how to get the initial data store to load before I create the second data store (the flattened data one).

Prior to the load I can't call store.each, because there is nothing in the store. so I've tried using store.load(function) and given the load a function to flatten out the data, but that isn't called until after the other stores are created.

srarnold
31 May 2012, 10:49 AM
What I need to do is display the following arrays in Grids, but at the moment really struggling to find any simple examples of this

I have a form which will show the basic information, then with this will be three seperate grids holding, comment information, tag information and Version information.

This is my model definition, when viewed under FireBug, I can see all the data I need,


Ext.define('DocDetails', {
extend: 'Ext.data.Model',
fields: [
{ name: 'DocumentID', type: 'int' },
{ name: 'Title', type: 'string' },
{ name: 'Version', type: 'int' },
{ name: 'Author', type: 'string' },
{ name: 'LastAmended', type: 'date' },
{ name: 'FileAmended', type: 'date' },
{ name: 'UserID', type: 'string' },
{ name: 'FileID', type: 'int' },
{ name: 'Synopsis', type: 'string' },
{ name: 'FileInfoID', type: 'int' },
{ name: 'DocumentStatusID', type: 'int' },
{ name: 'Icon', type: 'string' },
{ name: 'Tags' },
{ name: 'Comments' },
{ name: 'Versions' },
{ name: 'Success' },
{ name: 'ErrMsg', type: 'string' },
],
associations: [
{ type: 'hasMany', model: 'Tags', name: 'tags' },
{ type: 'hasMany', model: 'Comments', name: 'comments' },
{ type: 'hasMany', model: 'Versions', name: 'versions' }
]
});

Ext.define('Tags', {
extend: 'Ext.data.Model',
fields: [
{ name: 'DocumentID', type: 'int' },
{ name: 'TagMasterID', type: 'int' },
{ name: 'TagCategoryID', type: 'int' },
{ name: 'TagCategoryName', type: 'string' },
{ name: 'TagCategoryDescription', type: 'string' },
{ name: 'TagDataTypeID', type: 'int' },
{ name: 'TagTextVal', type: 'string' }
],
belongsTo: {
model: 'DocDetails',
foreignKey: 'DocumentID'
}
});

Ext.define('Comments', {
extend: 'Ext.data.Model',
fields: [
{ name: 'DocumentID', type: 'int' },
{ name: 'UserID', type: 'string' },
{ name: 'Comment', type: 'string' },
{ name: 'Created', type: 'date' }
],
belongsTo: {
model: 'DocDetails',
foreignKey: 'DocumentID'
}
});

Ext.define('Versions', {
extend: 'Ext.data.Model',
fields: [
{ name: 'DocumentID', type: 'int' },
{ name: 'Title', type: 'string' },
{ name: 'Version', type: 'int' },
{ name: 'Author', type: 'string' },
{ name: 'LastAmended', type: 'date' }
],
belongsTo: {
model: 'DocDetails',
foreignKey: 'DocumentID'
}
});


How do I show these hasMany items in there own grids ?

whirling dervish
31 May 2012, 10:53 AM
srarnold, did you want each child in the relationship to be nested in grids under the grid? Or flat out in a single row?

srarnold
31 May 2012, 11:08 AM
The top part of the form has static display, this information is not in a grid, but what I do need is the Comments to be in a grid, as they will be allowed to add new comments to this, also I need a grid for the Tags, this also will also need to allow additions and deletions also the facility to edit the items, attached the screen as it is at the moment:
35810

whirling dervish
31 May 2012, 11:16 AM
You should take a look at Connall's suggestion on the first page.


See this http://www.sencha.com/forum/showthread.php?201062-Populate-sub-grid-with-data-from-main-grid

whirling dervish
31 May 2012, 11:30 AM
Here's what I would try, the grid wouldn't be editable this way though without a fair amount of work:
Create a store of fathers before you create the grid.
Use. the store.each() function to go through each father and find the maximum number of daughters while at the same time building an array of father models, flattening the hasMany daughter relationship along the way (ie father[i].daughter1, father[i].daughter2, ...).
Create a new store copying all the fields (store.fields = []) from the father model and add as many daughter fields as the maximum from the above step and use the array from the above step as the data for the store.
Create your grid with columns for the father properties and as many daughter columns as found in step 2. Use the store created in step 3 as the grid's store.
Good luck!

Another question about this solution. If I have father.daughter1 and have a property called name, how do I go about specifying that as a dataIndex on a column?

I tried dataIndex: daughter1.name but that doesn't work.

[I]Note: That is a set the dataIndex to just daughter1, it comes in the grid as Object. So I know I have the object attached to the parent object.

EDIT:
I was able to get this to work using a template column.


text: 'daughter name',
xtype: 'templatecolumn',
tpl: '{' + daughter + '.name}'


There probably is a better way to do this.

I am still looking for an answer to my question at the bottom of the first page.

sdt6585
31 May 2012, 2:19 PM
On the question from the first page, you don't actually need to use store.each(). The callback from the load() function receives an array of records as an argument so you can just do a normal loop through those model instances rather than using store.each.

As far as getting the dataIndex on the column to work right, I think you would need to avoid using a full daughter model instance as the value for the daughter field. Just concatenate whatever you want to display when you flatten it into a single value. Then you don't have to deal with the column template and can set each dataIndex to ("daughter" + i) or whatever you feel like serializing the field names to.


//Goals
var flatGrid;
var flatStore;
var data = [];
var maxDaughters = 0;

this.fatherStore.load({
scope: this,
callback: function(records, operation, success) {
if (success === true) {
//Loop records
for (var i = 0, l = records.length; i < l; i++) {
//Get daughters store
var dStore = record[i].daughters();

//Update max daughters
if (maxDaughters < dStore.count()) {
maxDaughters = dStore.count();
}

//Add all the father info to the data object for the flat store
data[i] = record.getData();

//Loop through daughters and add to the flat data
var x = 0;
dStore.each(function (record) {
//you could concatenate First + Last here
data[i]['daughter' + x] = record.get('first') + record.get('last');
}, this);
}

flatStore = //Build new store with (maxDaughters - 1) ("daughter" + i) named fields

flatGrid = //Build new grid with (maxDaughters - 1), ("daughter" + i) indexed columns, and use flatStore as the data store
} else {
Ext.Error.raise('Oh no!');
}
}
});

sdt6585
31 May 2012, 2:34 PM
You have to build the flattened store inside the callback function for store.load(). Pretty much anything you do after the load call needs to be inside that function otherwise all the code will be executed out of order since load is an asynchronus xhr request. If it's getting too far nested that way, I would suggest doing everything after the store load in another named function and just referencing the function and scope in the callback.



store.load(function(records) {
parseRecords(records);
buildFlatStore();
buildFlatGrid();
}, this)

var parseRecords = function(records) {...}
var buildFlatStore = function() {...}
var parseRecords = function() {...}

srarnold
1 Jun 2012, 3:03 AM
1. Create a new Model just for the Array Element of the Parent Model.
2. Assign this empty model as the Store to the grid
3. This is my Store.load


store.load({
callback: function(rec, options, success) {

if (success) {

var dtComments = rec[0].data.Comments;
commentStore.loadData(dtComments);
grid.reconfigure(commentStore);

}

}
});


Now I'm happy it is working the way I want it to :)

whirling dervish
1 Jun 2012, 6:55 AM
Got it working using the method sdt6585 suggested



Ext.onReady(function() {


// models
Ext.define('Daughter', {
extend: 'Ext.data.Model',
belongsTo: 'Father',
fields: [ 'age', 'name' ]
});


Ext.define('Father', {
extend: 'Ext.data.Model',
fields: [ 'id', 'married', 'name', 'sons' ],
hasMany: 'Daughter'
});

// store
Ext.create('Ext.data.Store', {
storeId: 'fatherStore',
autoLoad: true,
model: 'Father',
proxy: {
type: 'ajax',
url: 'data1.json',
reader: {
root: 'fathers'
}
},
listeners: {
load: function(store, records, successful, eOpts) {
if (!successful) {
return;
}


var fathers = [];
var maxDaughters = 0;
Ext.Array.forEach(records, function(record) {
var number = record.daughters().data.items.length;
if (number > maxDaughters) {
maxDaughters = number;
}


var father = record.data;
for (var i=0; i < number; i++) {
father['daughter' + i] = record.daughters().data.items[i].data;
fathers.push(father);
}
});


buildGrid(fathers, maxDaughters);
}
}
});


var buildGrid = function(fathers, maxDaughters) {


var buildFields = function(maxDaughters) {
var fields = [ 'id', 'married', 'name', 'sons' ];
for (var i = 0; i < maxDaughters; i++) {
fields.push('daughter' + i);
}
return fields;
};


Ext.create('Ext.data.Store', {
storeId: 'flattenedStore',
autoLoad: true,
fields: buildFields(maxDaughters),
data: { fathers: fathers },
proxy: {
type: 'memory',
reader: {
type: 'json',
root: 'fathers'
}
}
});


// columns
var buildColumns = function(maxDaughters) {
var columns = [
{
xtype: 'rownumberer'
},
{
text: 'name',
dataIndex: 'name'
},
{
text: 'married',
dataIndex: 'married'
},
{
text: 'sons',
dataIndex: 'sons'
}
];
for (var i = 0; i < maxDaughters; i++) {
var d = 'daughter' + i;
columns.push({
text: d,
columns: [
{
text: 'name',
xtype: 'templatecolumn',
tpl: '{' + d + '.name}'
},
{
text: 'age',
xtype: 'templatecolumn',
tpl: '{' + d + '.age}'
}
]
});
}
return columns;
};


// grid panel
Ext.create('Ext.grid.Panel', {
id: 'gridpanel',
title: 'Fathers',
renderTo: Ext.getBody(),
width: 600,
height: 400,
store: Ext.data.StoreManager.lookup('flattenedStore'),
columns: buildColumns(maxDaughters)
});
};
});


Unfortunately the sort for the flatten fields are not working, but I'll keep banging on it for a little and see if I come up with anything.

I wonder if the flatten step is needed, it might be possible to achieve the same thing using template columns.

whirling dervish
5 Jun 2012, 7:29 AM
I was able to get this working by accessing the child arrays using the template column, with no flatten of the data store needed.

Here is the code,


Ext.onReady(function() {


// models
Ext.define('Daughter', {
extend: 'Ext.data.Model',
belongsTo: 'Father',
fields: [ 'age', 'name' ]
});


Ext.define('Father', {
extend: 'Ext.data.Model',
fields: [ 'id', 'married', 'name', 'sons' ],
hasMany: 'Daughter'
});

// store
Ext.create('Ext.data.Store', {
storeId: 'fatherStore',
autoLoad: true,
model: 'Father',
proxy: {
type: 'ajax',
url: 'data1.json',
reader: {
root: 'fathers'
}
},
listeners: {
load: function(store, records, successful, eOpts) {
if (!successful) {
return;
}


var maxDaughters = 0;
Ext.Array.forEach(records, function(record) {
var number = record.daughters().data.items.length;
if (number > maxDaughters) {
maxDaughters = number;
}
});


buildGrid(maxDaughters);
}
}
});


var buildGrid = function(maxDaughters) {


// columns
var buildColumns = function(maxDaughters) {
var columns = [
{
xtype: 'rownumberer'
},
{
text: 'name',
dataIndex: 'name'
},
{
text: 'married',
dataIndex: 'married'
},
{
text: 'sons',
dataIndex: 'sons'
}
];
for (var i = 0; i < maxDaughters; i++) {
var d = 'daughter' + i;
columns.push({
text: d,
columns: [
{
text: 'name',
xtype: 'templatecolumn',
tpl: '{[values.daughters[' + i + '] ? values.daughters[' + i + '].name : "" ]}'
},
{
text: 'age',
xtype: 'templatecolumn',
tpl: '{[values.daughters[' + i + '] ? values.daughters[' + i + '].age : "" ]}'
}
]
});
}
return columns;
};


// grid panel
Ext.create('Ext.grid.Panel', {
id: 'gridpanel',
title: 'Fathers',
renderTo: Ext.getBody(),
width: 600,
height: 400,
store: Ext.data.StoreManager.lookup('fatherStore'),
columns: buildColumns(maxDaughters)
});
};


});


I think this is a better method then the one suggested by sdt6585, as it doesn't involve building an additional store to handle building the columns. If I could mark this as the best answer I would.

It still has the issue where I can't sort the daughter columns.

sdt6585
5 Jun 2012, 7:53 AM
Nice, I'll have to dig into the templating features, I haven't had much need for them so far. Wish I could have pointed you in that direction in the first place, and thanks for sharing your solution.

whirling dervish
5 Jun 2012, 9:42 AM
No problem. Hope my solution is useful.

Let me know if you find a way to handle sorting.

H@v
14 Mar 2013, 12:54 AM
@whirling dervish

i used your method to work arround in MVC way but no success i am not be able to build grid dynamically can you please tell me what is the MVC way. following is the code which i am using in my grid view widget in the initComponent function:

var store = Ext.getStore('Routes');
var maxStores = 0;
store.load({
callback: function(records, operation, success) {
if (!success) {
return;
}
Ext.Array.forEach(records, function(record) {


var number = record.get('stores').length;
if(number > maxStores) {
maxStores = number;
}
});
}
});
this.columns = [
{
text: 'id',
dataIndex: 'routeID'
},
{
text: 'Route Name',
dataIndex: 'routeName'
},
{
text:'CDC',
renderer:function(v,m,r){
return r.get('cdc').name;
}
}


];
for (var i = 0; i < maxStores; i++) {
var s = 'store ' + (i+1);
this.columns.push({
text: s,
xtype: 'templatecolumn',
sortable:true,
tpl: '{[values.stores[' + i + '] ? values.stores[' + i + '].storeName : "" ]}'


});
}

saurabh14august
16 Dec 2013, 3:47 AM
Hi,

I am trying to do the same thing with a difference that i want to add checkcolumns dynamically.
With template columns, its working fine but with checkcolumns, i am not able to specify the dataIndex and the header.

Kindly help me with the same.



Thanks,
Saurabh