extjs model associations in a list view - extjs

I have two models: Page and Department. I am showing pages in a list view in extjs and I would like to display the Department's Name instead of the department_id in the List view. I have not gotten to the part of actually adding the department to the page VIA the GUI, only through direct db insert statements, but I would like to at least be able to display the department name in the list view.
I have the following so far, this is showing department_id
models
Ext.define('ExtMVC.model.Department', {
extend: 'Ext.data.Model',
fields: ['name']
});
Ext.define('ExtMVC.model.Page', {
extend: 'Ext.data.Model',
fields: ['title','body','department_id'],
associations: [
{type: 'belongsTo', model: 'Department'}
]
});
stores
Ext.define('ExtMVC.store.Pages', {
extend: 'Ext.data.Store',
model: 'ExtMVC.model.Page',
autoLoad: true,
proxy: {
type: 'rest',
url: '/admin/pages',
format: 'json'
}
});
Ext.define('ExtMVC.store.Departments', {
extend: 'Ext.data.Store',
model: 'ExtMVC.model.Department',
autoLoad: true,
proxy: {
type: 'rest',
url: '/admin/departments',
format: 'json'
}
});
List View
Ext.define('ExtMVC.view.page.List' ,{
extend: 'Ext.grid.Panel',
alias : 'widget.pagelist',
title : 'All Pages',
store: 'Pages',
initComponent: function() {
this.tbar = [{
text: 'Create Page', action: 'create'
}];
this.columns = [
{header: 'Title', dataIndex: 'title', flex: 1},
{header: 'Department', dataIndex: 'department_id', flex: 1}
];
this.callParent(arguments);
}
});
controller (fwiw)
Ext.define('ExtMVC.controller.Pages', {
extend: 'Ext.app.Controller',
init: function() {
this.control({
'pagelist': {
itemdblclick: this.editPage
},
'pagelist > toolbar > button[action=create]': {
click: this.onCreatePage
},
'pageadd button[action=save]': {
click: this.doCreatePage
},
'pageedit button[action=save]': {
click: this.updatePage
}
});
},
onCreatePage: function () {
var view = Ext.widget('pageadd');
},
onPanelRendered: function() {
console.log('The panel was rendered');
},
doCreatePage: function (button) {
var win = button.up('window'),
form = win.down('form'),
values = form.getValues(),
store = this.getPagesStore();
if (form.getForm().isValid()) {
store.add(values);
win.close();
this.getPagesStore().sync();
}
},
updatePage: function (button) {
var win = button.up('window'),
form = win.down('form'),
record = form.getRecord(),
values = form.getValues(),
store = this.getPagesStore();
if (form.getForm().isValid()) {
record.set(values);
win.close();
this.getPagesStore().sync();
}
},
editPage: function(grid, record) {
var view = Ext.widget('pageedit');
view.down('form').loadRecord(record);
},
stores: [
'Pages',
'Departments'
],
models: [
'Page'
],
views: [
'page.List',
'page.Add',
'page.Edit'
]
});

