ExtJS 4 I don't understand models + associations - extjs

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

Related

ExtJS Model with repeating structures

What patterns have been successful when mapping a model with a few repeating structures.
I have to model and create a grid view a record with has a few columns which are basically the same repeating structure.
Ext.define('Ep.mm.model.MyComparison', {
var derived = function(val,)
extend: 'Ext.data.Model',
fields : [
{name:'id',type:'string'},
{name:'count',type:'number'},
{name:'special',type:'number'},
{name:'A_a',type:'number'},
{name:'A_b',type:'number'}
{name:'A_derived',renderer = function(val,meta,rec){return rec.get('A_b')-rec.get('A_a')}}
{name:'B_a',type:'number'},
{name:'B_b',type:'number'}
{name:'B_derived',renderer = function(val,meta,rec){return rec.get('B_b')-rec.get('B_a')}}
{name:'C_a',type:'number'},
{name:'C_b',type:'number'}
{name:'C_derived',renderer = function(val,meta,rec){return rec.get('C_b')-rec.get('C_a')}}
]
});
I am likely to end up with Server models/stores that differ only in the number of times this structure repeats (A) (A,B) (A,B,C,D). Is there a way to use a common model for each of A,B,C,D and just re-use the common model as the type. This is something akin to a 'nested' structure.
Ext.define('MyType',{
extend :'Ext.data.Model',
fields : [
{name : 'a', type:'number'},
{name : 'b', type:'number'},
{ name : 'derived',
convert : function(value, record) {
return record.get('a') - record.get('b');
}
}
]
}
Then MyComparison would be defined as
Ext.define('Ep.mm.model.MyComparison',{
extend : 'Ext'data.Model',
fields : [
{name:'A', type:'MyType'}
{name:'B', type:'MyType'}
{name:'C', type:'MyType'}
]
});
I know this isn't quite right, but this is sort of the functionality I am trying to create.
Perhaps using the 'hasOne' association type?
Yes, you can extend your own model classes:
Ext.define('Base', {
extend: 'Ext.data.Model',
fields: ['name']
});
Ext.define('Child', {
extend: 'Base',
fields: ['age']
});
var o = new Child({
name: 'Foo',
age: 100
});
console.log(rec.get('name'), rec.get('age'));
As a sidenote, models don't have renderers. You're probably looking for the convert function on the field, here.
I'd say you want functionality of a field type:
Ext.data.Types.A_DERIVED = {
convert: function(v, data) {
return rec.get('A_b')-rec.get('A_a');
},
type: 'number'
};
Then use it as a type of A_derived field:
Ext.define('Ep.mm.model.MyComparison', {
extend: 'Ext.data.Model',
fields: [
{name:'id',type:'string'},
{name:'A_a',type:'number'},
{name:'A_b',type:'number'},
{name:'A_derived', type: 'Ext.data.Types.A_DERIVED'}
]
});
Afterthoughts:
- Maybe you would like to aggregate all A_* data into one field? See documentation for an example.
- There is a trouble when you want to use the same mechanism with B_derived because there is no way of supplying B into convert function. A dirty hack might be to set the defaultValue to B and to bind the field to null data. Then v would be read from defaultValue.

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.

Extjs4: How to share data between multiple stores or models?

I'm new to Ext and I'm struggling to figure out Models Stores and Proxies.
The server returns one large JSON object.
For example.
{
"responseHeader":{
"status":0,
"QTime":12,
"params":{
"facet":"true",
"facet.limit":"40"
}
},
"response":{
"numFound":3806,
"start":0,
"docs":[
{
//Lots of fields
"id":"1234",
...
//Some array properties
"testfield":[
"",
""
],
...
}
]
},
"facet_counts":{
"facet_queries":{
"email":3806
},
"facet_fields":{
"emailaddress":{
},
"subject":{
"candles":136,
"filter":130
},
"fromemail":{
},
//...
},
"facet_dates":{ },
"facet_ranges":{}
},
"highlighting":{
"some doc id":{
"emailtext":[ " Tel.: blah blah <em>blah</em>" ],
"combined":[ "<em>Email</em> To: blah blah blah" ]
}
}
}
I don't want to load this data more than once, I want to grab data from this object, for example the docs object, and put it into a grid. Then pull out another part to put into a selectbox.
How do I load this data once, yet create models and stores to give to grids and selectboxes, from it?
From what I read the proxy holds the servers response? So I tried creating a proxy out side of a store. Thinking I could use the same proxy with more than one store.
var myProxy1 = Ext.create('Ext.data.proxy.Proxy', {
type: 'ajax',
url : '../test',
reader: {
type: 'json',
root: 'responseHeader'
}
});
But when I pass myProxy1 to a store
Ext.define('Test', {
extend: 'Ext.data.Model',
fields: [
{name: 'status', type: 'int'},
{name: 'QTime', type: 'int'},
{name: 'param', type: 'auto'}
]
});
var myStore = Ext.create('Ext.data.Store', {
model: 'Test',
proxy: myProxy1,
autoLoad: true,
listeners:{
load: function( ths, records, successful, operation, eOpts ){
debugger;
}
}
});
It doesn't work. The load event is never fired. No data is loaded. I can see that the proxy made the request, I see the response from the server, but it doesn't load.
If I put the proxy inline it loads.
var myStore = Ext.create('Ext.data.Store', {
model: 'Test',
proxy:{
type: 'ajax',
url : '../test',
reader: {
type: 'json',
root: 'responseHeader'
}
},
autoLoad: true,
listeners:{
load:function( ths, records, successful, operation, eOpts ){
debugger;
}
}
});
I was thinking I could have one proxy, attach it to multiple stores, and just change the reader on it before I load the store.
You are pretty much there, and although I'm pretty sure you are understanding it all, for the benefit of others allow me to give an extended answer and a slightly modified solution to your problem.
Definitions:
The Model - primarily defines the fields a record has.
A Store - holds a collection of records.
A Proxy - facilitates server communication through a chosen method (Ajax, Direct, etc.) and maps CRUD (Create/Read/Update/Destroy) operations when such result from a change in the associated store or model.
A Reader - Tells a proxy how to interpret the data the server returns.
A Grid or Combobox - can display store records.
You scenario is not an uncommon one - while by default ExtJS loads each store separately, it is likely an application would prefer various stores to be loaded at once through a single read call; for example, when rendering one store-dependent component is dependent on another store.
Your code is not far off from achieving this, but here is how I do it. In effect, when a 'master' (Tasks) store loads, the server response also carries the data of a 'slave' (Tags) store, which is then manually loaded to that 'slave' store.
The 'slave' store (notice autoload: false and no read operation):
Ext.define('DL.store.Tags', {
extend: 'Ext.data.Store',
model: 'DL.model.Tag',
// Notice the tags are actually returned when the tasks are loaded and loaded into this store by the TasksStore.
autoLoad: false,
autoSync: true,
proxy: {
type: 'direct',
api: {
create: Tags.Create,
update: Tags.Update,
destroy: Tags.Destroy,
},
reader: {
type: 'json',
root: 'tags'
}
},
});
Then the 'master' store:
Ext.define('DL.store.Tasks', {
extend: 'Ext.data.TreeStore',
model: 'DL.model.Task',
autoLoad: true,
autoSync: true,
root: {
text: 'Root',
id: null,
expanded: true
},
proxy: {
type: 'direct',
api: {
create: Tasks.Create,
read: Tasks.Read,
update: Tasks.Update,
destroy: Tasks.Destroy,
},
},
onProxyLoad: function( aOperation )
{
// A read request for tasks will also return the user tags.
// So feed these tags into their store.
var iResult = aOperation.response.result,
iTagsStore = Ext.StoreManager.lookup( 'Tags' );
if ( Ext.isDefined( iResult.tags ) )
iTagsStore.loadRawData( iResult );
// We need this line for "Tasks" store to load its own data
this.callParent(arguments);
}
});
Basically all it does is it takes part of the server response and loads it manually to the 'slave' store.
The PHP server side code (for tasks read operation) involves:
return array(
'success' => true,
'children' => $iNodes,
'tags' => $iTags
);
Where children is the reader's root of the 'master' store, and tags is additional data that is then loaded into the 'slave' store.
I hope you can work how how to apply these concepts to your code.

Backbone-relational between two models

By using Backbone-relational I would like to have the model task in this way:
task = {
id: 1
assigned_id: 2
name: 'read a book',
user: userModel
};
I did try this way (1) but the result is the following (2).
Any ideas?
(1)
var User = Backbone.RelationalModel.extend({
// urlRoot
});
var Task = Backbone.RelationalModel.extend({
relations: [
{
type: Backbone.HasOne,
key: 'user',
relatedModel: User
}
],
// urlRoot
});
(2)
task = {
id: 1
assigned_id: 2
name: 'read a book',
user: null // null instead of having something related to user model
};
Not sure what your the exact JSON is for your Task model, so I'm guessing here.
Backbone-relational is expecting either a fully nested model:
task = {
id: 1
assigned_id: 2
name: 'read a book',
user: {
name: 'Fred Rogers',
id: 42,
occupation: 'Hero'
}
};
Or a string/number, which it will assume to be the id of the related model:
task = {
id: 1
assigned_id: 2
name: 'read a book',
user: 42
};
I'm guessing you're hitting the second case, based on the null value you're getting for the user model.
When backbone-relational instantiates an instance of a model, and the related model is a "key" string/number, it will search its internal store of models to try to find a matching model. If it finds it, it sets that model as the value for the user property.
If it cannot find the model, it stashes the key in the model's relevant relation property model._relations[n].keyContents, and sets the user value to null.
It is at this point that you would use the fetchRelated function to get the related model from the datastore/API.
So, try calling task.fetchRelated() to get the related user model:
task.fetchRelated('user');

Resources