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