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]
saki
Follow me:
Latest posts by saki (see all)

Enter your username and password to log into your account. Don't have an account? Sign up.

Want to collaborate on an upcoming project?