Ext's associations have visibly not been designed for use in stores, but rather for working with single records... So, I agree with what has already been said, that you'd better flatten your model on the server-side. Nevertheless, it is possible to achieve what you want.
The association won't load your associated model (i.e. Department) until you call the generated getter method (i.e. getDepartment()). Trying to go this way, that is calling this method for each Page record loaded in your store would require an incredible amount of hack because the grid reacts synchronously to the refresh event of the store, while the getDepartment() method returns asynchronously...
That's why you will have to load your departments data in the same request that loads your pages. That is, your server must return records of the form:
{title: 'First Page', body: 'Lorem', department_id: 1, department: {name: 'Foo'}}
In order for your Page model's proxy to consume this, you need to configure your association this way:
Ext.define('ExtMVC.model.Page', {
// ...
associations: [{
type: 'belongsTo'
// You need the fully qualified name of your associated model here
// ... which will prevent Ext from generating everything magically
,model: 'ExtMVC.model.Department'
// So you must also configure the getter/setter names (if you need them)
,getterName: 'getDepartment'
// Child data will be loaded from this node (in the parent's data)
,associationKey: 'department'
// Friendly name of the node in the associated data (would default to the FQ model name)
,name: 'department'
}]
});
Then comes the really ugly part. Your grid's columns cannot access the associated data with the classic dataIndex property. However, provided the associated records have already been loaded, this can be accessed from a TemplateColumn like this:
{
header: 'Department'
,xtype: 'templatecolumn'
,tpl: '{department.name}'
}
Unfortunately, that will prevent you from using some more appropriate column class (date column, etc.), that you may have configured globally. Also, this column loses track of the model field it represents, which means that some features based on introspection won't be able to do their magic (the grid filter ux, for example, uses the field type to decide automatically on the type of filter).
But in the particular case you've exposed, that won't matter...
Complete Example
Here's what it gives when you bring it all together (or see it in action)...
Ext.define('ExtMVC.model.Department', {
extend: 'Ext.data.Model',
fields: ['name'],
proxy: {
type: 'memory'
,reader: 'json'
,data: [
{id: 1, name: 'Foo'}
,{id: 2, name: 'Bar'}
,{id: 30, name: 'Baz'}
]
}
});
Ext.define('ExtMVC.model.Page', {
extend: 'Ext.data.Model',
fields: ['title','body','department_id'],
associations: [{
type: 'belongsTo'
,model: 'ExtMVC.model.Department'
,getterName: 'getDepartment'
,associationKey: 'department'
,name: 'department'
}],
proxy: {
type: 'memory'
,reader: 'json'
,data: [
{title: 'First Page', body: 'Lorem', department_id: 1, department: {name: 'Foo'}}
,{title: 'Second Page', department: {name: 'Bar'}}
,{title: 'Last Page', department: {name: 'Baz'}}
]
}
});
Ext.define('ExtMVC.store.Pages', {
extend: 'Ext.data.Store',
model: 'ExtMVC.model.Page',
autoLoad: true
});
Ext.define('ExtMVC.view.page.List', {
extend: 'Ext.grid.Panel',
alias : 'widget.pagelist',
title : 'All Pages',
store: Ext.create('ExtMVC.store.Pages'),
initComponent: function() {
this.tbar = [{
text: 'Create Page', action: 'create'
}];
this.columns = [
{header: 'Title', dataIndex: 'title', flex: 1}
,{header: 'Department', xtype: 'templatecolumn', flex: 1, tpl: '{department.name}'}
];
this.callParent(arguments);
}
});
Ext.widget('pagelist', {renderTo: 'ct', height: 200});

As one of the comments mentions, it would be a lot simpler to bind the department name to the Pages model when working with a grid/list. Denormalisation for display is not a bad thing.
An alternative is perhaps reverse the modelling, in that you could say the a department 'hasMany' Pages.
This allows you define the primary and foreign keys on the relationship and then your department store will (per row) have a automatic 'pages()' store which will contain the child info.
I've typically done this for master/detail forms where I want to bind 'pages()' to a list/grid, but keep the department model as the master record on a form for example.

Why shouldn't use renderer config function on the grid's column ?
This implies to (auto)load the departments store upfront on application launch (you might need it in more places anyway, think of grid cell editor as full department list).
{
name:'Department',
dataIndex:'department_id',
renderer: function(deptId){
return Ext.data.StoreManager.lookup('Departments').getById(deptId).get('name');
}
}
PS: I was using myself denormalisation for display, but doesn't feel good ;)

Related

Binding Grid and Form ExtJs 6

