Implementing Many-to-Many Associations in ExtJS 4 - extjs

This question has been asked twice before in on SO:
https://stackoverflow.com/questions/10244753/extjs-many-to-many-association-how
Extjs 4.1 many-to-many model association
BUT neither of these questions have an actual answer, so I'm going to try again!
Let's say I have two models, User and Group. A user can be in many groups, and groups can contain many users. Here's the model code for User:
Ext.define('User', {
extend: 'Ext.data.Model',
alias: 'model.User',
fields: [
{name: 'username', type: 'string'},
...
],
proxy: {
// Omitted for brevity...
},
});
And Group:
Ext.define('Group', {
extend: 'Ext.data.Model',
alias: 'model.Group',
fields: [
{name: 'name', type: 'string'},
...
],
proxy: {
// Omitted for brevity...
},
});
Now, let's say I wanted a Grid which lists my groups, and allows me to double-click a group and edit which users are in that group in second grid.
Let's also say there's a lot of users per group, so I don't want to load all the associated users when I load the groups.
I want to be able get a store of users for a particular group, and give that to my grid, which will load data as needed (using the usual pagination that a grid does).
I see two potential approaches here. There may another better way, but I will outline what I've tried do so far below.
Intermediate model
Add another joining model
Add hasMany associations from User and Group to that model
Add belongsTo associations from my joining model back the way to User and Group.
Joining model code:
Ext.define('GroupUsers', {
extend: 'Ext.data.Model',
alias: 'model.GroupUsers',
fields: [
{name: 'group_id', type: 'int'},
{name: 'user_id', type: 'int'},
],
associations: [
{type: 'belongsTo', model: 'User'},
{type: 'belongsTo', model: 'Group'}
],
...
});
Association in Group:
associations: [
{type: 'hasMany', model: 'GroupUsers', name: 'group_users'}
],
I will now be able to access a store of GroupUsers for a particular Group:
group.group_users()
The problem with this approach, is that I can't just bind a store of GroupUsers to my second grid, because I want to display things like the user's name. I could iterate the store's items, fetch each User object with getUser(), add them to another store, and use that for my Grid, but that results in a server request per item! Alternatively, I could use my store of GroupUsers directly, but then would need to do something with renderers and I still need to fetch each User individually.
Direct association
Associate User and Group directly with a hasMany association on each
Associations on Group:
associations: [
{type: 'hasMany', model: 'User', name: 'users', foreignKey: '???'}
],
I can now get a store of actual User objects for a given group:
group.users()
Which would be great, except there's nothing for me to set the foreignKey of the association to. User can't have a group_id field, because a User can have many Groups!

Maybe this is not the answer you look for, but this is how I would solve this issue :
I would not link the groups and the users with extjs store associations, but rather on the server side.
In the controller of your grid put something like this :
init: function(){
this.control({
'grid': {itemdblclick: this.onGridItemdblclick}
})
},
onGridItemdblclick: function(grid, record){
var group_id = record.getId(),
usersStore = this.getStore('Users');
usersStore.load({params: {group_id: group_id}});
var win = Ext.widget('UsersGrid'); // adapt the code to your naming scheme
win.show();
}
The request to load the Users store will be sent with an extra parameter group_id. On the server side, your can use this extra parameter to filter your users.

Related

ExtJS Grid column with dataIndex from referenced model

I have an ExtJS gridanel with a viewmodel store bound to it. The model of that store is a defined model with a field that references another model.
For the sake of simplicity here's an example:
Ext.define('User', {
extend: 'Ext.data.Model',
fields: ['name']
});
Ext.define('Order', {
extend: 'Ext.data.Model',
fields: ['date', {
name: 'user',
reference: 'User'
}]
});
viewmodel: {
store: {
order: {
model: 'Order',
}
}
}
In my gridpanel I have 2 columns. In one column, dataIndex: 'date' works correctly, displaying the date. However, in my second column I want to display the name of the user. I have no idea how to do that. The user field itself is on object so declaring dataIndex: user doesn't work. I tried dataIndex: {user.name} but that doesn't work either. I even tried
bind: {
data: '{user.name}'
}
also to no avail. I have found a solution using renderer config of the column but it is really ugly and beats the point of having a model reference another if I have to manually traverse the data structure for the appropriate field.
TL;DR
I'm looking for a way to declare a column's dataIndex to be a field from a reference model.

