Blog.

Ext Grid Renderer Danger

Well, not a real danger. You won’t get hurt if you use a renderer even if you use it wrongly. Nevertheless, renderers can introduce unexpected behavior that is not easy to fix what can cause frustrations and upsets or increased development time, at least.

Let’s see what we need to know to avoid that.

What is a renderer?

renderer is a function called by Ext for each cell in a grid column to generate the HTML markup for that cell.

Renderer function is call with arguments that are described in detail in the documentation. We take just first three for the demonstration. They are:

  • value – raw value from the underlying record
  • meta – the object used for adding a css or attributes to the grid cell
  • record – the record being processed

The default renderer only returns value and grids can work happily without a configured renderer in many cases.

renderer:function(value, meta, record) {
    return value;
}

However, we can use the value and data from other fields of the record to change how the value is displayed or we can even derive a new value from the existing data in the record.

Renderers can also be used for conditional highlighting of cells, make some font bold or other color or even to define cell tooltips.

Example

In this example I will first demonstrate problems introduced by incorrect use of renderers and then the solution of these problems.

The grids use actors database from http://themoviedb.org. Grids pull ~1000 records with the following structure:

{
    id: "1",
    birthday: "1968-10-12",
    fname: "Hugh",
    lname: "Jackman",
    profile_path: "/wnl7esRbP3paALKn4bCr0k8qaFu.jpg",
    tmdb_id: "6968"
}

We want to display the following columns:

  • First Name
  • Last Name
  • Full Name
  • Birthday
  • Age
  • Photo

Obviously, we do not have all data in the received JSON; full name is missing but we have first and last name so we can concatenate, age is missing but we have birthday so we can calculate the difference between today and the birthday and we can calculate the full img src from the profile path.

So we configure our renderers as follows:

// 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 Grid Renderer Danger Example

Copyright (c) 2014, Jozef Sakalos, Saki