I have a grid populating records from view model (Ajax proxy data from Java restful web services). When I select a record in the grid form open and the fields are displayed. The records are editable using the form.
I have one tag field which is binded to a store and I should be able to populate the data associated to the record whenever the user selects a record.
"data" : [ {
"createdOn" : 1475678859000,
"updatedOn" : 1475679885000,
"updatedBy" : null,
"id" : 174,
"userName" : "ffff,
"firstName" : "gg",
"lastName" : "ggg",
"salesDepartment" : [ {
"id" : 3,
"code" : "FC",
"name" : "Fiat-Chrysler",
"departmentHead" : "xxx",
"applicationCode" : null} ],}
I need to bind the value of id from sales department object . how can I achieve this. Please help me.
{xtype: 'tagfield',
anchor: '100%',
reference: 'salesDept',
fieldLabel: 'Sales Dept\'s',
name: 'salesDepartment',
allowBlank: false,
displayField: 'name',
valueField: 'id',
bind: {
value: '{record.salesDepartmentIds}',
store: '{SalesDepartmentStore}'},
}
Tag fields are really only designed to work with arrays or comma separated lists;
It looks like you are trying to use with associations, which it would be nice if there was more support for this out of the box.
I've had a go, and in terms of displaying got it working however saving values will really depend on how you plan to sync values to the server, if you are going to use in built proxies etc. then this will present a whole different challenge. I would like a similar component for my own apps, I will think on this further and may even create a UX for this.
I think you have several issues here, one is getting your associations working properly, then you need to make your tagfield work as expected.
Here is the fiddle
And the key parts are:
An extended tagfield:
Ext.define('RelatedTagField', {
extend: 'Ext.form.field.Tag',
xtype: 'rtagfield',
config: {
relatedStore: null
},
setValue: function (val) {
this.setRelatedStore(val);
var value;
if (val && val.isStore) {
value = val.collect('id');
} else {
value = '';
}
this.callParent([value]);
},
getValue: function () {
return this.relatedStore;
},
onBindStore: function () {
this.callParent(arguments);
this.on('select', function (tagfield, selection) {
var selectedIds = [];
//add any new selections
Ext.each(selection, function (rec) {
var rrec = this.relatedStore.getById(rec.id);
if (!rrec) {
this.relatedStore.add(rec.data)
}
selectedIds.push(rec.id);
}, this);
//remove any not selected anymore
this.relatedStore.each(function (rrec) {
if (selectedIds.indexOf(rrec.id) == -1) {
this.relatedStore.remove(rrec);
}
}, this);
}, this);
},
})
Binding the value:
{
xtype: 'rtagfield',
fieldLabel: 'Sales Department',
name: 'id',
displayField: 'name',
valueField: 'id',
bind: {
store: '{salesDepartment}',
value: '{record.salesDepartment}'
}
},
And adding a definition for the association:
Ext.define('MyApp.model.User', {
extend: 'Ext.data.Model',
alias: 'model.user',
hasMany: [{
model: 'MyApp.model.SalesDepartmentModel',
associationKey: 'salesDepartment',
role: 'salesDepartment'
}],
requires: [
'Ext.data.field.String'
],
fields: [{
type: 'string',
name: 'userName'
}, {
type: 'string',
name: 'firstName'
}, {
type: 'string',
name: 'lastName'
}, {
name: 'salesDepartment'
}, {
name: 'roles'
}, {
type: 'string',
name: 'email'
}, {
name: 'notificationFrequencyId'
}]
});
Further reading
I suggest you read more on associations and the tagfield source code
In terms of saving records back to the server, I would recommend looking at sessions

Ext JS grid panel events

I am developing my first EXT js mvc application. I have a problem adding events listeners to my grid panel from controller class.
My question is why i am getting below error while adding event handlers through 'on' method using below code
Error:
Uncaught TypeError:
gridPanel.on is not a function
at constructor.init
Relevant Code:
var gridPanel = Ext.ComponentQuery.query('userlist');
gridPanel.on('itemdblclick', this.editUser, gridPanel);
I am able to add handlers through a different method though but i am trying to figure out what is wrong with adding through 'on' method on grid panel reference. Any help is appreciated.
Thanks
Here is my complete controller class
Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',
views: ['user.List'],
init: function() {
var gridPanel = Ext.ComponentQuery.query('userlist');
gridPanel.on('itemdblclick', this.editUser, gridPanel);
},
editUser: function() {
console.log('User edit has begun..');
}
}, function() {
});
And my grid panel class:
Ext.define('AM.view.user.List', {
extend: 'Ext.grid.Panel',
alias: 'widget.userlist',
title: 'All Users',
id: 'usergrid',
initComponent: function() {
this.store = {
fields: ['name', 'email'],
data : [
{name: 'Ed', email: 'ed#sencha.com'},
{name: 'Tommy', email: 'tommy#sencha.com'}
]
};
this.columns = [
{header: 'Name', dataIndex: 'name', flex: 1},
{header: 'Email', dataIndex: 'email', flex: 1}
],
this.callParent(arguments);
}
});
Ext.ComponentQuery.query('userlist') returns an array of found components - so you need to select the first item in the array to actually get your component:
Ext.ComponentQuery.query('userlist')[0]
The Ext.first('userlist') shorthand makes this simpler. (As #sceborati mentioned in the comments)
When you use component query you are querying by its xtype*
You have also specified an id in your component, which is OK if you will only have one instance on a page, (as soon as you have more than one - you should not use ids)
You can find components much quicker using Ext.getCmp('componentid') so Ext.getCmp('userlist') would also work for you.
All that being said, to make better use of ExtJs MVC capabilities, you could switch to using a ViewController, and then you don't need to do any lookups for your components at all:
Ext.define('AM.controller.Users', {
extend: 'Ext.app.ViewController',
alias: 'controller.userlist',
editUser: function () {
console.log('User edit has begun..');
}
});
Ext.define('AM.view.user.List', {
extend: 'Ext.grid.Panel',
xtype: 'userlist',
title: 'All Users',
id: 'usergrid',
controller: 'userlist',
store: {
fields: ['name', 'email'],
data: [{
name: 'Ed',
email: 'ed#sencha.com'
}, {
name: 'Tommy',
email: 'tommy#sencha.com'
}]
},
columns: [{
header: 'Name',
dataIndex: 'name',
flex: 1
}, {
header: 'Email',
dataIndex: 'email',
flex: 1
}],
listeners:{
itemdblclick:'editUser'
}
});
Notice the alias config added to controller, and the corresponding controller config on the view.
This means you can specify listeners just by providing the method name on the controller.
listeners:{
itemdblclick:'editUser'
}
You may also notice I have moved your column and store configuration out of the initComponent method - your use of initComponent would work, but this method is much cleaner.
I have created a fiddle to demonstrate this updated code:
https://fiddle.sencha.com/#view/editor&fiddle/1qvu
You also seem to be passing an extra function as a third argument when defining your controller - you should remove this.
* you have used alias:'widget.userlist' which is the same as xtype:'userlist'
The query method on Ext.ComponentQuery returns an array of components. So you would need to provide an index, like:
var gridPanel = Ext.ComponentQuery.query('userlist')[0];

Extjs two instances of stores not using different models

edit http://jsfiddle.net/zns6B/4/ Added js fiddle link and running into a cannot get 'Fields' of undefined now
edit2 So i found that the second grids store model is correct with Sc.Model.B. But the records in the store have ids that are Sc.Model.A . So my store model is set to Sc.Model.B but the store is using Sc.Model.A . It still gets stores the data in the store but only as if the model was set to Sc.Model.A in the first place.
edit3 I have take all the creation of instance out of my ListGrid. I have instead added them when creating the list grid. I have added the following. This does not work either. I am at a lose for what to do.
var obj1 = Ext.create('Sc.ListGrid',{
title: "first Component",
foo: true,
id: 'firstGrid',
myStore: Ext.create('My.Store.MyStore',{model:Ext.create('My.Model.Model'});
renderTo: 'renderToMe1'
});
I am trying to generate these two grids. When foo == true i want it to generate a store with model A. When it equals false i want it to use model B. I have tried to just specifically add the My.Model.MyModel but that does not work. The second grid will somehow inherit the first models model. I have changed it just to try and use fields instead of using the model at all but the second grid still uses the first grids.
I have also tried declaring the Stores inside the initComponent as well but i get the same result either way.
var obj1 = Ext.create('Sc.ListGrid',{
title: "first Component",
foo: true,
id: 'firstGrid',
renderTo: 'renderToMe1'
});
var obj2 = Ext.create('Sc.ListGrid',{
title: "second Component",
foo: false,
renderTo: 'renderToMe2'
});
Sc.ListGrid
Ext.define('Sc.ListGrid', {
extend: 'Ext.grid.Panel',
title: 'Grid',
store: Ext.data.StoreManager.lookup('bleh'),
requires: ['stuff'],
columns: [
{ text: 'id', dataIndex: 'id' },
],
config:{
foo: null,
},
initComponent: function(){
if(this.foo == true){
Ext.apply(this,{
store: this.buildStore1()
});
}
if(this.foo == false){
Ext.apply(this,{
store: this.buildStore2()
});
}
this.callParent();
},
buildStore1:function(){
return Ext.create('Sc.Store.League.LeagueStore',{url:'somewhere',fields:["S"]});
},
buildStore2:function(){
return Ext.create('Sc.Store.League.LeagueStore',{url:'somewhere',fields:["A"]});
}
});
Also an example of a model i am trying to use as well.
Ext.define('Sc.Model.A', {
extend: 'Ext.data.Model',
fields: [
{name: 'id', type: 'string'},
{name: 'type', type: 'string'},
{name: 'gamename', type: 'string'}
]
});
Ext.define('Sc.Model.B', {
extend: 'Ext.data.Model',
fields: [
{name: 'id', type: 'string'},
{name: 'type', type: 'string'},
{name: 'gamename', type: 'string'},
{name: 'something1', type: 'string'},
{name: 'something2', type: 'string'},
]
});
It will create both grids and load the data from my webservice. When i check the grid with Sc.Model.B's data it will have id and type. But will not have any data for something1, and something2. My webserivce is returning json and all values are entered. There are no nulls. If i Ext.getCmp('firstGrid').getStore().getData(0); If i use Ext.getCmp('firstGrid').getStore() and check the model name. It shows Model B but reflects A
Do you need it to be done in the initComponent()??
This is a fiddle I saved from a while ago when I was trying to do something similar. If you need help tweaking it let me know and ill update it.
The main thing to note is the grid.reconfigure(store,columns);
That will change the grid's store and columns appropriately.
http://jsfiddle.net/zqG55/1/
The issue was that the proxy wasn't being set or created properly because the proxy model was referencing the previous model instance. This is my solution
var themodel = 'A.Model.SomeModel';
var myProxy = new Ext.data.proxy.Ajax({
model: themodel,
url: url,
reader: {
type: 'json',
}
});
Ext.apply(this,{
columns: modelColumns.columns,
store: Ext.create('M.Store.MyStore',{
model: themodel ,
autoLoad: true,
proxy: myProxy
})
});

Use of ArrayStore to filter grid locally

I need to FILTER my grid based on certain criteria.
For example; if there are 3 fields, Name,age,school. I need to filter the grid Locally, so that it displays all students with the age 10 in the grid.
I read that this is achievable if i use Ext.data.ArrayStore. But i am not sure how to apply this in to my code. My Store class looks like this;
Ext.define('Project.store.Person',{
extend:'Ext.data.Store',
model:'App.model.Person',
proxy: {
type: 'ajax',
url : '/person.php'
}
});
My Model;
Ext.define ('Project.model.Person',{
extend: 'Ext.data.Model',
fields:['name','age','school']
});
How can i apply Ext.data.ArrayStore, in order to filter columns locally. Like to display all students who are 10 years of age.
**UPDATE**
GRID - View
this.columns = [
{
{ text: "Size", dataIndex: 'size' ,filter: {
type: 'list',
options: ['small', 'medium', 'extra large']
} }, ...
STORE
Ext.define('SerenExample.store.GridFilterExample',{
extend:'Ext.data.Store',
model:'App.model.GridFilterExample',
remoteFilter: false,
remoteGroup:true,
proxy: { ...
I don't see the check box saying filter, when i click on the respected column
UPDATE
Ext.define('SerenExample.view.GridFilterExample' ,{
extend: 'Ext.grid.Panel',
alias : 'widget.gridfilt',
features: [],
initComponent: function() {
this.store = 'GridFilterExample';
this.columns = [
{
...
UPDATE 2
GET http://localhost/SerenExample/feature/filters.js?_dc=1341677311248 404 Not Found 104ms
"NetworkError: 404 Not Found - http://localhost/SerenExample/feature/filters.js?_dc=1341677311248"
Ext.define('SerenExample.view.GridFilterExample' ,{
extend: 'Ext.grid.Panel',
alias : 'widget.gridfilt',
features: [ {ftype: 'filters',
autoReload: false,
local: true,
filters: [{
type: 'list',
dataIndex: 'status',
options: ['small', 'medium', 'extra large']
}]}],
initComponent: function() {
this.store = 'GridFilterExample';
this.columns = [
{
You don't need to use ArrayStore. Just specify remoteFilter: false in your store definition and apply filter. It will be done locally.

Sencha touch 2.0 many-to-many associations - how?

I've been having a hell of a time getting Sencha Touch 2.0 hasMany associations working, especially since it looks like their data models don't directly allow for many-to-many relationships. I've got two models - People and Roles (theere are a bunch more, but these are the two that matter in this example) , each has a many-to-many to the other.
I originally thought that I could do this with a hasMany in each of the models, but snce the data is stored in third-normal form in my db, I figure that I need a third, person-to-role model. Code is here:
Ext.define('SMToolkit.model.Person', {
extend: 'Ext.data.Model',
config: {
fields: [
'id',
'first_name',
'last_name',
'email',
'address',
'phone1',
'phone2',
'type',
'location'
],
hasMany: [
{
model: 'SMToolkit.model.Person_Role',
name: 'role'
}
],
proxy: {
type: 'rest',
url : 'index.php/api/persons'
}
}
});
Ext.define('SMToolkit.model.Role', {
extend: 'Ext.data.Model',
config: {
fields: [
'id',
'name',
'description',
'type',
'show_id'
],
hasMany: [
{
model: 'SMToolkit.model.Person_Role',
name: 'person'
},
{
model: 'SMToolkit.model.Scene_Role',
name: 'scene'
},
{
model: 'SMToolkit.model.Thing',
name: 'thing'
}
],
proxy: {
type: 'rest',
url : 'index.php/api/roles'
}
}
});
Ext.define('SMToolkit.model.Person_Role', {
extend: 'Ext.data.Model',
config: {
fields: [
'person_id',
'role_id'
],
associations: [
{
type: 'belongsTo',
model: 'SMToolkit.model.Person',
name: 'person'
},
{
type: 'belongsTo',
model: 'SMToolkit.model.Role',
name: 'role'
},
],
proxy: {
type: 'rest',
url : 'index.php/api/personsroles'
}
}
});
I've confirmed that the personsroles url above does in fact return a valid data set, so I know that there should be something in there...
When I look at a Role record, I can see fields for the associated stores, but even if I know for certain that there's an appropriate record in the Person_Role table in the db, the Persons array in the record is empty.
I'm getting the record like so:
onRoleSelect: function(list, index, node, record) {
var editButton = this.getEditButton();
if (!this.showRole) {
this.showRole = Ext.create('SMToolkit.view.role.Show');
}
person = record.person();
thing = record.thing();
scene = record.scene();
person.load();
thing.load();
scene.load();
// Bind the record onto the view
this.showRole.setRecord(record);
// Push the show show view into the navigation view
this.getRoleContainer().push(this.showRole);
},
What am I doing wrong? Why is there no association data?
Here's an alternative approach for handling complex model relations in Sencha.
I've not yet tested it but I think it will likely handle Many-Many relations as well.
Recursive M-M relations might cause you grief with the linkChildAssociations() function.
http://appointsolutions.com/2012/07/using-model-associations-in-sencha-touch-2-and-ext-js-4/

Resources