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 recordmeta
– the object used for adding a css or attributes to the grid cellrecord
– 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
The problems:
- Try to sort by (full) Name. The grid does not sort by full name but by last name.
- Try to sort by Age ascending (using the column menu). You can see that the sort is descending in fact.
- 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.
The solution
[su_content_free id=13 price=”9.90″]The general rule is:
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.
The general rule is:
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]
- Ext, Angular, React, and Vue - 27. June 2019
- The Site Resurgence - 11. February 2018
- Configuring ViewModel Hierarchy - 19. June 2015
5 Responses
The search of “The problematic grid” is right
Well, if I search for Tom, Tom Hanks is not found so I cannot fully search in the column with renderer.
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.
how can you have renderer on more then one column in a grid?
You can have renderer for each column in the grid. They are defined on per-column basis, so as many columns that many possible renderers.
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
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.
This is a great post. I logged in and cannot find the code example. Can you direct me?
It can be that the browser is still caching before-login version. Reload the browsers and the code should be there.
Use cache buster, add ?_dc=12345 to the url