Making a beautiful list with GroupingStore / View and ExtJS
Today we’ll talk about how to make ExtJS a beautiful (and functional) list of any data, for example, a list of users or groups. I use this list in one of the current projects (although it’s not so beautiful and convenient there anymore). Such a widget can be used to display any data that is characterized not only by a test line, but also by extended data, and it’s also necessary to map some actions to each recruitment. You can dynamically update data (via the Store), as well as sorting and grouping - in general, all the features provided by the Grid component from ExtJS. I must say right away that I will use the version of ExtJS 3.0, but in the previous release, 2.3.x the example should also be workable. The given component is an example and by no means ready for use by the code, but only a demonstration of the possibilities, in your projects you can change and modify as you wish according to your capabilities. For the same reason, there is intentionally no source code for the article.
The main point that I took into account in this component is to minimize the distance between actions, that is, looking at the list, the user should immediately get the maximum of the information he needs (in the context of the list, of course), and if that cannot be displayed, that information about the action itself ( opportunities) should also be immediately visible. That is why I refused the context menu, or rather, duplicated the actions of the icons directly in the list. Thus, I eliminate the need for an additional action (opening the menu only in order to find out the answer to the question “what else can I do from here”), immediately showing possible actions. Proceeding from the same principle, tooltips are handed out only reference information,
Okay, let's get down to development. As a data source for the list, we will use the data array (reading it through ArrayReader ), and as the data store - Ext.data.GroupingStore . This side allows you to use data grouping by one of the fields, although I came across an interesting situation with the rendering of groups, but more on that later. And so, we transfer our data to the store, and he sorts and groups them according to the specified criterion, for which we use one of the fields. Array as the information store was chosen based on the fact that I did not need to directly update data from the server, we have a separate mechanism for this, so for simplicity we believe that we already have all the data locally.
The source data has the following structure:
And this is how the Store preparation code looks. We set the group code as a field for grouping, then we will get the rest of the data from the service array. Unfortunately, there are some difficulties in working with the grouping field - when rendering each line, the script will rewrite the group header and therefore we need to rewrite it every time. Although there is a mechanism for template header grouping, but despite the examples, nothing worked for me, so I redefined the header rendering function manually. Keep in mind that grouping is possible only for the same field that we sort.
We will also need an additional source of group data. I implemented it as an object, where you can get its description, icon and other data by the group code. This, of course, could all be included in the original data array, however, the same data will be used in other places, therefore they are taken out separately.
In addition to the usual group settings, I have two additional options - whether the group is private and private. You can have any other arbitrary parameters. They are used to display the necessary icons and actions for each group based on the parameters. That is, if the group is closed, then there is no need to display an icon with the action "join the group", etc.
Additionally, I store in the array the codes of the groups the current user is a member of:
Now we have all the data to render our list. For this, we use the Ext.grid.GridPanel component, although its functionality, of course, is a bit redundant for such an example. It would be better to use something like ListView, perhaps in the next article I will try to remake everything for it, but for now we will try through the Grid. Otherwise, one would have to come up with an independent implementation of the grouping, and the future expansion would be in question. So you have to consider your case personally - if you do not need all the features of the grid, try using the lightweight and fast ListView , but there will be much more manual work.
And so, we create the component Ext.grid.GridPanel , as the data source we use the previously created by usExt.data.GroupingStore (_usergroup_store). The main functionality is concentrated in the description of the columns and their renderings, which we will talk about in more detail now. First, I will show you a description of columns without renderers (a renderer or renderer is a special method that returns arbitrary text or html code that is displayed as a value in a cell).
As you can see, grouping occurs according to the last field - the group_type column, which uses data from the same column in the store (I usually use the same column names, because the column id matches the dataIndex). But we don’t need to display this column, it is purely utility, for grouping, so we set the hidden: true parameter, and also indicate that we want to group by this field - groupable: true.
There are two types of renderers for each column - renderer and groupRenderer. The first is responsible for displaying data in cells, the second is used to display the group header. Here lies one significant difficulty (although, apparently, this is just a strange architectural decision by the developers). The group header is used as the style name of this element, and if we redefine the render of the group, for example, by adding an html code, then all this text will go to the group style identifier. Below is a screenshot from Firebug to illustrate this point. I also fully admit that I did not fully understand the grouping mechanism, so if you can add or correct me, I will be grateful.
We proceed to describe the output of each column. Before the name of the group, we can have one or two icons. The green dot indicates whether the group is open - if there is one, then the group is publicly available, or is it private or closed, another icon is displayed. You can use your own group attributes and display other information. Each icon has its own tooltip, added through the markup (often more convenient).
Read more about the renderer and how to set it in the official documentation . Various data are transferred to the method, we are only interested in the initial value (the first parameter), the mat data about the html object where the value is rendered, and also the object of the current record (third parameter).
The second column is the number of participants in each group. But if the group is private, it is necessary to hide this data, so in the render we check the type of group and display the necessary value.
The most interesting will be the third and last column, where we will display the button icons for user actions, while their set will depend on the parameters of the group. True, there are user extensions, RowActions and CellActions , but today we will do the same manually (honestly, I just did it first and then found these extensions, so I didn’t redo the finished code).
Next, we have a hidden column for grouping. For her, we will redefine the render of the group to display a beautiful title. The render gets one value, the field that is declared as a condition for grouping, in our case, is the group code, by which we will get all the rest of the information.
There is the final touch, it is necessary to specify the necessary View in our table, in our case - GroupingView .
We pointed out that there can be no lines without groups, and also banned the display of the name of the group, since we redefined the render of the group. groupTextTpl - here you specify a template for displaying the name of the group, but there are difficulties with it, since it is rather difficult to describe conditions and processing of global values inside the template. Therefore, we simply output what our render function returned and that’s all, without any processing.
That's all, as a result we get a beautiful grouped list. To implement custom actions, you need to hang handlers on the icons, but I think you can handle this yourself. I also note that in the example the table header is intentionally hidden, but if it is enabled (via the grid config), it will be possible to sort the data (by those columns that are declared sortable) by clicking on the header. In this case, the grouping will be saved, that is, the data within each group will be sorted individually.
In conclusion, I will show another screenshot, the second list, which displays all users and additional data like the name of the company and links to its website. It is obtained by a little refinement of the example described above, so you can experiment yourself.
The main point that I took into account in this component is to minimize the distance between actions, that is, looking at the list, the user should immediately get the maximum of the information he needs (in the context of the list, of course), and if that cannot be displayed, that information about the action itself ( opportunities) should also be immediately visible. That is why I refused the context menu, or rather, duplicated the actions of the icons directly in the list. Thus, I eliminate the need for an additional action (opening the menu only in order to find out the answer to the question “what else can I do from here”), immediately showing possible actions. Proceeding from the same principle, tooltips are handed out only reference information,
Okay, let's get down to development. As a data source for the list, we will use the data array (reading it through ArrayReader ), and as the data store - Ext.data.GroupingStore . This side allows you to use data grouping by one of the fields, although I came across an interesting situation with the rendering of groups, but more on that later. And so, we transfer our data to the store, and he sorts and groups them according to the specified criterion, for which we use one of the fields. Array as the information store was chosen based on the fact that I did not need to directly update data from the server, we have a separate mechanism for this, so for simplicity we believe that we already have all the data locally.
The source data has the following structure:
- group id unique
- default group icon (file name)
- group name
- number of users in a group
- group code, this is to get extended information from an additional array of group data
- group description
- var _user_groups = [
- [0,'user_home.png','Модераторы системы',2,
- 'system','Служебная группа для модераторов и администраторов системы'],
- [10,'user_home.png','Бизнес на играх',10,'profy',''],
- [20,'user_home.png','Игры под iPhone/iTouch',22,'profy',''],
- [30,'user_home.png','Разработка под FreeBSD',11,'profy',''],
- [40,'user_home.png','Первый филиал',20,
- 'filials','Приватная группа московского филиала нашей компании'],
- [50,'user_home.png','О политике и не только',120,'publicusers',''],
- [60,'user_home.png','Приколы на работе',99,
- 'publicusers','Общая группа для развлечений, юмора и просто отдыха']
- ];
* This source code was highlighted with Source Code Highlighter.
And this is how the Store preparation code looks. We set the group code as a field for grouping, then we will get the rest of the data from the service array. Unfortunately, there are some difficulties in working with the grouping field - when rendering each line, the script will rewrite the group header and therefore we need to rewrite it every time. Although there is a mechanism for template header grouping, but despite the examples, nothing worked for me, so I redefined the header rendering function manually. Keep in mind that grouping is possible only for the same field that we sort.
- var _usergroup_store = new Ext.data.GroupingStore({
- reader: new Ext.data.ArrayReader({},
- [{
- name: 'id',
- type: 'int'
- }, {
- name: 'group_icon',
- type: 'string'
- }, {
- name: 'group_name',
- type: 'string'
- }, {
- name: 'count_users',
- type: 'string'
- }, {
- id:'group_type',
- name: 'group_type',
- type: 'string'
- }, {
- name: 'group_desc',
- type: 'string'
- }]),
- data: _user_groups,
- sortInfo: {
- field: 'group_type',
- direction: "ASC"
- },
- groupField: 'group_type',
- groupOnSort: true
- });
* This source code was highlighted with Source Code Highlighter.
We will also need an additional source of group data. I implemented it as an object, where you can get its description, icon and other data by the group code. This, of course, could all be included in the original data array, however, the same data will be used in other places, therefore they are taken out separately.
- var _user_groups_cats = {
- system:{
- icon:'user_home.png',
- title:'Системные группы',
- desc:'Служебная группа только для сотрудников Wheemplay Ltd.',
- isClosed:true,
- isPrivate:false
- },
- profy:{
- icon:'user_star.png',
- title:'Профессиональные группы',
- desc:'Группы по интересам',
- isClosed:false,
- isPrivate:false
- },
- filials:{
- icon:'user_earth.png',
- title:'Группы филиалов',
- desc:'Частные группы филиалов',
- isClosed:false,
- isPrivate:true
- },
- publicusers:{
- icon:'group.png',
- title:'Общие пользовательские',
- desc:'Общедоступные группы, куда может войти любой желающий',
- isClosed:false,
- isPrivate:false
- },
- my:{
- icon:'user_comment.png',
- title:'Мои группы',
- desc:'Мои группы (в которых вы состоите)',
- isClosed:true,
- isPrivate:true
- }
- }
* This source code was highlighted with Source Code Highlighter.
In addition to the usual group settings, I have two additional options - whether the group is private and private. You can have any other arbitrary parameters. They are used to display the necessary icons and actions for each group based on the parameters. That is, if the group is closed, then there is no need to display an icon with the action "join the group", etc.
Additionally, I store in the array the codes of the groups the current user is a member of:
var _i_in_group = [0, 30, 60];
Now we have all the data to render our list. For this, we use the Ext.grid.GridPanel component, although its functionality, of course, is a bit redundant for such an example. It would be better to use something like ListView, perhaps in the next article I will try to remake everything for it, but for now we will try through the Grid. Otherwise, one would have to come up with an independent implementation of the grouping, and the future expansion would be in question. So you have to consider your case personally - if you do not need all the features of the grid, try using the lightweight and fast ListView , but there will be much more manual work.
And so, we create the component Ext.grid.GridPanel , as the data source we use the previously created by usExt.data.GroupingStore (_usergroup_store). The main functionality is concentrated in the description of the columns and their renderings, which we will talk about in more detail now. First, I will show you a description of columns without renderers (a renderer or renderer is a special method that returns arbitrary text or html code that is displayed as a value in a cell).
- columns: [{
- id: 'group_name',
- header: "Группы",
- sortable: false,
- width: 150,
- dataIndex: 'group_name'
- }, {
- header: "Участников",
- width: 35,
- hidden:false,
- sortable: true,
- dataIndex: 'count_users'
- },{
- header: "действия",
- width: 60,
- hidden:false,
- sortable: false,
- dataIndex: 'count_users'
- }, {
- id: 'group_type',
- dataIndex: 'group_type',
- hidden: true,
- groupable: true
- }]
* This source code was highlighted with Source Code Highlighter.
As you can see, grouping occurs according to the last field - the group_type column, which uses data from the same column in the store (I usually use the same column names, because the column id matches the dataIndex). But we don’t need to display this column, it is purely utility, for grouping, so we set the hidden: true parameter, and also indicate that we want to group by this field - groupable: true.
There are two types of renderers for each column - renderer and groupRenderer. The first is responsible for displaying data in cells, the second is used to display the group header. Here lies one significant difficulty (although, apparently, this is just a strange architectural decision by the developers). The group header is used as the style name of this element, and if we redefine the render of the group, for example, by adding an html code, then all this text will go to the group style identifier. Below is a screenshot from Firebug to illustrate this point. I also fully admit that I did not fully understand the grouping mechanism, so if you can add or correct me, I will be grateful.
We proceed to describe the output of each column. Before the name of the group, we can have one or two icons. The green dot indicates whether the group is open - if there is one, then the group is publicly available, or is it private or closed, another icon is displayed. You can use your own group attributes and display other information. Each icon has its own tooltip, added through the markup (often more convenient).
- renderer:function(obj, x, y)
- {
- var src = '';
- //для упрощения
- var tmp = _user_groups_cats[y.data.group_type];
-
- if (tmp.isClosed == true)
- {
- src = src +
- ' ';
- }
-
- if (tmp.isPrivate == true)
- {
- src = src +
- ' ';
- }
-
- if ((tmp.isClosed == false) && (tmp.isPrivate == false) )
- {
- src = src +
- ' ';
- }
-
- // обязательно вернуть результат
- return src + ' ' + obj + '';
- }
* This source code was highlighted with Source Code Highlighter.
Read more about the renderer and how to set it in the official documentation . Various data are transferred to the method, we are only interested in the initial value (the first parameter), the mat data about the html object where the value is rendered, and also the object of the current record (third parameter).
The second column is the number of participants in each group. But if the group is private, it is necessary to hide this data, so in the render we check the type of group and display the necessary value.
- renderer:function(obj, x, y)
- {
- if (_user_groups_cats[y.data.group_type].isPrivate == false)
- {
- return obj + ' учасн.';
- }
- else
- return 'скрыто';
- }
* This source code was highlighted with Source Code Highlighter.
The most interesting will be the third and last column, where we will display the button icons for user actions, while their set will depend on the parameters of the group. True, there are user extensions, RowActions and CellActions , but today we will do the same manually (honestly, I just did it first and then found these extensions, so I didn’t redo the finished code).
- renderer:function(obj, x, y)
- {
- var src = '';
- var tmp = _user_groups_cats[y.data.group_type];
-
- src = src + '
- 'src="/images/icons/vcard.png" alt="" align="absmiddle" /> ';
-
- // если группа не приватная и юзер в ней не состоит
- if ((tmp.isPrivate == false) && (_i_in_group.indexOf(y.data.id) == -1))
- {
- src = src + '
- 'src="/images/icons/user_add.png" alt="" align="absmiddle" /> ';
- }
-
- if ((tmp.isPrivate == false) && (tmp.isClosed == false))
- {
- src = src + '
- 'src="/images/icons/group.png" alt="" align="absmiddle" /> ';
- }
-
- // проверю, состоит ли юзер в группе
- if (_i_in_group.indexOf(y.data.id) != -1)
- {
- src = src + '
- 'src="/images/icons/user_delete.png" alt="" align="absmiddle" /> ' +
- ' ';
- }
-
- return src;
- }
* This source code was highlighted with Source Code Highlighter.
Next, we have a hidden column for grouping. For her, we will redefine the render of the group to display a beautiful title. The render gets one value, the field that is declared as a condition for grouping, in our case, is the group code, by which we will get all the rest of the information.
- groupRenderer:function(group)
- {
- return ' ' + _user_groups_cats[group].title +
- '';
- }
* This source code was highlighted with Source Code Highlighter.
There is the final touch, it is necessary to specify the necessary View in our table, in our case - GroupingView .
- view: new Ext.grid.GroupingView({
- forceFit: true,
- enableNoGroups: false,
- autoFill: true,
- scrollOffset:0,
- showGroupName: false,
- groupTextTpl: '{text}'
- })
* This source code was highlighted with Source Code Highlighter.
We pointed out that there can be no lines without groups, and also banned the display of the name of the group, since we redefined the render of the group. groupTextTpl - here you specify a template for displaying the name of the group, but there are difficulties with it, since it is rather difficult to describe conditions and processing of global values inside the template. Therefore, we simply output what our render function returned and that’s all, without any processing.
That's all, as a result we get a beautiful grouped list. To implement custom actions, you need to hang handlers on the icons, but I think you can handle this yourself. I also note that in the example the table header is intentionally hidden, but if it is enabled (via the grid config), it will be possible to sort the data (by those columns that are declared sortable) by clicking on the header. In this case, the grouping will be saved, that is, the data within each group will be sorted individually.
In conclusion, I will show another screenshot, the second list, which displays all users and additional data like the name of the company and links to its website. It is obtained by a little refinement of the example described above, so you can experiment yourself.