ExtJS 4: Problema com Grid ActionColumn + MVC

A dica de hoje é como resolver um problema do ExtJS relacionado a Grid ActionColumn usando a arquitetura MVC.

O Problema:

Os exemplos da Sencha que usam action column não usam o MVC como arquitetura. Todo o código fica em apenas em um arquivo. Até aí não tem problema nenhum. Mas como no ExtJS 4 é aconselhável que você use a arquitetura MVC para o seu código ExtJS, temos que usá-la né?

No MVC, todas as ações tomadas pelo usuário precisam ser controladas pelo Controller. Ou seja, se o usuário clicar no botão da actioncolumn do grid, o controller precisa capturar esse evento e tomar alguma decisão.

Vamos ao primeiro passo antes de mostrar o problema: o código abaixo foi retirado do exemplo array-grid, um dos exemplos disponíveis na docmentação do ExtJS 4. Esse array grid possui 2 action columns, como mostradas abaixo:

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
{
xtype: 'actioncolumn',
width: 50,
items: [{
icon : '../shared/icons/fam/delete.gif',
tooltip: 'Sell stock',
handler: function(grid, rowIndex, colIndex) {
var rec = store.getAt(rowIndex);
alert("Sell " + rec.get('company'));
}
}, {
getClass: function(v, meta, rec) {
if (rec.get('change') < 0) {
this.items[1].tooltip = 'Hold stock';
return 'alert-col';
} else {
this.items[1].tooltip = 'Buy stock';
return 'buy-col';
}
},
handler: function(grid, rowIndex, colIndex) {
var rec = store.getAt(rowIndex);
alert((rec.get('change') < 0 ? "Hold " : "Buy ") + rec.get('company'));
}
}]
}
[/code]

Bem, como imaginamos que o Action Column é um botão, ao transformamos o código acima em MVC, a primeira coisa que tentamos fazer é isso:

GridX.js

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false" highlight="7,22"]
{
xtype: 'actioncolumn',
width: 50,
items: [{
icon : 'resources/icons/delete.gif',
tooltip: 'Sell stock',
action: 'sell'
/*handler: function(grid, rowIndex, colIndex) {
var rec = store.getAt(rowIndex);
alert("Sell " + rec.get('company'));
}*/
}, {
getClass: function(v, meta, rec) {
if (rec.get('change') < 0) {
this.items[1].tooltip = 'Hold stock';
return 'alert-col';
} else {
this.items[1].tooltip = 'Buy stock';
return 'buy-col';
}
},
action: 'buy'
/*handler: function(grid, rowIndex, colIndex) {
var rec = store.getAt(rowIndex);
alert((rec.get('change') < 0 ? "Hold " : "Buy ") + rec.get('company'));
}*/
}]
}];

[/code]

ControllerX.js

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
init: function() {
this.control({
'stockgrid actioncolumn[action=sell]': {
click: this.sellStock
},
'stockgrid actioncolumn[action=buy]': {
click: this.buyStock
}
});
},
[/code]

Já que a action column é um botão, adicionamos uma action e podemos tratar no controller como se fosse qualquer outro botão, correto? ERRADO! Isso não funciona pois o Action Column NÃO é um componente.

O que fazer então nesse caso? Não vou colocar o handler da Action Column na própria classe grid pois como estou usando MVC, isso é muuuuito feio!

Eis então algumas soluções:

Gambiarras Soluções:

Bem, quando estava com esse problema, dei uma pesquisada no fórum da Sencha e encontrei 3 soluções. Vou apresentar as 3 para vocês:

Gambiarra Solução

Como a Action Column não é um componente, precisamos então disparar um evento quando o usuário clicar no botão, e aí vamos conseguir tratar esse evento no controller. Mas atenção, essa gambiarra solução só funciona se você tiver apenas 1 item dentro do seu action column.

GridX.js

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false" highlight="7,10"]
{
xtype: 'actioncolumn',
width: 50,
items: [{
icon : 'resources/icons/delete.gif',
tooltip: 'Sell stock',
scope: this,
handler: function(grid, rowIndex, colIndex) {
var rec = grid.getStore().getAt(rowIndex);
this.fireEvent('sellStock', this, rec);
}
}]
}
[/code]

