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!!!
Queria tirar uma dúvida… Quando lançado os campos teria como enviar para uma impressão??? Para uma impressora matricial… com um formulário pré impresso???
Vlw agradeço desde já…
De uma tela do Ext direto pra impressora matricial acho bem difícil. Eu usaria o Ext como uma forma usual do usuário preencher uma nota fiscal, e uma tela separada com texto puro pra enviar pra impressora. Tipo, o usuário clica em imprimir e você abre uma pop-up com a nota fiscal na versão impressa, só em texto.
abs!
Bruno, parabéns pelo ótimo material!
Salvou meu dia!!
Um comentário interessante, se você deseja colocar esse formulário em uma janela , ao final da interação, após o sucesso do envio dos dados, você deve fazer algo do tipo
this.grid.store.each(function(record){
this.grid.store.remove(record);
});
E aí quando abrir uma nova janela o formulário e o grid estarão limpos.
Nossa que tutorial show, estava mesmo precisando disso!!
me tira uma duvida? como eu faria para gerar tipo um arquivo de texto ou pdf com as informações dessa nota e envia-la por email??
grande abraço
Impressão é bom manter no bom e velho HTML… Fazer impressão com componentes Ext não fica legal. O jeito mais fácil seria, depois de salva, criar um botão “Gerar PDF” em algum lugar. Quando clicado, ele abre uma window JavaScript padrão mesmo (window.open()), passando o ID da nota. Daí vc gera uma página e dá um window.print(). (: abs!
opa, brigadão Bruno vou tentar quando chegar em casa
um abraço
Primeiro Parabéns pelo Post Bruno.
Minha Dúvida fica a seguinte aqui neste exemplo você está usando o ExtJs 4?
Obrigado Abraços