Description
This example shows how to present one copy of data from the store/model in various forms and how to synchronize data changed at various places. The synchronization of selection between grid and data view is also shown.
Main Features
- one store with static inline data
- editable grid
- read-only data view
- form with Commit and Reject buttons
- controller synchronizes selection between grid and data view
- controller synchronizes data updated either in grid or form
- FontAwesome font icons
- one instance per application
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:
app.js app.json app/Application.js app/model/PersonModel.js app/store/PersonStore.js app/view/MainView.js app/view/PersonDataView.js app/view/PersonFormView.js app/view/PersonGridView.js app/view/PersonPanelView.js app/controller/MainController sass/src/view/PersonDataView.scss
Source Code
app.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 Complex Data Binding Example Copyright (c) 2014, Jozef Sakalos, Saki Package: Complex Data Binding Example Author: Jozef Sakalos, Saki Contact: https://learnfromsaki.com/contact Date: 02. May 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. */ /** * The application startup file */ // Scoped css is only needed if embedding in an existing page Ext.scopeCss = true; // Font family for font icons Ext.setGlyphFontFamily('FontAwesome'); // Instantiate the Application class Ext.application({ name: 'ExampleCdb' ,extend: 'ExampleCdb.Application' }); // eof
app.json:
{ "name": "ExampleCdb", "theme":"ext-theme-gray", "requires": [ ], "id": "f9b8cc3c-6786-4f46-9e78-42c4aa5ff4a7" }
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 Complex Data Binding Example Copyright (c) 2014, Jozef Sakalos, Saki Package: Complex Data Binding Example Author: Jozef Sakalos, Saki Contact: https://learnfromsaki.com/contact Date: 02. May 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. */ /** * Application file definition */ Ext.define('ExampleCdb.Application', { name: 'ExampleCdb' ,extend: 'Ext.app.Application' ,controllers:[ 'MainController' ] // instantiates the main view in the provided container or in the document body ,launch:function() { var me = this ,ct = Ext.fly('example-ct') || Ext.getBody() ; Ext.widget({ xtype:'mainview' ,renderTo:ct ,height:480 ,width:720 ,frame:true ,title:'Complex Data Binding Example by Saki' ,glyph:0xf0eb }); } // eo function launch }); // eof
app/model/PersonModel.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 Complex Data Binding Example Copyright (c) 2014, Jozef Sakalos, Saki Package: Complex Data Binding Example Author: Jozef Sakalos, Saki Contact: https://learnfromsaki.com/contact Date: 02. May 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. */ /** * Model for persons */ Ext.define('ExampleCdb.model.PersonModel', { extend:'Ext.data.Model' ,idProperty:'id' ,fields:[ {name:'id', type:'int'} ,{name:'fname', type:'string'} ,{name:'lname', type:'string'} ,{name:'age', type:'int'} ] }); //eof
app/store/PersonStore.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 Complex Data Binding Example Copyright (c) 2014, Jozef Sakalos, Saki Package: Complex Data Binding Example Author: Jozef Sakalos, Saki Contact: https://learnfromsaki.com/contact Date: 02. May 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. */ /** * Store for persons. * One instance of this store is used by both grid and data view. * Has inline static data. */ Ext.define('ExampleCdb.store.PersonStore',{ extend:'Ext.data.Store' ,model:'ExampleCdb.model.PersonModel' ,autoLoad:true ,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} ] }); // eof
app/view/MainView.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 Complex Data Binding Example Copyright (c) 2014, Jozef Sakalos, Saki Package: Complex Data Binding Example Author: Jozef Sakalos, Saki Contact: https://learnfromsaki.com/contact Date: 02. May 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 */ Ext.define("ExampleCdb.view.MainView", { extend: 'Ext.panel.Panel' ,alias:'widget.mainview' ,initComponent:function() { var me = this ,cfg = {} ; Ext.apply(cfg, { layout:{ type:'hbox' ,align:'stretch' } ,defaults:{ flex:1 } ,items:[{ // left column xtype:'container' ,layout:{ type:'vbox' ,align:'stretch' } ,defaults:{ flex:1 ,margin:5 } ,items:[{ // top cell of left column title:'Editable Grid' ,xtype:'persongridview' ,glyph:0xf0ce },{ // bottom cell of left column title:'DataView' ,glyph:0xf009 ,layout:'fit' ,items:[{ xtype:'persondataview' }] }] },{ // right column xtype:'container' ,layout:{ type:'vbox' ,align:'stretch' } ,defaults:{ flex:1 ,margin:5 } ,items:[{ // top cell of right column title:'Form' ,xtype:'personformview' ,glyph:0xf044 ,frame:true },{ // bottom cell of right column title:'Data Panel' ,xtype:'personpanel' ,glyph:0xf0f6 }] }] }); // eo apply Ext.apply(me, cfg); me.callParent(arguments); } // eo function initComponent }); // eof
app/view/PersonDataView.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 Complex Data Binding Example Copyright (c) 2014, Jozef Sakalos, Saki Package: Complex Data Binding Example Author: Jozef Sakalos, Saki Contact: https://learnfromsaki.com/contact Date: 02. May 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. */ /** * DataView to display personal data. Individual items can * be selected - selection is synchronized with the grid * by Main Controller */ Ext.define('ExampleCdb.view.PersonDataView',{ extend:'Ext.view.View' ,alias:'widget.persondataview' ,autoScroll:true ,initComponent:function() { var me = this ,cfg = {} ; Ext.apply(cfg, { store:Ext.getStore('PersonStore') ,itemSelector:'div.person-item' ,tpl:[ '<tpl for=".">' ,'<div class="person-item">' ,'<strong>{fname} {lname}</strong> ({age})' ,'</div>' ,'</tpl>' ] ,listeners:{ selectionchange:'onSelectionChange' } ,selModel:{ allowDeselect:true } }); // eo cfg Ext.apply(me, cfg); me.callParent(arguments); } // eo function initComponent // we fire a custom event on selectionchange. // This event is listened to by Main Controller ,onSelectionChange:function(selModel, selected, eOpts) { this.fireEvent('itemselectionchange', this, selected, eOpts); } // eo function onSelectionChange }); // eof
app/view/PersonFormView.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 Complex Data Binding Example Copyright (c) 2014, Jozef Sakalos, Saki Package: Complex Data Binding Example Author: Jozef Sakalos, Saki Contact: https://learnfromsaki.com/contact Date: 02. May 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. */ /** * Form for person details with Commit and Reject buttons */ Ext.define('ExampleCdb.view.PersonFormView', { extend:'Ext.form.Panel' ,alias:'widget.personformview' // no need to require as we use the class in initComponent ,uses:[ 'Ext.form.field.Number' ] ,initComponent:function() { var me = this ,cfg = {} ; Ext.apply(cfg, { defaultType:'textfield' ,defaults:{ anchor:'100%' } ,bodyPadding:10 ,items:[{ fieldLabel:'First Name' ,name:'fname' },{ fieldLabel:'Last Name' ,name:'lname' },{ fieldLabel:'Age' ,name:'age' ,xtype:'numberfield' }] // Commit and Reject buttons. Clicks are listened // to by Main Controller ,buttons:[{ text:'Reject' ,itemId:'reject' ,disabled:true ,glyph:0xf0e2 },{ text:'Commit' ,itemId:'commit' ,glyph:0xf00c ,disabled:true }] }); // eo cfg Ext.apply(me, cfg); me.callParent(arguments); } // eo function initComponent /** * Loads the record to the form or clears values if record * is not provided * @param {ExampleCdb.model.PersonModel/undefined/false} record */ ,loadRecord:function(record) { var me = this; if(record) { me.callParent([record]); } else { me.clearValues(); } } // eo function loadRecord /** * Clears the values of all fields and unsets * the previously loaded record */ ,clearValues:function() { var me = this; me.getForm()._record = null; me.getForm().setValues({ fname:'' ,lname:'' ,age:undefined }); me.updateUi(); } // eo function clearValues /** * Commits the record. Called by MainController */ ,commitRecord:function() { var me = this ,record = me.getRecord() ; if(record) { me.updateRecord(); record.commit(); me.updateUi(); } } // eo function commitRecord /** * Rejects changes in the record. Called by Main Controller */ ,rejectRecord:function() { var me = this ,record = me.getRecord() ; if(record) { record.reject(); me.loadRecord(record); me.updateUi(); } } // eo function rejectRecord /** * Updates enable/disable states of the buttons * depending on dirty state of the loaded record */ ,updateUi:function() { var me = this ,record = me.getRecord() ,disabled = record && record.dirty ? false : true ; Ext.each(me.query('button'), function(btn){ btn.setDisabled(disabled); }); } }); // eof
app/view/PersonGridView.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 Complex Data Binding Example Copyright (c) 2014, Jozef Sakalos, Saki Package: Complex Data Binding Example Author: Jozef Sakalos, Saki Contact: https://learnfromsaki.com/contact Date: 02. May 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. */ /** * Grid with the list of persons */ Ext.define('ExampleCdb.view.PersonGridView',{ extend:'Ext.grid.Panel' ,alias:'widget.persongridview' // no need to require the plugin as it is used in initComponent // during instantiation ,uses:[ 'Ext.grid.plugin.CellEditing' ] ,initComponent:function() { var me = this ,cfg = {} ; Ext.apply(cfg, { store:Ext.getStore('PersonStore') ,columns:[{ text:'First Name' ,dataIndex:'fname' ,editor:{ xtype:'textfield' } },{ text:'Last Name' ,flex:1 ,dataIndex:'lname' ,editor:{ xtype:'textfield' } },{ text:'Age' ,dataIndex:'age' ,editor:{ xtype:'numberfield' } }] ,listeners:{ selectionchange:'onSelectionChange' } ,selModel:{ allowDeselect:true } ,plugins:[{ ptype:'cellediting' ,clicksToEdit:2 ,pluginId:'cellediting' }] ,tbar:{ xtype:'toolbar' ,items:['->', { text:'Add Record' ,itemId:'addRecord' ,glyph:0xf067 ,handler:me.onAddRecord ,scope:me }] } }); Ext.apply(me, cfg); me.callParent(arguments); } // eo function initComponent // add record button click handler ,onAddRecord:function() { var me = this ,store = me.getStore() ,record = store.add({})[0] ; me.getPlugin('cellediting').startEdit(record, 0); } // eo function onAddRecord // fires a custom event when selection changes ,onSelectionChange:function(selModel, selected, eOpts) { this.fireEvent('rowselectionchange', this, selected, eOpts); } // eo function onSelectionChange }); // eof
app/view/PersonPanelView.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 Complex Data Binding Example Copyright (c) 2014, Jozef Sakalos, Saki Package: Complex Data Binding Example Author: Jozef Sakalos, Saki Contact: https://learnfromsaki.com/contact Date: 02. May 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('ExampleCdb.view.PersonPanelView',{ extend:'Ext.panel.Panel' ,alias:'widget.personpanel' ,cls:'person-panel' ,data:{} ,bodyPadding:10 ,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>' ] /** * Updates the body of this from by application data from the record * to the template or clears the values if record is not provided. * Called by MainController * @param {ExampleCdb.model.PersonModel/undefined/false} record */ ,loadRecord:function(record) { var me = this; if(record) { me.update(record.getData()) } else { me.update({}); } } // eo function loadRecord }); // eof
app/controller/MainController:
// 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 Complex Data Binding Example Copyright (c) 2014, Jozef Sakalos, Saki Package: Complex Data Binding Example Author: Jozef Sakalos, Saki Contact: https://learnfromsaki.com/contact Date: 02. May 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 Controller that implements the selection and data syncing * throughout the views */ Ext.define('ExampleCdb.controller.MainController', { extend:'Ext.app.Controller' ,views:[ 'MainView' ,'PersonGridView' ,'PersonFormView' ,'PersonDataView' ,'PersonPanelView' ] ,stores:[ 'PersonStore' ] // we can use refs as we only use one instance of all views in the app // See https://learnfromsaki.com/blessing-and-curse-of-refs/ for details ,refs:[{ ref:'dataView' ,selector:'persondataview' },{ ref:'form' ,selector:'personformview' },{ ref:'panel' ,selector:'personpanel' },{ ref:'grid' ,selector:'persongridview' }] ,init:function() { var me = this; me.listen({ // component domain component:{ persongridview:{ rowselectionchange:'onRowSelectionChange' ,edit:'onGridEdit' } ,persondataview:{ itemselectionchange:'onItemSelectionChange' } ,'personformview button':{ click:'onFormButtonClick' } ,'personformview field':{ change:'onFormFieldChange' } } }); // eo listen } // eo function init /** * Updates views after in-grid data editing * @private * @param {Ext.Editor} editor * @param {Object} e */ ,onGridEdit:function(editor, e) { var me = this; me.getForm().loadRecord(e.record); me.getPanel().loadRecord(e.record); } // eo function onGridEdit /** * Updates views after in-form field editing * @private * @param {Ext.form.field.Field} field */ ,onFormFieldChange:function(field) { var me = this ,form = me.getForm() ,record = form.getRecord() || false ; if(record) { form.updateRecord(); me.getPanel().loadRecord(record); } form.updateUi(); } // eo function onFormFieldChange /** * Commits or rejects the edited record * @private * @param {Ext.button.Button} btn */ ,onFormButtonClick:function(btn) { this.getForm()[btn.itemId + 'Record'](); } // eo function onFormButtonClick /** * Loads the record selected in grid to other views * and synchronizes selection with the data view * @private * @param {Ext.grid.Panel} grid * @param {ExampleCdb.model.PersonModel[]} selected */ ,onRowSelectionChange:function(grid, selected) { var me = this ,record = selected[0] || false ,sm = me.getDataView().getSelectionModel() ; me.getForm().loadRecord(record); me.getPanel().loadRecord(record); if(record) { sm.select([record]); } else { sm.deselectAll(); } } // eo function onRowSelectionChange /** * Loads the record selected in data view to other views * and synchronizes selection with the grid * @private * @param view * @param selected */ ,onItemSelectionChange:function(view, selected) { var me = this ,record = selected[0] || false ; me.getForm().loadRecord(record); me.getPanel().loadRecord(record); me.getGrid().getSelectionModel().select(selected, false, true); } // eo function onItemSelectionChange }); // eof
sass/src/view/PersonDataView.scss:
// 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 Complex Data Binding Example Copyright (c) 2014, Jozef Sakalos, Saki Package: Complex Data Binding Example Author: Jozef Sakalos, Saki Contact: https://learnfromsaki.com/contact Date: 02. May 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. */ /** * Styles for data view * For the sake of simplicity, this file contains also values. Values belong * to sass/var/view/PersonDataView.scss in real life * * @member ExampleCdb.view.PersonDataView */ .person-item { padding:5px; margin:5px; border:1px dotted gray; background: #fcfcfc; cursor:pointer; width:160px; float:left; } .person-item.x-item-selected { background: #E0E0E0; } // eof[ssba_hide]
I'm a well seasoned developer, consultant and educator of web applications based mainly on Sencha libraries, PHP, MySQL and Node.js. Besides (Apple) computers, I love photography and mountain biking.
Follow me:
Latest posts by saki (see all)
- Ext, Angular, React, and Vue - 27. June 2019
- The Site Resurgence - 11. February 2018
- Configuring ViewModel Hierarchy - 19. June 2015