Description
This example shows how to use MVVM architecture, new in ExtJS 5, to bind a record selected in the grid or data view to form and template panel. It also shows how to synchronize selection in grid and data view.
I have designed this example for same look and functionality as its Ext 4 counterpart. Although they look very similar and do virtually the same thing, the approach is entirely different. It is so different that I will dedicate a post to explaining the differences. Stay tuned.
Main Features
- store with inline data defined in view model
- one view model and one view controller used at the container level
- making selection in the grid or data view publishes the selected record to the view model
- the selected record bound to form and template panel
- the record is editable both in the grid and form, changes updated immediately in bound components
- buttons’ disabled state bound to the record (form) or store (grid) dirty condition
- shows two-way binding and deep binding
- no MVC controller
- no data binding related listeners
Example Files (relative to example root)
The example has been initially generated with sencha generate app so the following list contains only added or edited files relevant to the example:
app/Application.js app/view/Main.js app/view/MainModel.js app/view/MainController.js app/view/PersonGrid.js app/view/PersonForm.js app/view/PersonView.js app/view/PersonPanel.js
Source Code
app/Application.js:
// vim: sw=4:ts=4:nu:nospell:fdc=4
/*global Ext:true */
/*jslint browser: true, devel:true, sloppy: true, white: true, plusplus: true */
/*
This file is part of ExtJS 5 Data Binding Example
Copyright (c) 2014, Jozef Sakalos, Saki
Package: ExtJS 5 Data Binding Example
Author: Jozef Sakalos, Saki
Contact: https://learnfromsaki.com/contact
Date: 02. June 2014
Commercial License
Developer, or the specified number of developers, may use this file in any number
of projects during the license period in accordance with the license purchased.
Uses other than including the file in a project are prohibited.
See https://learnfromsaki.com/licensing for details.
*/
/**
* Main Application implementation
*/
Ext.define('Xbind.Application', {
extend: 'Ext.app.Application'
,name: 'Xbind'
,views:['Main']
,launch: function () {
var ct = Ext.fly('example-ct') || Ext.getBody();
Ext.widget({
xtype:'main'
,renderTo:ct
,height:480
,width:740
,frame:true
,title:'ExtJS 5 Data Binding Example by Saki'
,glyph:0xf0eb
});
// only for easy access to important instances in devel
Xbind.main = Ext.ComponentQuery.query('main')[0];
Xbind.vm = Xbind.main.getViewModel();
} // eo function launch
});
// eof
app/view.Main.js:
// vim: sw=4:ts=4:nu:nospell:fdc=4
/*global Ext:true */
/*jslint browser: true, devel:true, sloppy: true, white: true, plusplus: true */
/*
This file is part of ExtJS 5 Data Binding Example
Copyright (c) 2014, Jozef Sakalos, Saki
Package: ExtJS 5 Data Binding Example
Author: Jozef Sakalos, Saki
Contact: https://learnfromsaki.com/contact
Date: 02. June 2014
Commercial License
Developer, or the specified number of developers, may use this file in any number
of projects during the license period in accordance with the license purchased.
Uses other than including the file in a project are prohibited.
See https://learnfromsaki.com/licensing for details.
*/
/**
* Table-like view that puts together all other views.
* Instantiated and rendered from launch method of Application
* This is the only view that uses view model and view controller
*/
Ext.define("Xbind.view.Main", {
extend: 'Ext.panel.Panel'
,alias:'widget.main'
,requires:[
'Ext.layout.container.Fit'
,'Xbind.view.PersonGrid'
,'Xbind.view.PersonForm'
,'Xbind.view.PersonPanel'
,'Xbind.view.PersonView'
,'Xbind.view.MainModel'
,'Xbind.view.MainController'
]
,controller:'main'
,viewModel:{
type:'main'
}
,layout:{
type:'hbox'
,align:'stretch'
}
,defaults:{
flex:1
}
,items:[{
// left column
xtype:'container'
,layout:{
type:'vbox'
,align:'stretch'
}
,defaults:{
flex:1
,margin:5
,border:true
}
,items:[{
// top cell of left column
xtype:'persongrid'
,glyph:0xf0ce
},{
// bottom cell of left column
title:'DataView'
,glyph:0xf009
,autoScroll:true
,bind:{
title:'<b>{currentPerson.name}</b>'
}
,items:[{
xtype:'personview'
}]
}]
},{
// right column
xtype:'container'
,layout:{
type:'vbox'
,align:'stretch'
}
,defaults:{
flex:1
,margin:5
,border:true
}
,items:[{
// top cell of right column
title:'Form'
,xtype:'personform'
,glyph:0xf044
},{
// bottom cell of right column
title:'Data Panel'
,xtype:'personpanel'
,glyph:0xf0f6
}]
}] // eo items
});
// eof
app/view/MainModel.js:
// vim: sw=4:ts=4:nu:nospell:fdc=4
/*global Ext:true */
/*jslint browser: true, devel:true, sloppy: true, white: true, plusplus: true */
/*
This file is part of ExtJS 5 Data Binding Example
Copyright (c) 2014, Jozef Sakalos, Saki
Package: ExtJS 5 Data Binding Example
Author: Jozef Sakalos, Saki
Contact: https://learnfromsaki.com/contact
Date: 02. June 2014
Commercial License
Developer, or the specified number of developers, may use this file in any number
of projects during the license period in accordance with the license purchased.
Uses other than including the file in a project are prohibited.
See https://learnfromsaki.com/licensing for details.
*/
/**
* # Main view model class definition
* Holds and manages data and stores for the whole application
*/
Ext.define('Xbind.view.MainModel',{
extend:'Ext.app.ViewModel'
,alias:'viewmodel.main'
,data:{
currentPerson:null
} // eo data
,formulas:{
// used to enable/disable form buttons
dirty:{
bind:{
bindTo:'{currentPerson}'
,deep:true
}
,get:function(data) {
return data ? data.dirty : false;
}
} // eo dirty
// used to enable/disable grid buttons
,storeDirty:{
// bind is not used for data but for triggering the get
bind:{
bindTo:'{currentPerson}'
,deep:true
}
,get:function() {
return this.getStore('persons').isDirty()
}
} // eo storeDirty
} // eo formulas
,stores:{
persons:{
model:'Person'
,data:[
{id:1, fname:'John', lname:'Lennon', age:74}
,{id:2, fname:'Paul', lname:'McCartney', age:72}
,{id:3, fname:'George',lname:'Harrison', age:71}
,{id:4, fname:'Ringo', lname:'Starr', age:74}
]
,isDirty:function() {
var dirty = this.getModifiedRecords().length;
dirty = dirty || this.getNewRecords().length;
dirty = dirty || this.getRemovedRecords().length;
return !!dirty;
} // eo function isDirty
} // eo persons store
// chained store for data view
,personsChained:{
source:'{persons}'
}
} // eo stores
});
// eof
app/view/MainController.js:
// vim: sw=4:ts=4:nu:nospell:fdc=4
/*global Ext:true */
/*jslint browser: true, devel:true, sloppy: true, white: true, plusplus: true */
/*
This file is part of ExtJS 5 Data Binding Example Package
Copyright (c) 2014, Jozef Sakalos, Saki
Package: ExtJS 5 Data Binding Example
Author: Jozef Sakalos, Saki
Contact: https://learnfromsaki.com/contact
Date: 21. June 2014
Commercial License
Developer, or the specified number of developers, may use this file in any number
of projects during the license period in accordance with the license purchased.
Uses other than including the file in a project are prohibited.
See https://learnfromsaki.com/licensing for details.
*/
/**
* # Main view controller class definition
* Implements event handlers for grid and form buttons
*/
Ext.define('Xbind.view.MainController',{
extend:'Ext.app.ViewController'
,alias:'controller.main'
// common handler for all form buttons
,onFormButton:function(btn) {
var person = this.getViewModel().get('currentPerson')
action = btn.getItemId()
;
if(person && person.isModel) {
if('reject' === action) {
person.reject()
}
if('commit' === action) {
person.commit();
// Ext bug workaround
// dirty flag is not refreshed without this
person.reject();
}
}
} // eo function onFormButton
// common handler for all grid buttons
,onGridButton:function(btn) {
var action = btn.getItemId()
,vm = this.getViewModel()
,store = vm.getStore('persons')
,record
;
if('add' === action) {
record = store.insert(0, {})[0];
vm.set('currentPerson', record);
}
if('reject' === action) {
store.rejectChanges()
}
if('commit' === action) {
store.commitChanges();
// Ext bug workaround
// dirty flag is not refreshed without this
record = vm.get('currentPerson');
record.commit();
record.reject();
}
} // eo function onAddPerson
});
// eof
app/view/PersonGrid.js:
// vim: sw=4:ts=4:nu:nospell:fdc=4
/*global Ext:true */
/*jslint browser: true, devel:true, sloppy: true, white: true, plusplus: true */
/*
This file is part of ExtJS 5 Data Binding Example
Copyright (c) 2014, Jozef Sakalos, Saki
Package: ExtJS 5 Data Binding Example
Author: Jozef Sakalos, Saki
Contact: https://learnfromsaki.com/contact
Date: 02. June 2014
Commercial License
Developer, or the specified number of developers, may use this file in any number
of projects during the license period in accordance with the license purchased.
Uses other than including the file in a project are prohibited.
See https://learnfromsaki.com/licensing for details.
*/
/**
* # Person grid class definition
* Grid with cell editing bound to view model data
*/
Ext.define('Xbind.view.PersonGrid',{
extend:'Ext.grid.Panel'
,alias:'widget.persongrid'
,uses:[
'Ext.form.field.Text'
,'Ext.form.field.Number'
]
,requires:[
'Ext.grid.plugin.CellEditing'
]
,publishes:['currentPerson']
,bind:{
currentPerson:'{currentPerson}'
,store:'{persons}'
,title:'<b>{currentPerson.name}</b>'
}
,config:{
currentPerson:null
}
// update selection when currentPerson changes
,updateCurrentPerson:function(current, previous) {
var sm = this.getSelectionModel();
if(current) {
sm.select(current);
}
if(previous) {
sm.deselect(previous)
}
} // eo function updateCurrentPerson
// update currentPerson when selection changes
,listeners:{
scope:'this'
,select:'onPersonSelect'
} // eo listeners
// select event handler
,onPersonSelect:function(grid, person) {
this.setCurrentPerson(person);
} // eo function onPersonSelect
,plugins:[{
ptype:'cellediting'
,clicksToEdit:2
,pluginId:'cellediting'
}] // eo plugins
// buttons in header to save vertical space
,header:{
title:'Person Grid'
,padding:'4 9 5 9'
// button handlers are implemented in MainController
,items:[{
text:'New'
,xtype:'button'
,itemId:'add'
,glyph:0xf067
,handler:'onGridButton'
},{
tooltip:'Reject All'
,xtype:'button'
,itemId:'reject'
,handler:'onGridButton'
,disabled:true
,bind:{
disabled:'{!storeDirty}'
}
,glyph:0xf0e2
,margin:'0 0 0 5'
},{
tooltip:'Commit All'
,xtype:'button'
,itemId:'commit'
,handler:'onGridButton'
,disabled:true
,bind:{
disabled:'{!storeDirty}'
}
,glyph:0xf00c
,margin:'0 0 0 5'
}]
} // eo header
,columns:[{
text:'First Name'
,dataIndex:'fname'
,editor:{
xtype:'textfield'
,bind:'{currentPerson.fname}'
}
},{
text:'Last Name'
,flex:1
,dataIndex:'lname'
,editor:{
xtype:'textfield'
,bind:'{currentPerson.lname}'
}
},{
text:'Age'
,dataIndex:'age'
,width:120
,editor:{
xtype:'numberfield'
,bind:'{currentPerson.age}'
}
}] // eo columns
});
// eof
app/view/PersonForm.js:
// vim: sw=4:ts=4:nu:nospell:fdc=4
/*global Ext:true */
/*jslint browser: true, devel:true, sloppy: true, white: true, plusplus: true */
/*
This file is part of ExtJS 5 Data Binding Example
Copyright (c) 2014, Jozef Sakalos, Saki
Package: ExtJS 5 Data Binding Example
Author: Jozef Sakalos, Saki
Contact: https://learnfromsaki.com/contact
Date: 02. June 2014
Commercial License
Developer, or the specified number of developers, may use this file in any number
of projects during the license period in accordance with the license purchased.
Uses other than including the file in a project are prohibited.
See https://learnfromsaki.com/licensing for details.
*/
/**
* # Person form class definition
* Form fields and buttons disabled state are bound to view model data
*/
Ext.define('Xbind.view.PersonForm', {
extend:'Ext.form.Panel'
,alias:'widget.personform'
,requires:[
'Ext.form.field.Number'
]
,config:{
currentPerson:null
}
,bind:{
currentPerson:'{currentPerson}'
,title:'<b>{currentPerson.name}</b>'
}
// buttons in header to save the vertical space
,header:{
title:'Person Form'
,padding:'4 9 5 9'
,items:[{
text:'Reject'
,xtype:'button'
,itemId:'reject'
,handler:'onFormButton'
,glyph:0xf0e2
// disabled until currentPerson dirty
,disabled:true
,bind:{
disabled:'{!dirty}'
}
},{
text:'Commit'
,xtype:'button'
,itemId:'commit'
,handler:'onFormButton'
,glyph:0xf00c
,margin:'0 0 0 5'
// disabled until currentPerson dirty
,disabled:true
,bind:{
disabled:'{!dirty}'
}
}]
} // eo header
,bodyPadding:10
,defaultType:'textfield'
,defaults:{
anchor:'100%'
,selectOnFocus:true
}
// form fields disabled until we have a currentPerson
,items:[{
fieldLabel:'First Name'
,disabled:true
,bind:{
value:'{currentPerson.fname}'
,disabled:'{!currentPerson}'
}
},{
fieldLabel:'Last Name'
,disabled:true
,bind:{
value:'{currentPerson.lname}'
,disabled:'{!currentPerson}'
}
},{
fieldLabel:'Age'
,xtype:'numberfield'
,disabled:true
,bind:{
value:'{currentPerson.age}'
,disabled:'{!currentPerson}'
}
}] // eo items (form fields)
});
// eof
app/view/PersonView.js:
// vim: sw=4:ts=4:nu:nospell:fdc=4
/*global Ext:true */
/*jslint browser: true, devel:true, sloppy: true, white: true, plusplus: true */
/*
This file is part of ___ Package
Copyright (c) 2014, Jozef Sakalos, Saki
Package: ___
Author: Jozef Sakalos, Saki
Contact: https://learnfromsaki.com/contact
Date: 21. June 2014
Commercial License
Developer, or the specified number of developers, may use this file in any number
of projects during the license period in accordance with the license purchased.
Uses other than including the file in a project are prohibited.
See https://learnfromsaki.com/licensing for details.
*/
/**
* # Person data view class definition
*/
Ext.define('Xbind.view.PersonView',{
extend:'Ext.view.View'
,alias:'widget.personview'
,publishes:['currentPerson']
,bind:{
store:'{personsChained}'
,currentPerson:'{currentPerson}'
}
,config:{
currentPerson:null
}
// update selection when currentPerson changes
,updateCurrentPerson:function(current, previous) {
var sm = this.getSelectionModel();
if(current) {
sm.select(current);
}
if(previous) {
sm.deselect(previous);
}
}
// update currentPerson when selection changes
,listeners:{
scope:'this'
,select:'onPersonSelect'
// Ext bug workaround
,beforecontainerclick:function(){
return false;
}
} // eo listeners
// select event handler
,onPersonSelect:function(view, person) {
this.setCurrentPerson(person);
} // eo function onPersonSelect
,itemSelector:'div.person-item'
,selectedItemCls:'selected'
,itemTpl:[
'<div class="person-item">'
,'<strong>{fname} {lname}</strong> ({age})'
,'</div>'
].join('')
});
// eof
app/view/PersonPanel.js:
// vim: sw=4:ts=4:nu:nospell:fdc=4
/*global Ext:true */
/*jslint browser: true, devel:true, sloppy: true, white: true, plusplus: true */
/*
This file is part of ExtJS 5 Data Binding Example
Copyright (c) 2014, Jozef Sakalos, Saki
Package: ExtJS 5 Data Binding Example
Author: Jozef Sakalos, Saki
Contact: https://learnfromsaki.com/contact
Date: 02. June 2014
Commercial License
Developer, or the specified number of developers, may use this file in any number
of projects during the license period in accordance with the license purchased.
Uses other than including the file in a project are prohibited.
See https://learnfromsaki.com/licensing for details.
*/
/**
* # Panel with tpl for displaying readonly person details
*/
Ext.define('Xbind.view.PersonPanel',{
extend:'Ext.panel.Panel'
,alias:'widget.personpanel'
,bodyPadding:10
,bind:{
/**
* we need to bind deep otherwise it does not update immediately
*/
data:{
bindTo:'{currentPerson}'
,deep:true
}
,title:'<b>{currentPerson.name}</b>'
}
,tpl:[
'<table>'
,'<tr><td>First Name:</td><td><strong>{fname}</strong></td></tr>'
,'<tr><td>Last Name:</td><td><strong>{lname}</strong></td></tr>'
,'<tr><td>Age:</td><td><strong>{age}</strong></td></tr>'
,'</table>'
] // eo tpl
});
// eof
[ssba_hide]
- Ext, Angular, React, and Vue - 27. June 2019
- The Site Resurgence - 11. February 2018
- Configuring ViewModel Hierarchy - 19. June 2015