ControllerX.js

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
init: function() {
this.control({
'stockgrid': {
'sellStock': this.sellStock
}
});
},
[/code]

Como o Action Column não é um componente, precisamos declarar a função handler dentro do código do grid mesmo, mas apenas para disparar um novo evento para o Controller escutar. Pelo menos a lógica do handler não vai ficar dentro do código do grid!

Gambiarra Solução

Essa solução funciona se você tiver 1 ou vários items no Action Column. Essa gambiarra solução é muito feia, mas pelo menos funciona.

estilo.css

[code lang="css" firstline="1" toolbar="true" collapse="false" wraplines="false"]
/* style rows on mouseover */
.x-grid-row-over .x-grid-cell-inner {
font-weight: bold;
}
/* shared styles for the ActionColumn icons */
.x-action-col-cell img {
height: 16px;
width: 16px;
cursor: pointer;
}
/* custom icon for the "buy" ActionColumn icon */
.x-action-col-cell img.icon-buy {
background-image: url(../icons/accept.gif);
}
/* custom icon for the "alert" ActionColumn icon */
.x-action-col-cell img.icon-alert {
background-image: url(../icons/error.gif);
}
/* custom icon for the "alert" ActionColumn icon */
.x-action-col-cell img.icon-delete {
background-image: url(../icons/delete.gif);
}
[/code]

GridX.js

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false" highlight="6,12,15"]
{
xtype: 'actioncolumn',
width: 50,
items: [{
//icon : 'resources/icons/delete.gif',
iconCls: 'icon-delete',
tooltip: 'Sell stock',
}, {
getClass: function(v, meta, rec) {
if (rec.get('change') < 0) {
this.items[1].tooltip = 'Hold stock';
return 'icon-alert';
} else {
this.items[1].tooltip = 'Buy stock';
return 'icon-buy';
}
}
}]
}
[/code]

ControllerX.js

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
init: function() {
this.control({
'stockgrid actioncolumn': {
click: this.onAction
}
});
},

onAction: function(view,cell,row,col,e){

var m = e.getTarget().className.match(/\bicon-(\w+)\b/);

var rec = this.getStocksStore().getAt(row);

if(m){
switch(m[1]){
case 'buy':
alert("Buy " + rec.get('company'));
break;
case 'alert':
alert("Hold " + rec.get('company'));
break;
case 'delete':
alert("Sell " + rec.get('company'));
break;
}
}
}
[/code]

Primeiro, mude o nome do estilo que irá aplicar nas Action Columns. Tem que ser algo do tipo icon-nomedaaction. Depois aplique esse css nos botões de acordo com a action de cada um.

No Controller, vamos apenas escutar o evento disparado pelo actioncolumn do grid, mas não saberemos qual é o botão que o usuário clicou. Através do nome da classe (css que aplicamos) do target do evento, conseguimos saber o botão que o usuário clicou.

Essa é uma gambiarra solução muitíssimo feia, mas o pessoal tem usado muito essa. Pelo menos funciona!

3ª Solução

Ah, esse é uma solução mesmo, e não gambiarra solução temporária que será permanente!

Em vez de usar o ActionColumn da API nativa do ExtJS, use o plugin Ext.ux.grid.column.ActionButtonColumn, que substitui o Action Column por um botão de verdade, que é um componente do ExtJS 4 e assim podemos tratar o evento no controller como um botão normalmente!

GridX.js

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false" highlight="2,7"]
{
xtype:'actionbuttoncolumn',
width: 50,
items: [{
iconCls: 'icon-delete',
tooltip: 'Sell stock',
action: 'sell'
}]
}
[/code]

ControllerX.js

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
init: function() {
this.control({
'stockgrid actionbuttoncolumn[action=sell]': {
click: this.sellStock
}
});
},
[/code]

Agora sim, tudo funcionando perfeitamente!

Bem, se você está passando por esse problema, escolha uma das 3 soluções acima e seja feliz!

Até a próxima!