No post CRUD Avançado com Ext recebi sugestão de exemplificar um CRUD Pai-Filho. Nessa interface temos um registro X que contém N registros Y, como por exemplo os produtos de uma nota fiscal, justamente o exemplo que irei abordar agora. O que difere esse tutorial do crud avançado postado anteriormente, é que esse usa um conceito muito bacana que chamo de buffer de dados. Todos os N produtos que serão associados a nota fiscal ficarão em um buffer, para que só depois quando o usuário clique em salvar eles possam ser persistidos. Quando isso acontecer, uma rotina irá percorrer cada registro do buffer, extrair os dados e enviar ao servidor. Também iremos ver nesse tutorial uma extensão muito bacana para gerar somatórios em grid.
Preparando Terreno
Assim como nos outros tutoriais usaremos a pasta examples do Ext JS 3.0 para criar nossa aplicação. Nela criarei a pasta notaFiscal.
Nota Fiscal
Atenção: Essa tela não é uma nota fiscal eletrônica e nem respeita todo o regulamento para se criar uma. Aproveitei apenas o bom exemplo de vários produtos em uma nota fiscal para criar esse tutorial.
Seguindo o conceito já explicado em outros tutorias de que cada interface é representada por uma classe, vamos criar a estrutura básica de nossa nota fiscal com uma classe extendida de Ext.Panel:
Ext.onReady(function()
{
//cria janela e coloca painel nota fiscal dentro
new Ext.Window({
layout : 'fit'
,title : 'Nota Fiscal'
,width : 700
,height : 500
,x : 10
,y : 110
,items : {
xtype : 'b-notafiscal'
,border : false
}
}).show();
});
/**
* Classe que representa a NotaFiscal
*/
var NotaFiscal = Ext.extend(Ext.Panel,{
//Config {
//super
layout : 'anchor'
,bodyStyle : 'padding:5px;'
,autoScroll : true
//}
//Inits {
,initComponent: function()
{
//TODO: colocar os stores aqui
//Components
Ext.apply(this,{
defaults : { anchor:'-17' }
,buttons : [{
text : 'Salvar'
,iconCls: 'icon-save'
,scope : this
,handler: this._onBtnSalvarClick
}]
,items : [{
//cabeçalho
xtype : 'box'
,cls : 'nf-header'
,autoEl : {
tag: 'div'
,cn: '<img src="'+Ext.BLANK_IMAGE_URL+'" alt="" />'+
'
<h1>Nota Fiscal</h1>
<h2>'+new Date().getTime()+'</h2>
'
}
}
//TODO: Criar demais componentes
]
});
//super
NotaFiscal.superclass.initComponent.call(this);
}
,initEvents: function()
{
//super
NotaFiscal.superclass.initEvents.call(this);
//TODO: Criar eventos
}
//}
//Overrides {
,onDestroy: function()
{
//super
NotaFiscal.superclass.onDestroy.apply(this, arguments);
//TODO: limpar referências
}
//}
//Listeners {
,_onBtnSalvarClick: function()
{
//TODO: fazer ação salvar
}
//}
});
Ext.reg('b-notafiscal',NotaFiscal);
O primeiro elemento visual é o cabeçalho, que não irei abordar. O segundo é um combo para seleção de cliente, com um painel para maiores informações abaixo. Toda vez que um cliente for selecionado o painel de informações irá mostrar os dados do cliente. O legal dessa rotina é ver como podemos extrair dados de uma seleção de um combo e interagir com outros componentes a partir desses dados.
No método initComponent definimos o store do cliente, e também o combo e o painel de informações:
//Colocar store na área dos stores
this._dsClientes = new Ext.data.JsonStore({
proxy : new Ext.data.MemoryProxy( Dummy.clientes )
,idProperty : 'clienteID'
,fields : [
{name: 'clienteID' , type: 'int' }
,{name: 'nomeFantasia' , type: 'string'}
,{name: 'razaoSocial' , type: 'string'}
,{name: 'cidade' , type: 'string'}
,{name: 'endereco' , type: 'string'}
]
});
//...
//colocar esta definição nos items do painel
{
//fieldset clientes
xtype : 'fieldset'
,title : 'Cliente'
,autoHeight : true
,defaults : { anchor: '0' }
,items : [
this._comboCliente = new Ext.form.ComboBox({
displayField : 'nomeFantasia'
,valueField : 'clienteID'
,emptyText : 'Selecione um cliente'
,triggerAction : 'all'
,hideLabel : true
,forceSelection : true
,allowBlank : false
,store : this._dsClientes
,tpl : new Ext.XTemplate(
''
,'
<div class="x-combo-list-item combo-cliente-item">'
,'
<h1><strong>{nomeFantasia}</strong> - {razaoSocial}</h1>
'
,'<span>{cidade} - {endereco}</span>'
,'
'
,''
)
}),
this._panelInfoCliente = new Ext.BoxComponent({
cls: 'info-cliente'
,tpl: new Ext.Template(
'
<div><label>Nome fantasia:</label><span>{nomeFantasia}</span></div>
'
,'
<div><label>Razão Social:</label><span>{razaoSocial}</span></div>
'
,'
<div><label>Localização:</label><span>{cidade} - {endereco}</span></div>
'
)
})]
}
Perceba que estamos instanciando os componentes e guardando as suas referências (this._comboCliente, this._panelInfoCliente ). Fazemos isso porque no método initEvents vou associar o evento no combo para que toda vez que um item seja selecionado, o painel de informações possa ser populado. Utilizo templates para fazer isso. Com ele você pode definir uma estrutura HTML que futuramente será preenchida com dados.
//Colocar esse código em initEvents
this._comboCliente.on({
scope : this
,select : function(combo, record)
{
this._panelInfoCliente.show();
this._panelInfoCliente.tpl.overwrite( this._panelInfoCliente.el , record.data );
}
,blur: function(combo)
{
if(Ext.isEmpty(combo.getValue()))
this._panelInfoCliente.hide();
}
});
Feito isto podemos ver o segundo elemento visual, que é o mais importante, o fieldset de produtos. Nele temos um combo para selecionar o produto, um numberfield para informar a quantidade, um botão de adicionar, e a listagem de produtos da nota fiscal. Eu utilizei um pequeno formulário para adicionar o produto ao grid, você poderia abrir uma janela a parte ou qualquer outra interface que quisesse, contando que você tenha como extrair os dados do produto qualquer interface serve.
//Define um dataStore que irá buscar os produtos no combo,
//e outro para armazenar os produtos no grid
this._dsProdutosCombo = new Ext.data.JsonStore({
proxy : new Ext.data.MemoryProxy( Dummy.produtos )
,idProperty : 'produtoID'
,fields : [
{name: 'produtoID' , type: 'int' }
,{name: 'nome' , type: 'string'}
,{name: 'valor' , type: 'float' }
]
});
this._dsProdutosGrid = new Ext.data.JsonStore({
data : []
,fields : [
{name: 'nome' , type: 'string'}
,{name: 'quantidade', type: 'int' }
,{name: 'valor' , type: 'float' }
,{name: 'total' , type: 'float' }
]
});
//logo abaixo das definições do store cria o plugin e um renderer que serão usados no grid
//renderer
var rendererReal = function(v)
{
return Ext.util.Format.usMoney(v).replace('$','R$');
}
// utilize custom extension for Group Summary
var summary = new Ext.ux.grid.GridSummary();
//...
//Nos itens da nota fiscal define o fieldset de produtos
{
//fieldset produtos
xtype : 'fieldset'
,title : 'Produtos'
,autoHeight : true
,labelWidth : 80
,items : [
this._comboProduto = new Ext.form.ComboBox({
displayField : 'nome'
,valueField : 'produtoID'
,emptyText : 'Selecione um produto'
,triggerAction : 'all'
,fieldLabel : 'Produto'
,anchor : '0'
,forceSelection : true
,store : this._dsProdutosCombo
})
,this._txtQuantidade = new Ext.form.NumberField({
fieldLabel : 'Quantidade'
,width : 100
}),{
xtype : 'button'
,text : 'adicionar'
,iconCls: 'silk-add'
,style : 'margin-left:85px;'
,scope : this
,handler: this._onBtnAdicionarProdutoClick
},
this._gridProdutos = new Ext.grid.GridPanel({
title : 'Produtos Selecionados'
,style : 'margin-top:10px;'
,autoExpandColumn : 'nome'
,height : 200
,store : this._dsProdutosGrid
,plugins : summary
,columns : [{
header : '&nbsp;'
,dataIndex : 'nome'
,align : 'center'
,width : 40
,fixed : true
,renderer : function()
{
return '<img class="silk-delete" style="cursor: pointer;" src="'+Ext.BLANK_IMAGE_URL+'" alt="" width="16" height="16" />'
}
},{
header : 'Produto'
,dataIndex : 'nome'
,id : 'nome'
,width : 300
},{
header : 'Quantidade'
,dataIndex : 'quantidade'
,summaryType: 'sum'
,width : 80
,align : 'center'
},{
header : 'Valor Unitário'
,dataIndex : 'valor'
,width : 80
,align : 'center'
,summaryType: 'max'
,renderer : rendererReal
},{
header : 'Valor Total'
,dataIndex : 'total'
,align : 'center'
,id : 'valorTotal'
,summaryType : 'sum'
,width : 80
,renderer : rendererReal
,summaryRenderer: rendererReal
}]
})]
}
//...
//No initEvents criar essa associação de evento
//grid produtos
this._gridProdutos.on({
scope : this
,cellclick : this._onGridProdutosCellClick
})
//...
//Esse método vai na região de listeners
,_onGridProdutosCellClick: function(grid, row, col, e)
{
if(col !== 0)
return;
//busca registro
var record = grid.store.getAt(row);
//remove do store
record.store.remove(record);
}
Tendo o fieldset definido, vamos a interação mais importante da tela, a adição de produtos. Essa adição se resume simplesmente em:
- extrair os dados do formulário
- instanciar um novo registro com base na definição de registros armazenado no datastore
- adicionar o novo registro ao store
/**
* Listener disparado ao clicar no botão de adicionar produto
*/
,_onBtnAdicionarProdutoClick: function()
{
//busca os dados
var recordProduto = this._comboProduto.store.getById(this._comboProduto.getValue());
var qtde = this._txtQuantidade.getValue();
//valida
if( !recordProduto || !qtde )
{
Ext.Msg.alert('Atenção','É necessário selecionar um produto e informar uma quantidade');
return;
}
//cria registro
var newRecord = new this._dsProdutosGrid.recordType({
nome : recordProduto.get('nome')
,quantidade : qtde
,valor : recordProduto.get('valor')
,total : (qtde * recordProduto.get('valor')).toFixed(2)
});
//adiciona
this._dsProdutosGrid.add(newRecord);
//reseta
this._comboProduto.reset();
this._txtQuantidade.reset();
}
E a última ação, salvar! Devemos extrair os dados que estão em buffer e mandá-los para o servidor. É uma rotina muito simples que acaba enrrolando muitos programadores por aÃ. Segue exemplo:
,_onBtnSalvarClick: function()
{
//valida
if( !this._comboCliente.isValid() )
return;</p>
if(this._dsProdutosGrid.getCount()===0)
{
Ext.Msg.alert('Atenção','É preciso adicionar ao menos um produto')
return;
}
//extrai produtos da nota fiscal
var produtos = [];
this._dsProdutosGrid.each(function( record )
{
produtos.push( Ext.encode(record.data) );
})
//alerta
Ext.Msg.alert('Requisição feita (visualize com firebug)','Parâmetros= ' + Ext.encode({
fnTarget : 'inserirNF'
,clienteID : this._comboCliente.getValue()
,'produtos[]' : produtos
}).replace(/\\\"/gi,'"'));
//requisição de exemplo
Ext.Ajax.request({
url : 'NotaFiscal.ajax.php'
,params : {
fnTarget : 'inserirNF'
,clienteID : this._comboCliente.getValue()
,'produtos[]' : produtos
}
});
}
Não podemos esquecer também de limpar todas as referências no método onDestroy:
,onDestroy: function()
{
//super
NotaFiscal.superclass.onDestroy.apply(this, arguments);
//limpa referências alocadas
this._panelInfoCliente = this._comboCliente = this._comboProduto =
this._txtQuantidade = this._gridProdutos = null;
}
É é isso aà pessoal. Temos uma interface de nota fiscal, com interação de combo e cadastro de vários produtos. Uma continuação desse tutorial seria criar um grid de nota fiscais e fazer com que ao inserir uma nova nota fiscal, o grid que estaria abaixo fosse recarregado. Creio que em algum momento você vá utilizar esse tutorial, porque CRUD Pai-Filho é uma situação muito comum de qualquer sistema Ext. Com essa abordagem contornamos qualquer solução que utiliza tabelas temporárias na database ou coisa parecida. Tudo é feito no cliente, salvo em buffer, e só depois enviado ao servidor.
Agradecimento à todos que participaram do post Crud Avançado em destaque ao Miguel Cartagena que foi quem sugeriu o post e interagiu comigo antes que o post fosse publicado.
E não se esqueçam de participar pessoal! Aproveito o post de hoje para agradecer o grande número de comentários, emails e discussões no MSN que estão sendo feitas. Forte abraço e até a próxima!







Fala Bruno!
Mais um vez o tutorial tá excelente, bastante claro e muito útil mesmo, como você disse, essa é uma situação comum em qualquer sistema.
Estou ansioso também pelo material da conferência, deve ser coisa muito boa :)
Parabéns mais um vez!
Abraços
Bruno, mais uma vez parabéns pelo tutorial.
Muito bom, util e profissional. Vlwsssssss
E ai Bruno blz? Estava procurando um totalizador de Grids, acabei encontrando seu post, salvou meu dia, alias noite :D
Abs.
prezado bruno boa tarde, fiz o download do projeto para analisar e estudar, mas estão faltando arquivos e não consegui faze^-lo funcionar. haveria a possibilidade de você me enviar? agradeço desde ja a sua ajuda e colaboração. abraços
Camarada!!! Muito Obrigado! Me ajudou muito!!!