With ExtJS5, Sencha added to the framework a new way of building web apps based on the MVVM pattern. A key role in this pattern is the ViewModel
which coordinates the changes between Model
data and the View
declaratively through data binding. I like the ViewModel
concept because it requires less writing, has declarative code, and View
s are much cleaner. Based on my experiences, following these tips and thoughts should be very useful for both newcomers and devs familiar with the framework.
- Always destroy your bindings in the
destroy
method of your view. In the example below, the grid is a child of a parent view which has aViewModel
. But if the child is removed from the parent the binding will still be called and can lead to unexpected surprises. So you could destroy them manually, or automatically by adding aViewModel
to the viewExt.define('MyApp.view.MyGridTab', { extend: 'Ext.grid.Panel', xtype: 'mygrid', columns: [], beforeRender: function () { var me = this; var vm = this.lookupViewModel(); me.callParent(arguments); me.myFieldBinding = vm.bind('{obj.myfield}', me.onMyFieldBindingChange,me); }, onMyFieldBindingChange: function(myfield) { // Do some stuff with myfield build the search query, then load the store var searchRequest = this.buildSearchRequest(myfield); this.searchByRequest(searchRequest) }, searchByRequest: function (searchRequest) { var me = this; var store = this.getStore(); if (searchRequest) { store.getProxy().setExtraParam('search_request', searchRequest); store.load(); } }, destroyViewBindings: function () { if (this.myFieldBinding) { this.myFieldBinding.destroy(); this.myFieldBinding = null; } }, destroy: function () { this.destroyViewBindings(); this.callParent(); } });
- If possible avoid creating bindings in
initComponent
. The component may not be rendered but instantiated, like in tabpanelsโ case, and the binding is called every time a change is detected. This occurs even if it is not needed, which decreases app performance. Depending on the context, you could use the solution below or create your bindings inbeforerender
. - Only activate bindings when needed. In the above example, the store of the grid tab should load the data only when the tab is visible and/or activated. Currently the
ViewModel
can’t be disabled and enabled as needed. By default, it is initialized to run in thebeforerender
phase untildestroy
time. But like above, sometimes we need to optimize this so the performance is not affected by the bindings. So you may find it useful to have a more generic and flexible solution like the one below. The only way to disable bindings running for now, is to remove them from theViewModel
->Scheduler
by destroying them, which costs less than destroying and creating theViewModel
. For a demo check this fiddle https://fiddle.sencha.com/#fiddle/1bcf and https://github.com/vadimpopa/Ux.mixin.CustomBindable - Use explicit binding in formulas to speed up
formula
parsingExt.define('MyApp.view.MyModel', { extend: 'Ext.app.ViewModel', formulas: { // Ok isPdfUseDisabled: function (get) { var country = get('patentData.country'); if (country) { return !MyApp.util.isPdfCollection(country); } return false; }, // Much better isPdfUseDisabled: { bind: '{patentData.country}', get: function (country) { if (country) { return !MyApp.util.isPdfCollection(country); } return false; } }, // or multiple binding something: { bind: { x: '{foo.bar.x}', y: '{bar.foo.thing.zip.y}' }, get: function (data) { return data.x + data.y; } } } });
- Careful with stores data binding (with a
memory proxy
) because of the nature ofstore.setData
, thegrid
is not cleared if the bound data is changed toundefined
ornull
Ext.define('MyApp.view.MyModel', { extend: 'Ext.app.ViewModel', formulas: { myCollectionsData: { bind: '{myData.anObject.myCollections}', get: function (myCollections) { return myCollections || []; } } }, store: { myCollections: { autoDestroy: true, fields: [], proxy: { type: 'memory', reader: { type: 'json' } }, // This bind is going to leave your grid un-cleared because of the how // store.setData is coded. data: '{myData.anObject.myCollections}' // Much better, if to use a formula to accept default data data: '{myCollectionsData}' } } });
- As a naming rule, use the
onBindingChange
pattern when naming your bound handlers. Same goes for the bind references, seemyFieldBinding
me.myFieldBinding = vm.bind('{obj.myfield}', me.onMyFieldBindingChange,me);
- Don’t overload your
ViewModels
with too much code or fragment the code. The balance should be between cleaning up and parent polluting.ViewModels
can inherit from each other in a parent – child relation, so you can still access the parent’s data in the child. Also, overloading means more dependencies to track.Ext.define('MyApp.view.MyChildModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.mychild', formulas: { myCollectionsData: { bind: '{myData.anObject.myCollections}', get: function (myCollections) { return myCollections || []; } } } }); Ext.define('MyApp.view.MyParentModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.myparent', data: { myData: null } }); Ext.define('MyApp.view.MyWindow', { extend: 'Ext.window.Window', viewModel: { type: 'myparent' }, items: [{ xtype: 'mygrid' }] }); Ext.define('MyApp.view.MyGrid', { extend: 'Ext.grid.Panel', xtype: 'mygrid' viewModel: { type: 'mychild' } });
- Don’t repeat yourself. Sometimes you have to call one property a few times, like with
dude
in below example. Creating a formula to shorten bindings path won’t add much benefit. Instead it will create additional bindings for the formula itself which means additional overhead for nothing. Here’s a fiddle to experiment with this https://fiddle.sencha.com/#fiddle/1bmkviewModel: { data: { simpsons: { dude: { name: 'jon simpson', role: 'developer', phone: '3434343' } } }, formulas: { dude: function(get) { return get('simpsons.dude'); } } }, ........ items: [ { xtype: 'component', bind: { html: 'hey {dude.name}' } }, { xtype: 'component', bind: { html: 'is your phone {dude.phone} ?' } } ]
- A quick and easy custom
Ext.form.field.Display
could be done by using aBind
and aRenderer
xtype: 'displayfield', fieldLabel: 'Custom display field', bind: { value: '{fooArray}' }, customFieldTpl: new Ext.XTemplate( '', '<span class="search-field-link" data-value="{xindex}">{name}</span>', '' ), renderer: function (value, field) { var html = field.customFieldTpl.apply(value); return field.htmlEncode ? Ext.util.Format.htmlEncode(html) : html; }
Conclusion
ViewModels
are very powerful but you have to be careful and keep the beast under control so you donโt lose performance. Keep in mind that the more bindings you have, the longer it will take to run, so use them wisely. Follow me on GitHub and gist for more tips and extensions.
Vadim Popa
Related Posts
-
How To Use Sencha Ext.Config
In a previous post, my colleague Stan explained the foundation of the Sencha Config System…
-
How To Use Sencha Ext.Config
In a previous post, my colleague Stan explained the foundation of the Sencha Config System…