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]
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?