I'm writing a custom component in Ext JS which needs a store as a part of its configuration. I want to keep this store in the component's ViewModel, and I also want it to be bindable. Currently I have code like this:
Ext.define('CustomComponent', {
extend: 'Ext.container.Container',
xtype: 'customcomponent',
viewModel: {
type: 'customcomponent'
},
config: {
store: 'ext-empty-store'
},
initComponent: function() {
var store = Ext.getStore(this.getConfig('store'));
this.lookupViewModel().set('myStore', store);
this.callParent(arguments);
}
});
Ext.define('CustomComponentViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.customcomponent',
data: {
myStore: null
}
});
This is not ideal for a number of reasons:
The store config option isn't bindable.
The store is contained in the data of the ViewModel, not the stores. This means it can't be accessed via the getStore method of the ViewModel.
To make the store bindable, I could write code like this:
Ext.define('CustomComponent', {
extend: 'Ext.container.Container',
xtype: 'customcomponent',
viewModel: {
type: 'customcomponent'
},
config: {
store: 'ext-empty-store'
},
publishes: 'store'
});
Ext.define('CustomComponentViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.customcomponent',
data: {
store: null
},
formulas: {
myStore: function(get) {
return Ext.getStore(get('store'));
}
}
});
This is not ideal either, though:
The ViewModel is polluted with the store config, which is not necessarily an Ext.data.Store. It could be a store's ID or a config object. Essentially, it's an implementation detail, and I'd like to keep it out of the ViewModel where it will be inherited by every child component.
The store is still not a part of the ViewModel's store configuration and so is still not accessible via getStore.
Essentially, I'm looking for a way to set up my View and ViewModel (and ViewController, if it would help) so that I can meet these three criteria:
The store config on the view is bindable.
The store config option is not kept in the ViewModel, or is somehow prevented from polluting the ViewModels of the component's children.
The store in the ViewModel is accessible via getStore.
Is it possible to meet all three of these criteria simultaneously? If not, what is the most canonical way to transform a bindable store config into a ViewModel store? The Ext JS source code doesn't use the View-ViewModel architecture to define components, so I can't just look at what they do.
I believe you could use the store like this.
Ext.define('CustomComponentViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.customcomponent',
formulas: {
myStore: function(get) {
return Ext.getStore(get('store'));
}
},
stores: {
myStore: {
fields: [
{
name: 'id'
},
{
name: 'name'
},
{
name: 'description'
}
]
}},
});
Ext.define('CustomComponent', {
extend: 'Ext.container.Container',
xtype: 'customcomponent',
viewModel: {
type: 'customcomponent'
},
config: {
bind: {
store: '{myStore}'
}
}
});
After reading the Ext JS 6.0.2 source code more thoroughly, I have come to the conclusion that meeting all three criteria I want is not possible without overriding or subclassing Ext.app.ViewModel. I'll go through the criteria one by one, and then discuss what I think is the best current practice.
Criteria
The store config must be bindable
This is actually accomplished by default; all config options are bindable. What I had meant when I said the store must be bindable was "binding to the store config will update the store in my ViewModel". The value of the store config will be sent to the ViewModel if the publishes option is set appropriately, i.e. publishes: 'store'. This will send the raw value of the store config back to the ViewModel as store, though, which is not what we want. I think the cleanest way to work around this is via the applyConfigName template method provided for each config option. Here's an example:
Ext.define('CustomComponent', {
extend: 'Ext.container.Container',
xtype: 'customcomponent',
viewModel: {
type: 'customcomponent'
},
config: {
store: 'ext-empty-store'
},
publishes: 'store',
applyStore: function(store) {
return Ext.getStore(store);
}
});
The applyStore function will be applied to the value of store before it gets set, transforming it from a store ID or config object into an actual store. This brings us to the second criterion:
The store config option is not kept in the ViewModel
By using the applyStore template method we can keep the store config option out of our ViewModel. (Indeed, it even keeps it out of our View!) Instead the value of store which is published to our ViewModel is the actual store which it defines. Unfortunately, however, publishing config values adds them to the data object of the ViewModel, not the stores object. This brings us to the final criterion:
The store in the ViewModel is accessible via getStore
This criterion is currently impossible to satisfy. The reason is two-fold:
getStore retrieves only stores added via the stores config option
In Ext JS 6.0.2, getStore retrieves stores by looking them up in the storeInfo private property of Ext.app.ViewModel. The storeInfo property is itself set via the setupStore private method, which is called only on each member of the stores config.
The stores config option only allows config objects
Unlike the store config of Ext.grid.Panel, the stores config option of Ext.app.ViewModel only allows the values of each store to be config objects; Store IDs and actual stores are not permitted. Setting the value to a binding directly seems like it works, e.g.:
Ext.define('CustomComponentViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.customcomponent',
data: {
dataStore: null
},
stores: {
storeStore: '{dataStore}'
}
});
But in actuality, the storeStore is a new Ext.data.ChainedStore which is configured with source: dataStore. This means that setting filters and sorters on storeStore will not affect dataStore, which is probably not what was desired.
The combination of these two restrictions means it is impossible to set a store in a ViewModel which can be retrieved with getStore.
So what should I do?
I think that with the available options, the best practice is to give up the ability to retrieve the store with getStore. While this is an annoying inconsistency, it is less likely to cause subtle bugs the way that allowing an implicit ChainedStore might. This gives us our final code:
Ext.define('CustomComponent', {
extend: 'Ext.container.Container',
xtype: 'customcomponent',
viewModel: {
type: 'customcomponent'
},
config: {
store: 'ext-empty-store'
},
publishes: 'store',
applyStore: function(store) {
return Ext.getStore(store);
}
});
Ext.define('CustomComponentViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.customcomponent',
data: {
store: null
}
});
Related
I'm having a real hard time understanding how to access the actual bound store instance on a component, like a ComboBox or Grid. It appears that when you use their getStore method, it returns a different instance.
In my example, I have a custom grid class, and in its initComponent, I want to add a load listener. Unfortunately, the load listener never gets hit, but the bound store's load listener does. You can see the listener is getting set up before the store is loaded, so that's not the issue. It looks like the store instance in initComponent is a memory store, so the bound store's instance is not synced to the component at this point.
I know I can have a load listener on the bound store, but this is a custom grid class, and for any custom grid class, I want there to be a listener set up... so I don't want to have to do this all over the place in my code... I just want it in one class, as they're all going to have the same logic carried out.
Can someone explain to me why my custom grid class's load listener is not getting hit, and what can I do to fix this? Can I somehow access the bound store instance?
Ext.application({
name: 'Fiddle',
launch: function() {
Ext.define('MyGrid', {
extend: 'Ext.grid.Panel',
xtype: 'myGrid',
initComponent: function() {
alert('initComponent')
this.callParent();
this.getStore().on('load', this.onLoad, this);
},
onLoad: function() {
alert('grid store loaded');
}
});
Ext.define('MyViewController', {
extend: 'Ext.app.ViewController',
alias: 'controller.myview',
onLoadBoundStore: function() {
alert('loaded bound');
}
})
Ext.define('MyViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.myview',
stores: {
myStore: {
fields: ['name', 'value'],
autoLoad: true,
proxy: {
type: 'ajax',
url: 'data1.json',
reader: {
type: 'json'
}
},
listeners: {
load: 'onLoadBoundStore'
}
}
}
});
Ext.define('MyView', {
extend: 'Ext.form.Panel',
renderTo: Ext.getBody(),
controller: 'myview',
viewModel: {
type: 'myview'
},
items: [{
title: 'blah',
xtype: 'myGrid',
reference: 'myComboBox',
bind: {
store: '{myStore}'
},
columns: [{
text: 'Name',
dataIndex: 'name'
}, {
text: 'Value',
dataIndex: 'value'
}]
}]
});
Ext.create('MyView')
}
});
Can someone explain to me why my custom grid class's load listener is
not getting hit, and what can I do to fix this?
You are not initially providing any store to your grid (the store config option is required by the way). Hence the grid is creating its own, empty store as coded here:
store = me.store = Ext.data.StoreManager.lookup(me.store || 'ext-empty-store');
That empty store is the one that gets your onLoad listener attached. It is not getting hit because the store is never loaded.
Can I somehow access
the bound store instance?
Yes, use this:
this.lookupViewModel().getStore('myStore')
You can attach your listener to the bound store instead of the empty one and see the difference. The grid's empty store eventually gets replaced by the bound one (this is why you see data in the grid, after all), but this happens after initComponent is executed. You can track the moment of setting the bound store to the grid by overriding setStore method.
I know I can have a load listener on the bound store, but this is a
custom grid class, and for any custom grid class, I want there to be a
listener set up... so I don't want to have to do this all over the
place in my code... I just want it in one class, as they're all going
to have the same logic carried out.
Since you are using MVVC it is recommended to stick to the pattern and keep views declaring view aspects only. Listeners should be declared and handled in controllers (not even in viewmodels like in your example). Otherwise, if you continue sticking to pre Ext JS 5 style and put dynamic stuff in views (i.e. the grid in your case) there is no point in using MVVC.
This is so incredibly hacky that I don't even want to post it as an answer, but I'm going to just for posterity sake. Building on Drake's answer, I figured out how to get my store using what was passed in the initial config of my grid class. The bind comes in with the braces, so that's why I'm doing a replace on it. Like I said, this probably should never be used, but it's something.
initComponent: function() {
var store = this.lookupViewModel().getStore(this.config.bind.store.replace(/[{}]/g, ''));
store.on('load', this.onLoad, this);
this.callParent();
},
Just to add one more option to the mix, you can listen to the reconfigure event on the grid, as that's what fires after binding a store is complete... so something like this would work:
Ext.application({
name: 'Fiddle',
launch: function() {
Ext.define('MyGrid', {
extend: 'Ext.grid.Panel',
xtype: 'myGrid',
initComponent: function() {
alert('initComponent')
this.on('reconfigure', this.onReconfigureGrid, this);
this.callParent();
},
onReconfigureGrid: function(grid, store, columns, oldStore, oldColumns, eOpts) {
store.on('load', this.onLoad, this);
},
onLoad: function() {
alert('grid store loaded');
}
});
Ext.define('MyViewController', {
extend: 'Ext.app.ViewController',
alias: 'controller.myview',
onLoadBoundStore: function() {
alert('loaded bound');
}
})
Ext.define('MyViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.myview',
stores: {
myStore: {
fields: ['name', 'value'],
autoLoad: true,
proxy: {
type: 'ajax',
url: 'data1.json',
reader: {
type: 'json'
}
},
listeners: {
load: 'onLoadBoundStore'
}
}
}
});
Ext.define('MyView', {
extend: 'Ext.form.Panel',
renderTo: Ext.getBody(),
controller: 'myview',
viewModel: {
type: 'myview'
},
items: [{
title: 'blah',
xtype: 'myGrid',
reference: 'myComboBox',
bind: {
store: '{myStore}'
},
columns: [{
text: 'Name',
dataIndex: 'name'
}, {
text: 'Value',
dataIndex: 'value'
}]
}]
});
Ext.create('MyView');
}
});
Is it possible to retrieve a store from within a ViewController?
I'm trying to dynamicaly populate a toolbar with buttons from a store, but I'm not sure how to access the store from the controller.
Any hints appreciated.
My Extjs version is 5.1.
View:
Ext.define('EurocampingsCMS.view.Foo', {
extend: 'Ext.toolbar.Toolbar',
alias: 'widget.foo',
requires: [
'EurocampingsCMS.controller.Foo'
],
controller: 'foo',
});
ViewController:
Ext.define('MyApp.controller.Foo', {
extend: 'Ext.app.ViewController',
alias: 'controller.foo',
control: {
'#': {
beforerender: 'onBeforeRender'
}
},
onBeforeRender: function (toolbar, eOpts) {
//How to get store here??
store.each(function (record) {
toolbar.add({
xtype: 'button',
itemId: record.get('localeId'),
text: record.get('label')
});
});
}
});
Best would be to fully utilize MVVM architecture and define the store in viewmodel stores object. Then it is easy:
onBeforeRender: function (toolbar, eOpts) {
var store = this.getViewModel().getStore('yourstorekey');
}
Note: This is possible only if the lifetime of the store is same as the lifetime of the view. If you need to access a global store with a longer lifetime then assign it a storeId and get it with:
var store = Ext.getStore('theStoreId');
Ext.getStore('storeIdHere') is one way to do it
I have store that I would like to initialize from a database but I couldn't find a standard init method for the Ext.data.Store. I found a couple of examples with the StoreManager component, but I think that's not what I'm looking for. I have an MVC structure for my app and I'd like to keep it, I only want to initialize my store's data field using a method I define. Could someone explain how to do so?
I either understand you wrong or your question is straight forward. You configure a store with a model like this. That's all. You may just chose a provider(reader/writer) that fit your needs.
// Set up a model to use in our Store
Ext.define('User', {
extend: 'Ext.data.Model',
fields: [
{name: 'firstName', type: 'string'},
{name: 'lastName', type: 'string'},
{name: 'age', type: 'int'},
{name: 'eyeColor', type: 'string'}
]
});
Ext.define('YourMVCNameSpace.data.UserStore', {
extend: 'Ext.data.Store',
constructor: function (config) {
config = Ext.Object.merge({}, config);
var me = this;
// do what you need with the given config object (even deletes) before passing it to the parent contructor
me.callParent([config]);
// use me forth on cause the config object is now fully applied
},
model: 'User',
proxy: {
type: 'ajax',
url: '/users.json',
reader: {
type: 'json',
root: 'users'
}
},
autoLoad: true
});
Note that the reader will expect a Json result like this:
{"total": 55, "users":["...modeldata.."]}
and is referring to a url like
http://localhost/YourAppDomain//users.json
Place the store as 'User' within the controller store array and retrieve it within the Controller by calling getUserStore() or directly from the Ext.StoreMgr using Ext.StoreMgr.lookup('User');
Note that by convention the Controller (MVC) will override any storeId you set on the store and will just use the name.
Im stack with ext js 4 at the very beginning. Im trying to get the current user data when starting the application using store. But Im not getting any data from the store, even the store.count return 0.
I found many description how to create store, but not how to access the data in it. I managed to get the data using Ext ajax request, but i think would be better using store and i cant avoid them..
My model:
Ext.define('MyApp.model.User', {
extend: 'Ext.data.Model',
fields: [
'id',
'username',
'email'
]
});
My store looks like:
Ext.define('MyApp.store.User.CurrentUser', {
extend: 'Ext.data.Store',
requires: 'MyApp.model.User',
model: 'MyApp.model.User',
autoLoad: true,
proxy: {
type: 'ajax',
method: 'POST',
url: Routing.generate('admin_profile'),
reader: {
type: 'json',
root: 'user'
}
}
});
The returned json:
{
"success":true,
"user":[{
"id":1,
"username":"r00t",
"email":"root#root.root"
}]
}
And the application:
Ext.application({
name: 'MyApp',
appFolder: '/bundles/myadmin/js/app',
models: ['MyApp.model.User'],
stores: ['MyApp.store.User.CurrentUser'],
//autoCreateViewport: true,
launch: function() {
var currentUser=Ext.create('MyApp.store.User.CurrentUser',{});
/*
Ext.Ajax.request({
url : Routing.generate('admin_profile'),
method: 'POST',
success: function(resp) {
var options = Ext.decode(resp.responseText).user;
Ext.each(options, function(op) {
var user = Ext.create('MyApp.model.User',{id: op.id,username:op.username,email:op.email});
setUser(user);
}
)}
});
*/
currentUser.load();
alert(currentUser.count());
}
});
The problem itself isn't that the store does not contain data, the problem is that the store load is asyncronous therefore when you count the store records, the store is actualy empty.
To 'fix' this, use the callback method of the store load.
currentUser.load({
scope : this,
callback: function(records, operation, success) {
//here the store has been loaded so you can use what functions you like
currentUser.count();
}
});
All the sencha examples have the proxies in the store, but you should actually put the proxy in the model, so that you can use the model.load method. the store inherits the model's proxy, and it all works as expected.
it looks like model.load hardcodes the id though (instead of using idProperty), and it always has to be an int, as far as I can tell.
good luck!
I am used to ExtJS 3.X, but am struggling with ExtJS 4.
I want to create an extension of a grid and be able to use an instance of the grid with the xtype. As far as im aware, I have to set the alias as widget.xtypename but its not working for me.
var MyGrid = Ext.define('mygrid', {
extend:'Ext.grid.Panel',
alias: 'widget.mygrid',
// rest of grid...
});
Ext.create('Ext.window.Window', {
title:'My Window',
items:[{
xtype:'mygrid'
}]
})
The Error I am getting in Chrome console is Cannot create an instance of unrecognized alias: widget.mygrid
Some help would be much appretiated
Ext.define('MyApp.Grid',{
extend: 'Ext.grid.GridPanel',
alias: 'widget.mygrid',
.......
.......
}
Now you can use as xtype:'mygrid'
The problem may be that you are attempting to instantiate an object that uses your new class, immediately following the call to Ext.define. Remember that Ext.define is an asynchronous process. Anything that needs to instantiate components should be in an onReady handler, or in Ext.application (launch), or in initComponent in a component class, or in init in a controller class, for these locations are guaranteed to be called only after all the defines have completed.
Specifying an alias beginning with "widget." will allow you to use it wherever xtype is expected. In your simple example, you might try doing the following:
var MyGrid = Ext.define('mygrid', {
extend:'Ext.grid.Panel',
alias: 'widget.mygrid',
// rest of grid...
}, function() {
Ext.create('Ext.window.Window', {
title:'My Window',
items:[{
xtype:'mygrid'
}]
});
});
This will instantiate your window within the callback after the define completes.
If you are using working on a MVC application, you can fix this by adding the view information to your controller. In your controller you need to specify the view in an array named views.. Here is an example:
Ext.define('MyApp.controller.Users', {
extend: 'Ext.app.Controller',
views: ['users.List'],
...
In your case you may need to define views:['mygrid'].
If you are not using MVC architecture, you will need to use the Ext.require and specify your grid class exists.
I believe you need to add a xtype to your config:
var MyGrid = Ext.define('mygrid', {
extend:'Ext.grid.Panel',
alias: 'widget.mygrid',
xtype: 'mygrid',
// rest of grid...
});
After researching more, I would expect the alias to be all you need. Are you defining an initComponent function? Below is an example from Sencha:
Ext.define('App.BookGrid', {
extend: 'Ext.grid.Panel',
// This will associate an string representation of a class
// (called an xtype) with the Component Manager
// It allows you to support lazy instantiation of your components
alias: 'widget.bookgrid',
// override
initComponent : function() {
// Pass in a column model definition
// Note that the DetailPageURL was defined in the record definition but is not used
// here. That is okay.
this.columns = [
{text: "Author", width: 120, dataIndex: 'Author', sortable: true},
{text: "Title", flex: 1, dataIndex: 'Title', sortable: true},
{text: "Manufacturer", width: 115, dataIndex: 'Manufacturer', sortable: true},
{text: "Product Group", width: 100, dataIndex: 'ProductGroup', sortable: true}
];
// Note the use of a storeId, this will register thisStore
// with the StoreManager and allow us to retrieve it very easily.
this.store = new App.BookStore({
storeId: 'gridBookStore',
url: 'sheldon.xml'
});
// finally call the superclasses implementation
App.BookGrid.superclass.initComponent.call(this);
}
});
This one also works:
Ext.define('Path.to.ClassUsingSubcomponent', {
...
requires: ['Path.to.YourSubcomponent'],
...
}