Related
I use ExtJs 4.1.3 and have the following scenario:
1. ExtJs grid which use some store with model
//model
Ext.define('MyApp.model.Tariff', {
extend: 'Ext.data.Model',
fields: [
{
name: 'country',
type: 'string'
},
{
name: 'tariff',
type: 'string'
}
]
});
//grid
var tariffsGrid = Ext.create('Ext.grid.Panel', {
id: 'tariffsGridId',
title: 'Tariffs',
store: tariffsStore,
columns: [
{ text: 'Country', dataIndex: 'country' },
{ text: 'Tariff', dataIndex: 'tariff' }
]
});
2. Http api which returns json
{
countries: [...],
tariffs: [..]
}
I want to get this json with ajax request and transform it according to some logic to the array of model MyApp.model.Tariff.
How I can correctly implement this ?
I have two ideas:
Make custom reader (extend: 'Ext.data.reader.Json') and use it for proxy in store
Override load method of the store and use Ext.Ajax.request
Which design is better and please post example of the implementation.
I have 2 models... a Parent and a Child... a Parent hasMany Child models. In the Child, I have a field that is created by using its data (in this example, it's just converting and returning 1). In the Parent, I have two conversions going on... one uses its data, and the 2nd one depends on the Child's data. However, when creating the Parent model, it appears the Child association isn't fully baked yet, but when I go to update the Parent model with a new value, the Child association is there.
Basically, what I want to know is, when you see the Total2 console firing, I want Test to be populated. How do I force the associations to be read before using my conversion functions in the parent? And ideally, if there's a change in the Child, the dependent conversion function in Parent will automatically fire... I realize that most likely won't be possible, but it would be an incredible added bonus.
Here's my example.
app.js
Ext.application({
name : 'Fiddle',
launch : function() {
var store = Ext.create('Ext.data.Store', {
model: 'Namespace.model.Parent',
autoLoad: true,
listeners: {
load: onLoadStore
}
});
var grid = Ext.create('Ext.grid.Panel', {
title: 'associations',
store: store,
renderTo: Ext.getBody(),
columns: [{
text: 'Primary Key',
dataIndex: 'PrimaryKey'
}]
});
function onLoadStore(store, records, successful, eOpts) {
var firstGroups = store.first().getGroupsStore();
console.log('before', firstGroups.getCount(), firstGroups.isLoaded());
firstGroups.first().set('GroupName', 'blah');
store.first().set('blank', 1)
}
}
});
Parent
Ext.define('Namespace.model.Parent', {
extend: 'Ext.data.Model',
alias: 'model.parent',
requires: [
'Ext.data.field.Integer',
'Ext.data.field.String',
'Namespace.model.Child'
],
idProperty: "PrimaryKey",
fields: [{
type: 'string',
name: 'PrimaryKey',
critical: true
}, {
type: 'string',
name: 'Title'
}, {
type: 'int',
name: 'Rating',
defaultValue: 2
}, {
type: 'int',
name: 'Total',
depends: ['Rating'],
convert: function(value, record) {
console.log('Total', record)
return record.get('Rating') * 2;
}
}, {
name: 'Total2',
type: 'int',
depends: ['groupsMapping', 'blank'],
// depends on child's Test property
convert: function(value, record) {
var groupsMapping = record.get('groupsMapping');
if (groupsMapping) {
for (var i = 0; i < groupsMapping.length; i++) {
console.log('Total2', groupsMapping[i].GroupName, groupsMapping[i].Test);
}
}
return 0;
}
}, {
name: 'groupsMapping',
type: 'auto',
mapping: 'Groups'
}, {
name: 'blank',
type: 'int'
}],
hasMany: [{
model: 'Namespace.model.Child',
associationKey: 'Groups',
name: 'getGroupsStore'
}],
proxy: {
type: 'ajax',
url: 'data1.json'
}
});
Child
Ext.define('Namespace.model.Child', {
extend: 'Ext.data.Model',
alias: 'model.child',
fields: [{
type: 'int',
name: 'GroupInt',
critical: true
}, {
type: 'string',
name: 'GroupName'
}, {
name: 'Test',
type: 'int',
depends: ['GroupName'],
convert: function() {
console.log('Test, parent depends on this')
return 1;
}
}],
proxy: {
type: 'memory'
}
});
I've hit the same problem and took me some time in order to realise that the associations are not available on conversion time
Like in your example, I was sending the child records together with the parent ones (nested) and have 'manually' built a fake store using child model & parent field child data in order to do my math for the conversion / calculation, which did the trick:
{
name: 'validationScore',
depends: ['childRecords'],
type: 'number',
persist: false,
convertOnSet: true,
convert: function(val, rec){
// here we have access only to direct record field, and not the association association !
// we can use data directly in the calculation
children = rec.get('childRecords');
// or build a fake store in order to easily parse them
children = Ext.create('Ext.data.Store', {
model: 'Namespace.model.ChildModel',
data: rec.get('childRecords')
})
// ... calculate whatever
return result;
}
}
But now my 'new' challenge is to figure it out a (good) solution to update the converted value upon child records' changes ...
I did something similar to qdev, except I listened on the actual association's store. There's something funky about the constructor though... if I didn't put that timeout in there, then I would get an error with trying to access the first record in the store load... might be a framework bug, or I'm doing something terrible. The important code is the constructor and calculate method... in the app, I just have a 1 second timeout on changing the child data... you can see the grid's row data change from 3 to 102. Here's an updated example:
constructor: function(config) {
this.callParent(arguments);
var me = this;
setTimeout(function() {
var groupsStore = me.getGroupsStore();
if (groupsStore) {
groupsStore.on('update', me.calculateChanges, me);
groupsStore.on('datachanged', me.calculateChanges, me);
}
me.calculateChanges();
}, 1);
},
calculateChanges: function() {
console.log('calculating');
var groupsStore = this.getGroupsStore();
if (groupsStore) {
var total2 = 0;
groupsStore.each(function(group) {
total2 += group.get('Test');
});
this.set('Total2', total2);
}
},
I am trying to filter in a combobox under Sencha Touch framework but I am not loading correct the records in the list.
My code is:
onTemplatesFilterChoosen: function(field, newValue, oldValue){
var store = Ext.getStore('Customers'),
templatesStore = store.getAt(0).templates(), //get the associated store
text = field.getRecord().data.text;
templatesStore.clearFilter();
//here the code to filter main store based in records of the associated store
}
But it is not working correctly and my store.getCount()for example is 0, showing a empty list.
In my main model I am adding a new model based in the properties of the filter called "CustomerTemplateModel"
associations: [
{
type: 'hasMany',
associatedModel: 'x.customer.model.CustomerTemplateModel',
ownerModel: 'x.customer.model.CustomerModel',
associationKey: 'templates',
autoLoad: true,
filterProperty: 'value',
name: 'templates'
}
]
And here the model associated:
Ext.define('x.customer.model.CustomerTemplateModel', {
extend: 'Ext.data.Model',
requires:[
],
config: {
useCache: false,
rootProperty: 'templates',
fields: [
{
name: 'text',
type: 'string'
},
{
name: 'value',
type: 'string'
}
]
}
});
What am I doing wrong??
Thank you in advance.
I'm struggling with my application right in the beginning.
this.getScoresStore().on('load', function(score, records) {
var view = Ext.getCmp('scoreView');
view.down('form').loadRecord(records[0].data);
console.log(view.down('form').getRecord());
console.log(view.down('form').getValues());
});
After the store is loaded, I add the records to the form. Console says it's added, however the fields keep beeing empty.
Object { playerOne="301", playerTwo="301" }
Object { playerOne="", playerTwo="" }
Anyone got Ideas what could be wrong?
Controller:
Ext.define('Darts.controller.Scores', {
extend: 'Ext.app.Controller',
views: [
'score.View',
'score.Hit'
],
stores: [
'Scores'
],
models: [
'Score'
],
init: function() {
this.getScoresStore().on('load', function(score, records) {
var view = Ext.getCmp('scoreView');
view.down('form').loadRecord(records[0].data);
console.log(view.down('form').getRecord());
console.log(view.down('form').getValues());
});
this.control({
'scoreView' : {
afterrender: this.formRendered
}
});
},
formRendered: function(obj) {
console.log(obj.down('form').getRecord());
console.log('form rendered');
}
});
Views:
Ext.define('Darts.view.score.Hit' ,{
extend: 'Ext.panel.Panel',
alias : 'widget.scoreHit',
title : 'Hits',
score : 'Scores',
initComponent: function() {
this.items = [
{
xtype: 'form',
items: [
{
xtype: 'textfield',
name : 'playerTwo',
fieldLabel: 'Player 1'
}
]
}
];
this.callParent(arguments);
}
});
Ext.define('Darts.view.score.View' ,{
extend: 'Ext.panel.Panel',
alias : 'widget.scoreView',
id : 'scoreView',
title : 'Player Scores',
score : 'Scores',
initComponent: function() {
this.items = [
{
xtype: 'form',
items: [
{
xtype: 'numberfield',
name : 'playerOne',
fieldLabel: 'Player 1'
}, {
xtype: 'textfield',
name : 'playerTwo',
fieldLabel: 'Player 2'
}
]
}
];
this.buttons = [
{
text: 'Start Game',
action: 'start'
}
];
this.callParent(arguments);
}
});
Store
Ext.define('Darts.store.Scores', {
extend: 'Ext.data.Store',
model : 'Darts.model.Score',
autoLoad: true,
proxy: {
type: 'ajax',
api: {
read: 'data/scores.json',
update: 'data/updateScores.json'
},
reader: {
type: 'json',
root: 'scores',
successProperty: 'success'
}
}
});
Model:
Ext.define('Darts.model.Score', {
extend: 'Ext.data.Model',
fields: ['playerOne', 'playerTwo']
});
Data:
{
success: true,
scores: [
{id: 1, playerOne: '301', playerTwo: '301'}
]
}
I've tried numberfields, textfields as well as changing the data fom with ' to without ' and mixed.... nothing seems to help me.
The fields are rendered before store is loaded (test output still in the code)
I'm really out of ideas here and I've seen many topics, but none fits to my problem or fixes my problem. The form fields always keeps beeing empty.
I think your issue is that you need to pass a Model record into loadRecord method not the underlying data. So try changing line 3 to
view.down('form').loadRecord(records[0]);
As a side note, it's a bit odd to load the entire store just to get at a single record.
You might want to explore Model.load( id, {callback config} ) way of loading exact record that you need.
I have a list of users and if I click on an item in this list, a window opens. This is the same window for each user, and it's possible to have several window open at the same time. The window shows user informations so for this components I have the same store and the same model.
But if I load data in a specific window, I load the same data in all other open windows.
Ext.define('Cc.view.absence.Grid', {
extend: 'Ext.grid.Panel',
alias: 'widget.absencegrid',
border:false,
initComponent: function() {
Ext.apply(this, {
store: Ext.create('Ext.data.Store', {
model: 'Cc.model.Absence',
autoLoad: false,
proxy: {
type: 'ajax',
reader: {
type: 'json'
}
}
}),
columns: [
{header: 'Du', dataIndex: 'startdate', flex: 3, renderer: this.formatDate},
{header: 'Au', dataIndex: 'enddate', flex: 3, renderer: this.formatDate},
{header: 'Exercice', dataIndex: 'year', align: 'center', flex: 1},
{header: 'Statut', xtype:'templatecolumn', tpl:'<img src="../images/status-{statusid}.png" alt="{status}" title="{status}" />', align: 'center', flex: 1},
{header: 'Type d\'absence', dataIndex: 'absencetype', align: 'center', flex: 2},
{header: 'Commentaires', dataIndex: 'comment', flex: 6}
],
dockedItems: [{
xtype: 'toolbar',
dock: 'top',
items: [
{ xtype: 'tbfill'},
{ xtype: 'button', text: 'Rafraichir', action: 'refresh', iconCls: 'item-rafraichir' }
]
}]
});
this.callParent(arguments);
},
formatDate: function(date) {
if (!date) {
return '';
}
return Ext.Date.format(date, 'l d F Y - H:i');
}
});
my controller :
Ext.define('Cc.controller.Absences', {
extend: 'Ext.app.Controller',
models: ['Absence', 'AbsenceHistory'],
views: [],
refs: [
{ ref: 'navigation', selector: 'navigation' },
{ ref: 'tabPanel', selector: 'tabpanel' },
{ ref: 'absencePanel', selector: 'absencepanel' },
{ ref: 'refreshButton', selector: 'absencepanel button[action=refresh]'},
{ ref: 'absenceGrid', selector: 'absencegrid' },
{ ref: 'absenceHistory', selector: 'absencehistory' },
],
init: function() {
this.control({
'absencepanel button[action=refresh]': {
click: this.onClickRefreshButton
},
'absencegrid': {
selectionchange: this.viewHistory
},
'absencegrid > tableview': {
refresh: this.selectAbsence
},
});
},
selectAbsence: function(view) {
var first = this.getAbsenceGrid().getStore().getAt(0);
if (first) {
view.getSelectionModel().select(first);
}
},
viewHistory: function(grid, absences) {
var absence = absences[0],
store = this.getAbsenceHistory().getGrid().getStore();
if(absence.get('id')){
store.getProxy().url = '/absence/' + absence.get('id') +'/history';
store.load();
}
},
onClickRefreshButton: function(view, record, item, index, e){
var store = this.getAbsenceGrid().getStore();
store.load();
},
});
other controller which create only one instance of absence.Panel :
Ext.define('Cc.controller.Tools', {
extend: 'Ext.app.Controller',
stores: ['Tools', 'User' ],
models: ['Tool'],
views: [],
refs: [
{ ref: 'navigation', selector: 'navigation' },
{ ref: 'tabPanel', selector: 'tabpanel' },
{ ref: 'toolList', selector: 'toollist' },
{ ref: 'toolData', selector: 'toollist dataview' }
],
init: function() {
this.control({
'toollist dataview': {
itemclick: this.loadTab
},
});
},
onLaunch: function() {
var dataview = this.getToolData(),
store = this.getToolsStore();
dataview.bindStore(store);
},
loadTab: function(view, record, item, index, e){
var tabPanel = this.getTabPanel();
switch (record.get('tab')) {
case 'absences':
if(Ext.getCmp('absence-panel')){
tabPanel.setActiveTab(Ext.getCmp('absence-panel'));
}
else {
var panel = Ext.create('Cc.view.absence.Panel',{
id: 'absence-panel'
}),
store = panel.getGrid().getStore();
panel.enable();
tabPanel.add(panel);
tabPanel.setActiveTab(panel);
store.getProxy().url = '/person/' + this.getUserId() +'/absences';
store.load();
}
break;
default:
break;
}
},
getUserId: function(){
var userStore = this.getUserStore();
var id = userStore.first().get('id')
return id;
}
});
the other controller which create many instances of absence.Panel :
Ext.define('Cc.controller.Agents', {
extend: 'Ext.app.Controller',
stores: ['Agents'],
models: ['Agent', 'Absence', 'AbsenceHistory'],
views: ['agent.List', 'agent.Window', 'absence.Panel', 'absence.Grid', 'absence.History'],
refs: [
{ ref: 'agentList', selector: 'agentlist' },
{ ref: 'agentData', selector: 'agentlist dataview' },
{ ref: 'agentWindow', selector: 'agentwindow' },
],
init: function() {
this.control({
'agentlist dataview':{
itemclick: this.loadWindow
},
});
},
onLaunch: function() {
var dataview = this.getAgentData(),
store = this.getAgentsStore();
dataview.bindStore(store);
},
loadWindow: function(view, record, item, index, e){
if(!Ext.getCmp('agent-window'+record.get('id'))){
var window = Ext.create('Cc.view.agent.Window', {
title: 'À propos de '+record.get('firstname')+' '+record.get('lastname'),
id: 'agent-window'+record.get('id')
}),
tabPanel = window.getTabPanel();
absencePanel = window.getAbsencePanel();
store = absencePanel.getGrid().getStore();
absencePanel.enable();
tabPanel.add(absencePanel);
tabPanel.setActiveTab(absencePanel);
store.getProxy().url = '/person/' + record.get('id') +'/absences';
store.load();
}
Ext.getCmp('agent-window'+record.get('id')).show();
}
});
and the absence.Panel view, container of absence.Grid :
Ext.define('Cc.view.absence.Panel', {
extend: 'Ext.panel.Panel',
alias: 'widget.absencepanel',
title: 'Mes absences',
iconCls: 'item-outils',
closable: true,
border: false,
disabled: true,
layout: 'border',
initComponent: function() {
this.grid = Ext.create('Cc.view.absence.Grid', {
region: 'center'
});
this.history = Ext.create('Cc.view.absence.History', {
region: 'south',
height: '25%'
});
Ext.apply(this, {
items: [
this.grid,
this.history
]
});
this.callParent(arguments);
},
getGrid: function(){
return this.grid;
},
getHistory: function(){
return this.history;
}
});
Yes. Here is more details explanation of what I am doing. I hope you have read the forum completely. There is still one information that we are not clear about. That is the exact use of using "stores" property in controller. IMHO Sencha team should explain the MVC in much more detail with complex examples.
Yes, what you have newly posted is correct. when you have create new view, create a new instance of the store! Now, from the forum discussions, people argue about MVC. I would definitly go with steffenk . What we are doing here is injecting a new instance of store to my view. And I am ignoring the stores property of the controller.
Here is an example:
This is my view. Its a panel (with user profile information) that I display on my tabpanel :
Ext.define('Dir.view.profile.View' ,{
extend: 'Ext.panel.Panel',
alias : 'widget.profileview',
title : 'Profile',
profileId: 1, // default and dummy value
initComponent: function() {
// configure necessary stuff, I access my store etc here..
// console.log(this.profileStore);
this.callParent(arguments);
},
// Other view methods goes here
});
Now, look at my controller:
Ext.define('Dir.controller.Profile', {
extend: 'Ext.app.Controller',
//stores: ['Profile'], --> Note that I am NOT using this!
refs: [
{ref:'cp',selector: 'centerpane'}
],
views: ['profile.View'],
init: function() {
// Do your init tasks if required
},
displayProfile: function(selectedId) {
// create a new store.. pass your config, proxy url etc..
var store = Ext.create('Dir.store.Profile',{profileId: selectedId});
console.log('Display Profile for ID ' + selectedId);
// Create instance of my view and pass the new store
var view = Ext.widget('profileview',{profileId: selectedId,profileStore: store});
// Add my new view to centeral panel and display it...
this.getCp().add(view);
this.getCp().setActiveTab(view);
}
});
My displayProfile() is called from some event listeners (Menu, Tree etc) and it passes a id. My controller using this id to setup a new store and view. I hope the above code gives you a clear picture of what I said yesterday.
In your controller, you will have to add Ext.require('Dir.store.Profile'); so that ExtJS know you have such a store. This is because we are not making use of stores property.
Now, you can want to reuse these created stores elsewhere, you can add them to the StoreManager. With this, you can access your created stores at any place, add and remove stores. But this comes with a overhead of you managing the instances of the store.
Why do you share the same instance of the store with different views? when you have a new view, create a new instance of the store. This will prevent updated data appearing on other windows when one is refreshed.