How do I override the url field when extending a base model in Extjs?

I am new to Extjs and am following an Extjs 5 example on creating a model hierarchy:
(http://docs.sencha.com/extjs/5.0/core_concepts/data_package.html)
I created a base model which holds a proxy with a url field. I now want to extend this base model and only override the url part below:
Ext.define('MyPortal.model.Base', {
extend: 'Ext.data.Model',
fields: [{
name: 'id',
type: 'int'
}],
schema: {
namespace: 'MyPortal.model', // generate auto entityName
proxy: {
type: 'ajax'
,url : '/portal-web/{entityName}'
,reader: {
type:'json',
rootProperty:'{entityName:lowercase}',
idProperty: 'id'
}
}
}
});
Here is a child model:
Ext.define('MyPortal.model.Account', {
extend : 'MyPortal.model.Base'
,fields: [
{name: 'accountId', type: 'string'},
{name: 'name', type: 'string'}
]
, //add something here to override the url from the base model above?
});
I tried adding a url field or function to the child model, but these are ignored. Is it possible for a child model to override the parent's url param?
Thanks!
The proxy option in the schema is what Ext5 calls an ObjectTemplate. Simply put, it is used as a default configuration for the models of the schema.
You can override this defaults in the proxy configuration of the model itself (one proxy instance will be created by model class -- not by model instance). Note that the model class itself doesn't accept an url option, it must be set in the proxy.
For you, that would give something like this:
Ext.define('MyPortal.model.Account', {
extend : 'MyPortal.model.Base' // I guess you had a typo here
,fields: [
{name: 'accountId', type: 'string'},
{name: 'name', type: 'string'},
]
// Override proxy URL (the other options of the schema's proxy
// will be used)
,proxy {
url: 'path/to/accounts'
}
});
FYI, the reader doesn't have an idProperty option (so says the docs), it's in the model this time...
I have a slightly different problem: my data model is generated, so i am using override: to extend model classes like this:
Ext.define('app.override.Foo', {
override: 'app.data.Foo',
proxy: {url: 'rest/foo',type: 'rest'}
}
this proxy override does not take effect in extjs 5 (works great in 4.x)

How to insert association/child data into Ext.data.list as separate entries

I'm using ST2 and using MVC. I'm very new to ExtJS and Sencha, so am not au fair the best practices for many things - and on this issue I've hit a dead end despite research.
I'll use a toy example to illustrate my issue below, but essentially I have a relationship as follows (which all works correctly from an association perspective).
Business X -- Location A
|
-- Location N
The Problem
I want to then populate the data into (for instance) an Ext.dataview.List, but to process it such that each location (i.e. child location) has its own separate entry in the table; not just a simple itemTpl formatting a single entry. However, at present I can't find any way to do that. Is it possible to hook into a List and format the data as I want, or should I be creating a new store? Ideally I want to make best use of the associations.
As a rough example, each entry would look like this, with some parent data and some child data:
---------------------------
|Smith Co - 1 Smith Street|
---------------------------
|Smith Co - 24 High Street|
---------------------------
|Tea[...] - 12 Tea Leaf |
---------------------------
|Tea[...] - 3 Bis Kit |
---------------------------
Example Code
Raw data
[
{
"id":1,
"name":"Smith Co",
"locations":[
{
"address":"1 Smith Street"
},
{
"address":"24 High Street"
}
]
},
{
"id":2,
"name":"Tea So Good",
"locations":[
{
"address":"12 Tea Leaf"
},
{
"address":"3 Bis Kit"
}
]
}
]
Location Model
Ext.define('Example.model.Location', {
extend: 'Ext.data.Model',
config: {
fields: [
{ name: 'address', type: 'string' }
],
proxy: { ... }, // Rest proxy that loads data as shown above.
BelongsTo: 'Example.model.Company'
}
});
Company Model
Ext.define('Example.model.Company', {
extend: 'Ext.data.Model',
config: {
fields: [
{ name: 'id', type: 'int' },
{ name: 'name', type: 'string' }
],
proxy: { ... }, // Rest proxy that loads data as shown above.
hasMany: { model: 'Example.model.Location', name: 'locations' }
}
});
Store
Ext.define('Example.store.Companies', {
extend: 'Ext.data.Store',
require: 'Example.model.Company',
config: {
model: 'Example.model.Company'
}
});
Controller
// (works correctly, relationships are traversable)
// Companies store is looked up and loaded in #launch()
View
Ext.define('Example.view.CompaniesList', {
extend: 'Ext.List',
xtype: 'companieslist',
config: {
layout: 'fit',
store: 'Businesses',
itemTpl: [
// Tpl is only to format inside each element
]
}
});
Solution Edit (15th Sept 2013):
Solution
I used the solution #rixo suggested (and I had been hoping to avoid in the original question).
I created a separate store for the list, and loaded the data I need into it by using a load listener on the Companies store. This seems to be the most graceful solution available, although it means you may need to add extra logic in various places to ensure it remains satisfactorily synchronised.
By pushing the location objects themselves into the new store the associations remain intact (i.e. you can still do location.getCompany()).
Yes, create another store for locations.
You may have tried a template like this:
itemTpl: [
'{name}',
'<tpl for="locations">',
', {address}',
'</tpl>'
]
But that will indeed only display the information, it won't let you interact with each location as an individual list item.
You could get it working by hacking the view's doRefresh method, but that's just going against the lib's intention and other developer' expectations.
Maybe the problem is that you can get the data only in this format, that is with locations as children of companies, and you can't get the server to send you a flat list of companies. In that case, I think the most meaningful approach would be to customize a reader to flatten locations from companies, and feed a standalone location store. The extractData method seems a very promising start for that (see how the JSON reader uses it to implements its root property).

sencha touch 2: binding associations data to existing store

I have a simple data model that looks something like this (actual code below):
model Game:
fields: id, team_1_id, team_2_id
model GameScore:
fields: id, game_id, team_1_score, team_2_score, is_final, submission_date
model SpiritScore:
fields: id, game_id, team_1_score, team_2_score
What I want seems simple. I already have code that loads Games and GameScores in bulk. I have a 'Game' instance in hand, and can call gameScores(). And I get a store, but it's empty. I have code that will dynamically load it, by placing the store into the model's hasMany definition. But what I would really like is some way to bind the Game.gameScores() call to the my existing GameScores store. Even if it used a normal filter underneath, that gives me a single record that I can bind and use in a view. (Important note: the data does not come in nested form.)
This leads to my second question. Game:GameScores is 1:many, but I only ever display the most recent one (from live score reporting). What is the general approach here? I can also manually build a filter from the game_id, but I can only bind 1 record to a view, so I don't see how I can bring that other information into a view, short of a proper hasMany relationship. Is there another way?
Any and all advice, including telling me to RTFM (with a link to the relevant manual) would be greatly appreciated! I've been pulling my hair out on this (pro bono side project) for the last week.
Cheers!
b
Ext.define('TouchMill.model.Game', {
extend: 'Ext.data.Model',
config: {
fields: [ 'id', 'team_1_id', 'team_2_id' ],
hasMany: {
model: 'TouchMill.model.GameScore',
name: 'gameScores',
},
},
});
Ext.define('TouchMill.model.GameScore', {
extend: 'Ext.data.Model',
config: {
fields: [ 'id', 'game_id', 'team_1_score', 'team_2_score', 'is_final', 'submission_date', ],
},
// belongsTo necessary? Don't think so unless I want parent func?
});
Ext.define('TouchMill.model.SpiritScore', {
extend: 'Ext.data.Model',
config: {
fields: [ 'id', 'game_id', 'team_1_score', 'team_2_score', ],
},
},
I've never used touch, so I'm speaking about Ext4 here (4.2 to be precise)... And, your model definitions seem a bit broken to me (is that working with touch?). But whatever, you'll get the general idea. If my code don't work in touch, please try with Ext4.
Also, I understood that you're loading all your scores at once. If that's not the case, my solution will need to be adapted...
So, my general reasoning is the following: if you've loaded all your scores in memory, then why not use a memory proxy that uses the score store's data as the data source for the store generated for the association? I tried that and, quite to my surprise, it worked without a glitch.
To understand this, you need to know that a proxy is an independant data source, that is a proxy can be shared between multiple stores without problem. On the other hand, a store is expected to be bound to a single view or task. For example, if you bind the same store to two different grids, then filtering the first grid will affect the second as well.
And while most proxies do not "contain" their data, memory proxy do. Here's a relevant excerpt of Ext.data.proxy.Memory#read method:
resultSet = operation.resultSet = me.getReader().read(me.data)
So, enough theory, here's the proof of concept (tested in this fiddle):
// I instantiate this proxy myself in order to have a reference available
var masterScoreProxy = Ext.create('Ext.data.proxy.Memory');
Ext.define('TouchMill.model.GameScore', {
extend: 'Ext.data.Model',
fields: [ 'id', 'game_id', 'team_1_score', 'team_2_score', 'is_final', 'submission_date' ],
// I've used a remote server to ensure this all works even asynchronously
proxy: {
// configure your own
}
});
Ext.define('TouchMill.model.Game', {
extend: 'Ext.data.Model'
,fields: [ 'id', 'team_1_id', 'team_2_id' ]
,hasMany: {
model: 'TouchMill.model.GameScore'
,name: 'gameScores'
// required in order to avoid Ext autogenerating it as 'touchmill.model.game_id'
,foreignKey: 'game_id'
// needed if we don't want to have to call gameRecord.gameScores().load()
,autoLoad: true
// first part of the magic: make the generated store use my own proxy
,storeConfig: {
proxy: masterScoreProxy
}
}
});
// Just mocking a store with two games
var gameStore = Ext.create('Ext.data.Store', {
model: 'TouchMill.model.Game'
,data: [{id: 1}, {id: 2}]
,proxy: 'memory'
});
// Creating the "master" score store (that will use the model's proxy)
var scoreStore = Ext.create('Ext.data.Store', {
model: 'TouchMill.model.GameScore'
// second part's in there
,listeners: {
load: function(store, records, success) {
if (success) {
// 1. replace the data of the generated association stores' proxy
// (I must say I'm quite surprised that I didn't had to extract the data of
// every records, nor to configure a reader and all for my shared proxy...
// But hey, that works!)
masterScoreProxy.data = records;
// 2. update already generated stores
// Alternatively, you could call gameRecord.gameScores().load() individually
// before each usage of gameRecord.gameStores()
gameStore.each(function(record) {
var childStore = record.gameScoresStore;
if (childStore) {
childStore.load();
}
});
}
}
}
});
// test first load
scoreStore.load({
callback: function(records, operation, success) {
if (success) {
// and here's to prove it
gameStore.each(function(record) {
record.gameScores().each(function(score) {
console.log('Game ' + record.id + ': ' + JSON.stringify(score.data, undefined, 2));
});
});
testRefreshedData();
}
}
});
function testRefreshedData() {
// test refreshing
scoreStore.load({
callback: function(records, operation, success) {
if (success) {
console.log('--- Scores have changed ---');
gameStore.each(function(record) {
record.gameScores().each(function(score) {
console.log('Game ' + record.id + ': ' + JSON.stringify(score.data, undefined, 2));
});
});
}
}
});
}
Regarding your other questions...
If you have a 1:n for Game:Score, you've got a 1:1 for Game:MostRecentScore... So, I'd try to use that.
As for the view, there should always be a way -- even if hackish -- to access data nested in your records. The way will depend on what you're calling view here... See, for example this question.

How can I set up a complex model association in ExtJS 4?

Here's the basic table structure in the database:
Users <-> UserGroups <-> Groups
Basically, a group can contain many users, and a user can be a part of many groups. The UserGroups table is just an association.
How can I represent this same relationship using ExtJS 4 models?
I'm thinking the properties need to be something like:
Users belongsTo UserGroups
UserGroups hasMany Users and Groups
Groups belongsTo UserGroups
I'm just unsure if that will work as I need it to (since I will need to save the association between users and groups to the database).
I'm going to continue researching, but I hope either someone knows how to accomplish this, or at least has a good idea!
This is quite simple to do now, check this:
Ext.define('Group', {
extend: 'Ext.data.Model',
fields: ['id', 'user_id', 'group_id'],
belongsTo: 'UserGroups'
});
Ext.define('User', {
extend: 'Ext.data.Model',
fields: ['id', 'name', 'group_id'],
belongsTo: 'UserGroup'
});
Ext.define('UserGroup', {
extend: 'Ext.data.Model',
fields: ['id', 'name'],
hasMany : [{model: 'User', name: 'users'},
{model: 'Group', name: 'groups'}]
});
That should get you starting.

Resources