Package:  Grid Renderer Danger Example
Author:   Jozef Sakalos, Saki
Contact:  https://learnfromsaki.com/contact
Date:     27. 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 problematic grid class definition
*/
Ext.define('ExampleGr4.view.ActorGridView1',{
extend:'Ext.grid.Panel'
,alias:'widget.actorgrid1'
,cls:'actorgrid'
,border:true
,uses:[
'ExampleGr4.store.ActorStore'
,'ExampleGr4.model.ActorModel1'
,'Ext.grid.column.Date'
,'Ext.grid.plugin.BufferedRenderer'
,'Ext.toolbar.TextItem'
,'Ext.saki.grid.Search'
]
/**
* Propagates down to the store. We want to load all records at once.
* @cfg {Number} pageSize
*/
,pageSize:9999

/**
* Initializes store, columns, plugins, etc
* @private
*/
,initComponent:function() {
var  me = this
,cfg = {}
;

Ext.apply(cfg,{
store:Ext.create('ExampleGr4.store.ActorStore',{
     autoLoad:true
    ,model:'ExampleGr4.model.ActorModel1'
    ,pageSize:me.pageSize

    // only for google analytics, no danger to remove
    ,listeners:{
         load:me.onStoreLoad
    }
})
,columns:[{
     dataIndex: 'fname'
    ,text:'First Name'
    ,width:120
},{
     dataIndex: 'lname'
    ,text:'Last Name'
    ,width:120
},{
     text:'Name'
    ,flex:1

    /**
     * Name of the field in the underlying record. Data from that field is
     * rendered in the grid cell or, if a renderer function is defined, it
     * becomes value of `v` variable.
     *
     * It is optional in this case because the renderer below does not rely
     * on passed value. It is set only because we want to sort and filter.
     */
    ,dataIndex:'lname'

    /**
     * Full name renderer
     * @private
     *
     * @param {Mixed} v value to render - depends on dataIndex configuration
     *
     * @param {Object} meta A collection of metadata about the current cell;
     * can be used or modified by the renderer.
     * Recognized properties are: tdCls, tdAttr, and style.
     *
     * @param {Ext.data.Model} rec The record for the current row
     *
     * @returns {String} Link to the profile of the actor at themoviedb.org.
     * Text of the link is the actor's full name concatenated from
     * first and last names
     */
    ,renderer:function(v, meta, rec) {
        var  data = rec.getData()
            ,fname = data.fname
            ,lname = data.lname
            ,name = (lname && fname) ? fname + ' ' + lname : lname || fname
            ,href = 'http://themoviedb.org/person/' + data.tmdb_id
            ,fmt = '{1}'
            ,link = Ext.String.format(fmt, href, name)
        ;
        return link;

    } // eo function renderer (name)
},{
     dataIndex: 'birthday'
    ,text:'Birthday'
    ,xtype:'datecolumn'
    ,format:'m/d/Y'
    ,width:100
},{
     text:'Age'
    ,align:'center'
    ,width:80

    /**
     * Optional dataIndex pointing to birthday for sorting demonstration
     */
    ,dataIndex:'birthday'

    /**
     * Age renderer is necessary because we receive only birthday from the
     * server. Here we calculate the age = Year(now) - Year(birthday)
     * @private
     *
     * @param {Mixed} v value to render - depends on dataIndex configuration
     *
     * @param {Object} meta A collection of metadata about the current cell;
     * can be used or modified by the renderer.
     * Recognized properties are: tdCls, tdAttr, and style.
     *
     * @param {Ext.data.Model} rec The record for the current row
     *
     * @returns {Number} The calculated age or empty string
     */
    ,renderer:function(v, meta, rec){
        var  bd = rec.get('birthday')
            ,age
        ;
        age = Ext.isDate(bd) 
            ? (new Date()).getFullYear() - bd.getFullYear() 
            : ''
        ;
        return age;

    } // eo function renderer
},{
     dataIndex:'profile_path'
    ,text:'Photo'
    ,align:'center'
    ,width:80

    /**
     * Sorting by profile path is no good
     */
    ,sortable:false

    /**
     * Actors photo renderer creates images for inline display and for tooltip
     * @private
     *
     * @param {Mixed} v value to render - depends on dataIndex configuration
     *
     * @param {Object} meta A collection of metadata about the current cell;
     * can be used or modified by the renderer.
     * Recognized properties are: tdCls, tdAttr, and style.
     *
     * @param {Ext.data.Model} rec The record for the current row
     *
     * @returns {Number} Image tag for inline image
     */
    ,renderer:function(v, meta, rec){
        var  format = Ext.String.format
            ,tag = ''
            ,src = format('src="http://image.tmdb.org/t/p/w185{0}" ', v)
            ,img = format(tag, 'style="height:24px"', src)
            ,qtip =
                'data-qtip="' + Ext.htmlEncode(format(tag, '', src)) + '"'
        ;
        meta.tdAttr = qtip;
        return img;

    } // eo function renderer (photo)
}]
,plugins:[{
     ptype:'bufferedrenderer'
    ,trailingBufferZone:20
    ,leadingBufferZone:50
},{
    // see https://learnfromsaki.com/software/ext-grid-search-plugin
     ptype:'saki-gridsearch'
    ,mode:'local'
    ,beforeSeparator:false
    ,searchText:'Search Name'
    ,disableIndexes:[
         'fname'
        ,'profile_path'
        ,'age'
        ,'birthday'
    ]
    ,buttonHidden:true
    ,showSelectAll:false
    ,targetCt:'toolbar[dock=top]'
}]

// top toolbar for the custom grid search plugin configuration
,dockedItems:[{
     xtype:'toolbar'
    ,dock:'top'
    ,items:'Search Last Name:'
}]
});

Ext.apply(me, cfg);
me.callParent(arguments);

} // eo function initComponent

/**
* Only for google analytics
* @private
*/
,onStoreLoad:function() {

try {
top.ga('send', 'event', 'Live Demo', 'Load', 'Grid Renderer Store Load');
} catch(e) {};

} // eo function onStoreLoad

});

// eof

Go back and read the comments in the above code…

The problematic grid

Open grids in new window

The problems:

  1. Try to sort by (full) Name. The grid does not sort by full name but by last name.
  2. Try to sort by Age ascending (using the column menu). You can see that the sort is descending in fact.
  3. Try to search for “Tom”. Tom Hanks and Tom Cruise are not found.

Why all this?
The single origin of the above problems is that we use renderers to derive new data from the existing data. Grid is just a view, all sorting and filtering is done in the store but store does not have full name and age.

The working grid

Every problem has a solution so has this. Try the same actions in the following grid that works as expected. Finds Hanks and Cruise and sorts the correct direction.

Open grids in new window

The solution

[su_content_free id=13 price=”9.90″]  
 

The general rule is:

Never ever use renderers to derive new data from the existing data. Use them only for user interface embellishment.

 
Thus, we need to create the missing data elsewhere. Where? In the model.

// 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 Grid Renderer Danger Example

 Copyright (c) 2014, Jozef Sakalos, Saki

 Package:  Grid Renderer Danger Example
 Author:   Jozef Sakalos, Saki
 Contact:  https://learnfromsaki.com/contact
 Date:     27. 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.
 */

/**
 * # Actor model class definition with converters
 */

