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

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).

Related

Implementing Many-to-Many Associations in ExtJS 4

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.

ExtJS 4 I don't understand models + associations

I don't think the documentation is very clear on this - at least I can't figure out how associations work with models in ExtJS 4. Lets look at a simple example:
the models
Ext.define('app.model.Goo', {
...
fields: ['id', 'foo_id', 'goo_field'],
belongsTo: 'Foo'
});
Ext.define('app.model.Foo', {
...
fields: ['id', 'foo_field'],
hasMany: {model: 'Goo', name: 'goos'} //
});
this allows me to easily write a server response that returns nested data such as:
{
success: true,
foo: {
id: 42
foo_field: 'bacon',
goos: [
{ id: 0, goo_field: 'velociraptor' },
{ id: 1, goo_field: 'spidermonkey' },
...
]
}
}
and parse out the data into their respective models. But what if I want to load nested data lazily? Say I write my server such that it doesn't send any goos field in my returned foo object. What does it mean to write foo.goos().load()? What's being sent to my server then? GET <proxy:url>/<'id' of foo>/goos ?
If you want to lazy load goo you should not send goo in the foo response. But instead you call foo.goos(). This wil return a goo store with a filtervalue foo_id on 42 (primary id from your foo instance). Basicly its doing something like this for you:
Ext.create('Ext.data.Store', {
model: 'app.model.Goo',
filters: [
{
property: 'foo_id',
value: 42
}
]
});
So calling load does the request. Assuming you are using REST proxy, your goo proxy will do the following request: GET <proxy:url>with filter-queryparameters: filter:[{"property":"foo_id","value":42}].
Also I believe you need to specify fully qualified class name (ie. model: 'app.model.Goo').

Legend Template - Chart