Ext.define('ExampleGr4.model.ActorModel',{
     extend:'Ext.data.Model'
    ,idProperty:'id'
    ,fields:[
         {name:'id', type:'int'}

        /**
         * Calculated (full) name field
         */
        ,{name:'name', type:'string', persist:false, convert:function(v, rec){
            var  data = rec.getData()
                ,fname = data.fname
                ,lname = data.lname
            ;
            return (fname && lname) ? fname + ' ' + lname : lname || fname;
        }}

        ,{name:'fname', type:'string'}
        ,{name:'lname', type:'string'}
        ,{name:'birthday', type:'date', dateFormat:'Y-m-d'}

        /**
         * Calculated age field
         */
        ,{name:'age', type:'int', persist:false, convert:function(v, rec){
            var  bd = rec.get('birthday')
                ,age
            ;
            age = Ext.isDate(bd)
                ? (new Date()).getFullYear() - bd.getFullYear()
                : ''
            ;
        }}

        ,{name:'profile_path', type:'string'}
        ,{name:'tmdb_id', type:'int'}

    ] // eo fields

    ,proxy:{
         type:'ajax'
        ,url:'resources/service.php/actors1'
        ,actionMethods:{
            read:'POST'
        }
        ,reader:{
             type:'json'
            ,root:'data'
        }
    } // eo proxy
});

// eof

Now we only need to point dataIndex of Name and Age to the new model fields and we are done. Of course, the tooltip and name link renderers stay, but they are UI renderers, not data creation renderers.

[/su_content_free] [su_content_paid id=13]  
 
The general rule is:

Never ever use renderers to derive new data from the existing data. Use them only for user interface embellishment.

 
Thus, we need to create the missing data elsewhere. Where? In the model.

// 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 Grid Renderer Danger Example

 Copyright (c) 2014, Jozef Sakalos, Saki

 Package:  Grid Renderer Danger Example
 Author:   Jozef Sakalos, Saki
 Contact:  https://learnfromsaki.com/contact
 Date:     27. 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.
 */

/**
 * # Actor model class definition with converters
 */

Ext.define('ExampleGr4.model.ActorModel',{
     extend:'Ext.data.Model'
    ,idProperty:'id'
    ,fields:[
         {name:'id', type:'int'}

        /**
         * Calculated (full) name field
         */
        ,{name:'name', type:'string', persist:false, convert:function(v, rec){
            var  data = rec.getData()
                ,fname = data.fname
                ,lname = data.lname
            ;
            return (fname && lname) ? fname + ' ' + lname : lname || fname;
        }}

        ,{name:'fname', type:'string'}
        ,{name:'lname', type:'string'}
        ,{name:'birthday', type:'date', dateFormat:'Y-m-d'}

        /**
         * Calculated age field
         */
        ,{name:'age', type:'int', persist:false, convert:function(v, rec){
            var  bd = rec.get('birthday')
                ,age
            ;
            age = Ext.isDate(bd)
                ? (new Date()).getFullYear() - bd.getFullYear()
                : ''
            ;
        }}

        ,{name:'profile_path', type:'string'}
        ,{name:'tmdb_id', type:'int'}

    ] // eo fields

    ,proxy:{
         type:'ajax'
        ,url:'resources/service.php/actors1'
        ,actionMethods:{
            read:'POST'
        }
        ,reader:{
             type:'json'
            ,root:'data'
        }
    } // eo proxy
});

// eof

Now we only need to point dataIndex of Name and Age to the new model fields and we are done. Of course, the tooltip and name link renderers stay, but they are UI renderers, not data creation renderers.
[/su_content_paid] [su_content_anon][/su_content_anon]

saki
Follow me:
Latest posts by saki (see all)

5 Responses

  1. I’m stuck with the ‘sort by rendered value’ issue mentioned in the article.
    But the pros of render-way are that you can use a controller function for it, and can access any other associated viewmodel stores to calculate the result value.
    I didn’t succeed to achieve this within the convert function, which scope is limited by only the particular data record, not the view’s viewmodel or controller.

  2. Hi Saki,

    I am working on a project where i need to display a combobox inside a grid. i am successful by using widgetcolumn in ExtJS 5.0.1. i have another column checkboxcolumn, where based on this checkbox value i need to enable/disable combobox in respective row. But i am unable
    to get refernce of combobox reference on checkbox change event. I am stuck since week’s time, your help is really appreciable.
    –Srinivasa Rao S

    1. You can have combo as a column editor same way as here (double click on a row in Industry column).

      Other solution would be to configure the combo as above but for single click and, if you want, you can style the cell that it would look like combo for all rows while it would be a real combo only after click.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

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

Want to collaborate on an upcoming project?