I got this template (default)
<span class="x-legend-item-marker {[values.disabled?'x-legend-inactive':'']}" style="background:{mark};"></span>{name}
that produce this :
I want to have the same template with every of it's functionnality. But, I need one more if-clause to it. I don't want an item to be 'legendarize' if it's value is 0.
Here is the complete code
{
xtype: 'container',
title: 'Chart',
iconCls: 'chart',
itemId: 'chart_Tab',
layout: {
type: 'fit'
},
items: [
{
xtype: 'polar',
itemId: 'pie',
colors: [
'#115fa6',
'#94ae0a',
'#a61120',
'#ff8809',
'#ffd13e',
'#a61187',
'#24ad9a',
'#7c7474',
'#a66111',
'#222222',
'#115ea6',
'#94cc0a',
'#b61120',
'#dd8809',
'#11d13e',
'#a68887',
'#94df9d',
'#7f74f4',
'#112341',
'#abcdef1'
],
store: 'relativedata',
series: [
{
type: 'pie',
label: {
textBaseline: 'middle',
textAlign: 'center',
font: '9px Helvetica'
},
labelField: 'strName',
labelOverflowPadding: 0,
xField: 'numValue'
}
],
interactions: [
{
type: 'rotate'
}
],
listeners: [
{
fn: function(element, eOpts) {
var relStore = Ext.getStore('relativedata');
var eleStore = Ext.getStore('element');
var relModel;
var eleModel;
relStore.removeAll();
//Convert to CO2 qty
for(var i = 0; i< eleStore.getCount();i++)
{
eleModel = eleStore.getAt(i);
relModel = Ext.create(APPNAME + '.model.RelativeElement');
relModel.set('strName',eleModel.get('strName'));
relModel.set('numValue', eleModel.get('numValue')*eleModel.getFactor());
relStore.add(relModel);
}
relStore.sync();
//Hide arrows-legend
this._series[0]._label.attr.hidden=true;
},
event: 'painted'
}
],
legend: {
xtype: 'legend',
docked: 'bottom',
itemId: 'pie_legend',
itemTpl: [
'<span class="x-legend-item-marker {[values.disabled?\'x-legend-inactive\':\'\']}" style="background:{mark};"></span>{name}'
],
maxItemCache: 100,
store: 'element'
}
}
]
}
I ask for help because i'm not that good with templates. I would not dare say I understand everything of the default one actually.
I'm back! Yet, nobody's calling me slim shaddy for that... Unluckily!
So, to answer your initial question, the template you need would be something like the following:
// Configuration of the chart legend
legend: {
// Finally, we can use the value field to customize our templates.
itemTpl: [
'<tpl if="value != 0">', // <= template condition
'<span class="x-legend-item-marker {[values.disabled?\'x-legend-inactive\':\'\']}" style="background:{mark};"></span>{name}',
'</tpl>'
]
// ...
}
Unfortunately, as I've said in my previous comment, quick debugger inspection shows that this value variable, or any equivalence, is not available at the time this template is applied.
Now I'm going to give you a detailed explanation about how I was able to overcome this vexation. In part because this is such an involved hack that you'd better know what you're doing if you decide to apply it, and in part because you'll learn a lot more by witnessing the fishing techniques than by being given the fish right away -- in this case, the fish is not available for retail anyway. And also in a large part, I must confess, because I like to be lyrical about things I've put some energy in, and it's late, and my defenses against self congratulation have gotten a bit weak...
So, looking at Ext.chart.Legend's code shows that there's nothing to be done there, it's just a somewhat lightweight extension of Ext.dataview.Dataview. As such it must have a store bounded to it, which, obviously (and unfortunately), is not the one bound to the chart to provide its data.
Another judicious breakpoint (in the Legend's setStore method) shows that this store comes from Ext.chart.AbstractChart, and in the code of this class we can see two things: a dedicated legend store is created in the constructor, and chart series implement a method to feed this store, namely provideLegendInfo.
We're getting closer to our goal. What we need to do is add a value field to the legend store, and have our serie provide the data for this field. Great!
The wise approach now would be to implement these modifications with the minimal amount of replication of Ext's code... But after having spent an inconsiderate amount of time trying to do that with no luck, I'll just settle for wildly overriding these two methods, and giving the advice to put a big bold warning to check that the code of these methods doesn't change with the next versions of Touch:
if (Ext.getVersion().isGreaterThan('2.2.1')) {
// Give yourself a big warning to check that the overridden methods' code
// bellow has not changed (see further comments).
}
With that out of the way, let's go to the point without any further consideration for future generations.
That is, first we add a value field to the legend store:
/**
* Adds a value field to legend store.
*/
Ext.define(null, {
override: 'Ext.chart.AbstractChart'
// Berk, what a lot of code replication :( Let's just hope that this method's code
// won't change in the future...
,constructor: function() {
var me = this;
me.itemListeners = {};
me.surfaceMap = {};
me.legendStore = new Ext.data.Store({
storeId: this.getId() + '-legendStore',
autoDestroy: true,
fields: [
'id', 'name', 'mark', 'disabled', 'series', 'index'
// Adding my value field
,'value'
]
});
me.suspendLayout();
// For whatever reason, AbstractChart doesn't want to call its superclass
// (Ext.draw.Component) constructor and, by using callSuper, skips directly to
// Ext.Container's one. So well... I respect, but I must do it old school since
// callSuper would go to Ext.draw.Component from here.
Ext.Container.prototype.constructor.apply(this, arguments);
// me.callSuper(arguments);
me.refreshLegendStore();
me.getLegendStore().on('updaterecord', 'onUpdateLegendStore', me);
me.resumeLayout();
}
}, function() {
// Post-create functions are not called for overrides in touch as they are
// in ExtJS? Hmm... That would have been the perfect place to issue a big
// warning in case the version has changed, but we'll live with it :(
});
And, second, we make our chart serie feed that value. From your code, I can deduce that you're working with a pie chart, so I'm only giving the code for that, as a matter of illustration... But, if you've followed until here, it should be trivial to implement it for other kind of series. Anyway, here's the code:
/**
* Overrides `provideLegendInfo` to add the value to the legend records.
*
* Here again, let us all cross our fingers very hard, hoping for Sencha's team to not decide
* to add their own extra fields too soon...
*/
Ext.define(null, {
override: 'Ext.chart.series.Pie'
,provideLegendInfo: function(target) {
var store = this.getStore();
if (store) {
var items = store.getData().items,
labelField = this.getLabelField(),
field = this.getField(),
hidden = this.getHidden();
for (var i = 0; i < items.length; i++) {
target.push({
name: labelField ? String(items[i].get(labelField)) : field + " " + i,
mark: this.getStyleByIndex(i).fillStyle || this.getStyleByIndex(i).strokeStyle || 'black',
disabled: hidden[i],
series: this.getId(),
index: i
// Providing actual data value to the legend record
,value: items[i].get(field)
});
}
}
}
});
Let's sum it up. We've got two overrides and a custom template. We could hope that we'd be done by now. But here's what we get:
So, the DataView is adding some markup of its own around the itemTpl's markup. Well, well, well... At this point, I'm tired of tracking Ext's internals and, fortunately (for once!), I envision a quick patch for this. So that is without an hesitation that I'm throwing this CSS rule in:
.x-legend-item:empty {
display: none;
}
And finally we're done. I guess my line of thought and code might be a little tricky to replicate, so let me provide you with a definitive proof that this all works.
In this demo, there is a "metric four" that has a value of 0.
{
'name': 'metric four',
'data': 0
}
But you won't see it. Because that was the point of all this, wasn't it?

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.

Updated records don't match Store.sync()

I am having trouble when updating my store
Ext.define("ManageShows.model.Show", {
extend: "Ext.data.Model",
config: {
idProperty: 'id',
fields: [
{ name: 'id' , type: 'int'},
{ name: 'name', type: 'string' }
],
validations: [
{ type: 'presence', field: 'name', message: 'Please enter a name for this show.'}
]
}
});
When i try to sync() i get this error
[WARN][Ext.data.Operation#process] Unable to match the updated record that came back from the server.
The List holding the data then contains duplicate entries of the first element in the list. But if i refresh the page my list displays correctly with the updated term.
I don't know what part of the records that don't match. The only different i can see is that Store.id = "ext-record-4" which seem to be auto generated and i am not saving this value in my database but Store.data.id = {id from database}.
How can i find what is not matching?
EDIT:
Important note is that i get the error 7 times in a row and i have 8 entries. If i updated the first entry then the changes work and is displayed corrently
I stopped my servlet from returning json data after a sync() call. This wasn't need because sencha touch updates the data locally automatically

Resources