CRUD Avançado com Ext JS 3.0

CRUD Avançado com Ext JS 3.0

Código completo e demo online no final do post

Nesse tutorial iremos criar um CRUD completo de usuários. CRUD é um acrônimo de Create, Read, Update e Delete, ou seja, as 4 principais ações de um cadastro simples usando banco de dados: Criar, Ler, Atualizar e Deletar. Iremos abordadar conceitos como:

  • Criando um Grid e populando-o com dados do servidor com paginação
  • Abrindo janela pop-up ao clicar em um registro
  • Popular um formulário com base no registro
  • Como carregar corretamente um combo ao popular o formulário
  • Submitando um formulário Ext

O código apresentado não é simplesmente didático, ele é um exemplo da codificação que eu uso em meu dia-a-dia na programação Ext JS. Vou incrementar então nesse tutorial algumas coisas de arquitetura de aplicação Ext e também abordar conceitos de Orientação a Objetos.

Para dar uma visão geral do que será feito estou anexando um print do resultado final. A interface será baseada no outro tutorial meu chamado “Como abrir páginas de um menu no centro de sua aplicação“. O CRUD em si será formado de um grid de usuários, e ao usuário clicar em um registro uma tela de edição irá aparecer. Também usaremos a mesma tela para realizar inclusão de novos registros.

Visão geral do CRUD

Visão geral do CRUD

Preparando Terreno

Como em grande parte dos tutoriais irei utilizar a pasta examples do Ext para armazenamento da minha aplicação. Nela irei criar uma sub-pasta chamada crudUsuarios. Estou utilizando a IDE Aptana Studio e já tenho um projeto preparado para essa aplicação. Também já tenho um site no Apache criado e tudo está funcional. Podemos começar!

Estrutura Final da Aplicação

Estrutura Final da Aplicação

Server-Side

No lado do servidor usaremos PHP e MySQL. Gostaria de criar uma outra versão desse tutorial com ASP.NET e SQLServer mas alguém tem que mostrar interesse :). Não irei fazer nenhum passo-a-passo da programação server-side, estou disponibilizando um .zip com os arquivos e também dois diagramas: um de banco de dados e outro dos fontes PHP.

Diagramas

Diagramas

server-side.zip

server-side.zip

Listagem de Usuários

Para construção desse CRUD partimos do princípio de que cada interface é uma classe, e cada classe deve extender de um componente do Ext. Iremos dividir cada classe em um arquivo, portanto para a interface da listagem de usuários vamos criar uma classe chamada UsuarioLista e vamos extendê-la de Ext.grid.GridPanel. Esse é o ponto fundamental de uma boa arquitetura de aplicação Ext. Fazendo cada interface ser representada por uma classe podemos nos beneficiar de todo o ciclo de vida de um componente Ext (criação, renderizaçãom, destruição…), além de evitar a “misturança” de código que sempre acaba ocorrendo. Como o Ext aborda o conceito de “aplicação-de-uma-só-página” é fundamental estruturar cada interface dessa maneira.

Iremos sobreescrever o método initComponent, que é o metodo invocado para executar configurações no componente, e nele iremos definir um store para o grid, suas colunas e alguns atributos extra. Iremos definir o evento de clique sobre o grid no método initEvents. Veja como está a estrutura básica até agora:

var UsuarioLista = Ext.extend(Ext.grid.GridPanel,{

	//Config Options {

		 border		: false
		,stripeRows	: true
		,loadMask	: true

	//}

	//inits {

		,initComponent: function()
		{
			//store do grid
			this.store = new Ext.data.JsonStore({
				 url			: 'Ajax/UsuarioAjax.php'
				,root			: 'rows'
				,idProperty		: 'usuarioID'
				,totalProperty	: 'totalCount'
				,autoLoad		: true
				,autoDestroy	: true
				,baseParams		: {
					 action	: 'listaUsuarios'
					,limit	: 30
				}
				,fields:[
					 {name:'usuarioID'					, type:'int'}
					,{name:'nome'						, type:'string'}
					,{name:'UF'							, type:'string'}
					,{name:'cidade'						, type:'string'}
					,{name:'nivelHierarquicoID'			, type:'int'}
					,{name:'nivelHierarquicoDescricao'	, type:'string'}
				]
			});

			//demais atributos do grid
			Ext.apply(this,{
				 viewConfig:{
					 emptyText		: 'Nenhum registro encontrado'
				 	,deferEmptyText : false
				 }
				,bbar: new Ext.PagingToolbar({ //paginação
					 store		: this.store
					,pageSize	: 30
				})
				,tbar: ['->',{
					 text	: 'Novo usuário'
					,iconCls: 'silk-add'
					,scope	: this
					,handler: this._onBtnNovoUsuarioClick
				},{
					 text	: 'Excluir Selecionados'
					,iconCls: 'silk-delete'
					,scope	: this
					,handler: this._onBtnExcluirSelecionadosClick
				}]
				,columns:[{
					 dataIndex	: 'nome'
					,header		: 'Nome'
				},{
					 dataIndex	: 'UF'
					,header		: 'Estado'
				},{
					 dataIndex	: 'cidade'
					,header		: 'Cidade'
				},{
					 dataIndex	: 'nivelHierarquicoDescricao'
					,header		: 'Nivel Hierarquico'
				}]
			})

			//super
			UsuarioLista.superclass.initComponent.call(this);
		}

		,initEvents: function()
		{
			//super
			UsuarioLista.superclass.initEvents.call(this);

			this.on({
			 	 scope		: this
				,rowdblclick: this._onGridRowDblClick
			});
		}

	//}

	//Overrides{

		,onDestroy: function(){}

	//}

	//Listeners {

		,_onBtnNovoUsuarioClick: function(){}

		,_onBtnExcluirSelecionadosClick: function(){}

		,_onGridRowDblClick: function( grid, rowIndex, e ) {}

	//}

	//Demais métodos {
	//}

});

Ext.reg('e-usuariolista',UsuarioLista);

Dessa forma o nosso grid já deve estar requisitando dados no servidor e renderizando o grid. Como não temos ainda registros vamos criar a ação do botão “Novo usuário”. Este botão quando clicado irá criar a janela de cadastro e mostrá-la. Como também iremos mostrar essa janela ao editar um registro, vamos encapsular a rotina de criação da janela em um método para evitar repetição de código:

,_onBtnNovoUsuarioClick: function()
{
	//cria janela de cadastro
	this._criaWindowUsuario();

	//seta atributos
	this._winUsuario.setUsuarioID(0);

	//mostra
	this._winUsuario.show();
}

,_criaWindowUsuario: function()
{
	if(!this._winUsuario)
	{
		this._winUsuario = new UsuarioCadastro({
			 renderTo	: this.body //restringe área da janela
			,listeners	:{
				 scope	: this
				,salvar	: this._onCadastroUsuarioSalvarExcluir
				,excluir: this._onCadastroUsuarioSalvarExcluir
			}
		});
	}

	return this._winUsuario;
}

Nesse pedaço de código demonstro um conceito muito importante, o reaproveitamento de janelas. Como essa janela pode ser invocada várias vezes é uma importante otimização criá-la somente uma única vez, guardar sua referência, e depois só ir reciclando ela. Portanto, no método _criaWindowUsuario, eu verifico se a referência da janela existe. Se não existir, eu crio, senão somente retorno a referência. Com a referência em mãos, seja ela nova ou reciclada, iremos setar o id do usuário para a janela e ela irá “se virar” para mostrar as informações corretas. Mas atenção, essa otimização requer que sua janela tenha o config. option closeAction setado para hidden ao inves de destroy (veremos isso na definição da janela).

Assim como criamos uma janela e guardamos sua referência, precisamos também destruí-la e liberar sua referência. Aproveitamos o método onDestroy do grid para fazer isso. Quando o grid for destruído, deve destruir também a janela.

,onDestroy: function()
{
	UsuarioLista.superclass.onDestroy.apply(this,arguments);

	//destrói a janela de usuário e limpa sua referência
	Ext.destroy(this._winUsuario)
	this._winUsuario = null;
}

Além do conceito de reciclagem existe um outro apresentado no código de criação da janela, os eventos personalizados. Veja que ao criar a janela eu associo um listener aos eventos salvar e excluir. Esses eventos não são nativos do Ext. Nós iremos programá-los, para que quando o usuário salve ou exclua um usuário, o grid seja recarregado.

,listeners	:{
	 scope	: this
	,salvar	: this._onCadastroUsuarioSalvarExcluir
	,excluir: this._onCadastroUsuarioSalvarExcluir
}

...

,_onCadastroUsuarioSalvarExcluir: function()
{
	//recarrega grid
	this.store.reload();
}

Janela de Cadastro

A janela de cadastro é outra interface, por isso um outro arquivo e uma outra classe. Vamos criar a classe UsuarioCadastro e extendê-la de Ext.Window. Vamos em initComponent criar o formulário e ainda já definir o método onDestroy liberando as referências realizadas:

var UsuarioCadastro = Ext.extend(Ext.Window,{

	//Config Options {

		//UsuarioCadastro
		 usuarioID: 0

		//super
		,modal		: true
		,constrain	: true
		,maximizable: true
		,width		: 450
		,height		: 300
		,title		: 'Cadastro de Usuário'
		,layout		: 'fit'

		/* Essa janela será reaproveitada, por isso closeAction deve ser HIDE*/
		,closeAction: 'hide'

	//}

	//Acessores {

		,setUsuarioID: function(usuarioID)
		{
			this.usuarioID = usuarioID;
		}

	//}

	//Inits {

		,constructor: function()
		{
			this.addEvents({
				 salvar	: true
				,excluir: true
			});

			//super
			UsuarioCadastro.superclass.constructor.apply(this, arguments);
		}

		,initComponent: function()
		{
			//combo de nivel hierarquico
			this.comboNivelHierarquico = new Ext.form.ComboBox({
				 fieldLabel		: 'Nivel Hierárquico'
				,hiddenName		: 'nivelHierarquicoID'
				,xtype			: 'combo'
				,triggerAction	: 'all'
				,valueField		: 'nivelHierarquicoID'
				,displayField	: 'descricao'
				,emptyText		: 'Selecione um Nível'
				,allowBlank		: false
				,store			: new Ext.data.JsonStore({
					 url		: 'Ajax/NivelHierarquicoAjax.php'
					,baseParams	: {
						action: 'listaNiveisHierarquicos'
					}
					,fields:[
						 {name: 'nivelHierarquicoID', type:'int'}
						,{name: 'descricao'			, type:'string'}
					]
				})
			})

			//formulário
			this.formPanel = new Ext.form.FormPanel({
				 bodyStyle	: 'padding:10px;'
				,border		: false
				,autoScroll	: true
				,defaultType: 'textfield'
				,defaults	: {
					anchor: '-19'
				}
				,items:[{
					 fieldLabel	: 'Nome'
					,name		: 'nome'
					,allowBlank	: false
					,maxLength	: 100
				},{
					 fieldLabel	: 'Estado'
					,name		: 'UF'
					,anchor		: ' '
					,width		: 50
					,maxLength	: 2
				},{
					 fieldLabel	: 'Cidade'
					,name		: 'cidade'
					,maxLength	: 100
				}
				,this.comboNivelHierarquico]
			})

			Ext.apply(this,{
				 items	: this.formPanel
				,bbar	: ['->',{
					 text	: 'Salvar'
					,iconCls: 'icon-save'
					,scope	: this
					,handler: this._onBtnSalvarClick
				},
				this.btnExcluir = new Ext.Button({
					 text	: 'Excluir'
					,iconCls: 'silk-delete'
					,scope	: this
					,handler: this._onBtnDeleteClick
				})
				,{
					 text	: 'Cancelar'
					,iconCls: 'silk-cross'
					,scope	: this
					,handler: this._onBtnCancelarClick
				}]
			})

			//super
			UsuarioCadastro.superclass.initComponent.call(this);
		}

	//}

	//Override {

		,show: function(){

			//super
			UsuarioCadastro.superclass.show.apply(this,arguments);

			//TODO: rotina para carregar formulario
		}

		,onDestroy: function()
		{
			//super
			UsuarioCadastro.superclass.onDestroy.apply(this,arguments);

			this.formPanel = null;
		}

	//}

	//Listeners {

		,_onBtnSalvarClick: function(){}

		,_onBtnDeleteClick: function(){}

		,_onBtnCancelarClick: function(){}

	//}
});

Depois disso sua janela já pode ser visualizada, porém ainda não está funcional. Para incluir um novo usuário devemos definir uma ação para o botão “Salvar”. Perceba os passos realizados:  a validação é realizada, o submit é realizado, e após tudo isso disparamos o evento personalizado salvar. Um único método no Ajax trata da inserção e da atualização, única diferença é que na inserção usuarioID é igual a zero e na atualização não.

,_onBtnSalvarClick: function()
{
	//pego o formulário
	var form = this.formPanel.getForm();

	//verifico se é valido
	if(!form.isValid())
	{
		Ext.Msg.alert('Atenção','Preencha corretamente todos os campos!');
		return false;
	}

	//crio uma máscara
	this.el.mask('Salvando informações');

	/*
	 * Submitando formulário
	 */
	form.submit({
		 url	: 'Ajax/UsuarioAjax.php'
		,params	: {
			 action		: 'criaAtualizaUsuario'
			,usuarioID	: this.usuarioID
		}
		,scope:this
		,success: function() //ao terminar de submitar
		{
			//tirá máscara
			this.el.unmask();

			//esconde janela
			this.hide();

			//evento disparado
			this.fireEvent('salvar',this);
		}
	});
}

Feito isso já estamos aptos a acessar a listagem, clicar em novo usuário, preencher o formulário e salvar. Ao fazer isso a janela será fechada, o grid atualizado e nosso novo registro listado.

Vamos então agora ver como é feita a atualização. Para isso precisamos, na classe UsuarioLista, definir o método que monitora o duplo click nas linhas do grid. Fazemos basicamente o que fizemos ao clicar em novo usuário, porém agora o id do usuário não será mais zero.

,_onGridRowDblClick: function( grid, rowIndex, e )
{
	//busca registro da linha selecionada
	var record = grid.getStore().getAt(rowIndex);

	//extrai id
	var usuarioID = record.get('usuarioID');

	//cria janela de cadastro
	this._criaWindowUsuario();

	//seta atributos
	this._winUsuario.setUsuarioID(usuarioID);

	//mostra
	this._winUsuario.show();
}

A classe UsuarioLista está fazendo sua parte, agora basta a janela fazer a sua. Ao mostrar a janela devemos verificar qual o valor da variável usuarioID. Se for zero, o formulário deve ser resetado (lembra que ele pode ser reciclado e conter valores antigos?). Caso não for zero, o formulário deverá ser populado.

,show: function()
{
	//super
	UsuarioCadastro.superclass.show.apply(this,arguments);

	if(this.usuarioID !== 0) //se tem usuario
	{
		//pode excluir
		this.btnExcluir.show();

		//crio uma máscara
		this.el.mask('Salvando informações');

		/*
		 * Carregando o formulário. Ele deve respeitar algums formatos especificiados na documentação ext de
		 * Ext.form.Action.Load, como por exemplo conter uma propriedade success e data.
		 */
		this.formPanel.getForm().load({
			 url	: 'Ajax/UsuarioAjax.php'
			,params	: {
				 action		: 'buscaUsuario'
				,usuarioID	: this.usuarioID
			}

			/*
			 * Quando o formulário carregar vou tratar problemas de carregar o combo.
			 * e tirar a máscara
			 */
			,scope	: this
			,success: this._onFormLoad
		});
	}
	else //se não existir usuario
	{
		//não pode excluir
		this.btnExcluir.hide();

		/*
		 * Resetando o formulário
		 */
		this.formPanel.getForm().reset();
	}
}

Na rotina que popula o formulário existe um problema com o combo. O store do combo de níveis hierárquicos não foi carregado. O formulário ao ser populado vai tentar setar valor no combo, porém como o store está vazio o próprio ID vai ser atribuído. Resultado disso, temos na interface o id do nivel hierarquico e não a descrição. Esse é um problema muito enfrentado por iniciantes. Existem duas abordagens, carregar o combo de níveis hierarquicos antes do form, ou criar um registro local. Eu prefiro a segunda opção por ter performance melhor. Meu combo pode conter centenas de registros e meu form conter dezenas desses combos. Carregá-los todos de uma vez pode degradar a performance da tela. Por isso junto com os dados do formulário eu também retorno não somente o ID do combo mas também a descrição. Com o id e a descrição em mãos posso criar um registro local:

,_onFormLoad: function(form, request)
{
	var data = request.result.data;

	if( data.nivelHierarquicoID ) //se o registro possui nivelHierarquicoID
	{
		//crio novo registro
		var novoRegistro = new this.comboNivelHierarquico.store.recordType({
			 nivelHierarquicoID	: data.nivelHierarquicoID
			,descricao			: data.nivelHierarquicoDescricao
		});

		//adicionado no store
		this.comboNivelHierarquico.store.insert(0,novoRegistro);

		//e seto o valor
		this.comboNivelHierarquico.setValue(data.nivelHierarquicoID);
	}

	//tiro uma máscara
	this.el.unmask();
}

Até aqui já podemos listar usuários, criar novos usuários, e alterar usuários já registrados, resta somente a remoção de registros. Duas formas são propostas: remover o usuário na tela de seu cadastro, e remover múltiplos usuários na listagem. Ambos os métodos são bem fáceis e vou somente postá-los a vocês.

Em UsuarioLista o método é este:

,_onBtnExcluirSelecionadosClick: function()
{
	//busco selecionados
	var arrSelecionados = this.getSelectionModel().getSelections();

	if( arrSelecionados.length === 0 )
	{
		Ext.Msg.alert('Atenção','Selecione ao menos um registro!')
		return false;
	}

	Ext.Msg.confirm('Confirmação','Deseja mesmo excluir o(s) registro(s) selecionado(s)?',function(opt){

		if(opt === 'no')
			return;

		var usuariosID = [];
		for( var i = 0 ; i < arrSelecionados.length ; i++ )
		{
			usuariosID.push( arrSelecionados[i].get('usuarioID') );
		}

		this.el.mask('Excluindo usuários');

		Ext.Ajax.request({
			 url	: 'Ajax/UsuarioAjax.php'
			,params	: {
				 action			: 'deletaUsuarios'
				,'usuariosID[]'	: usuariosID
			}
			,scope	: this
			,success: function()
			{
				this.el.unmask();
				this.store.reload();
			}
		});

	},this);
}

E em UsuarioCadastro este:

,_onBtnDeleteClick: function()
{
	Ext.Msg.confirm('Confirmação','Deseja mesmo excluir esse registro?',function(opt)
	{
		if(opt === 'no')
			return

		this.el.mask('Excluir usuário.');

		Ext.Ajax.request({
			 url	: 'Ajax/UsuarioAjax.php'
			,params	: {
				 action		: 'deletaUsuario'
				,usuarioID	: this.usuarioID
			}
			,scope	: this
			,success: function()
			{
				this.el.unmask();
				this.hide();

				/*
				 * Evento personalizado excluir sendo disparado
				 */
				this.fireEvent('excluir',this);
			}
		})

	},this)
}

Conclusão

Bem galera, acho que acabei juntando muito conteúdo em um post só porém me esforcei ao máximo para deixar o código bem comentado e o post bem organizado. Espero ter sido o mais completo possível e ter sanado diversas dúvidas. Também espero ter demonstrado como organizar melhor o seu código, fazendo cada interface ser representada por uma classe. Vejo diversos códigos muito complicados em que o programador realiza um monte de carregamento dinâmico de javascript, uma “mistureba” de componentes, utiliza eval e iframes em todo lugar, etc…

O Ext é fácil! O difícil é entender que Javascript não é somente linguagem de validação de formulário e sim uma linguagem que implementa sua maneira de orientação a objetos e por isso necessita de uma abordagem mais profissional.

Estou a disposição aqui através dos comentários, no twitter, no email contato[at]extdesenv.com, no fórum brasileiro (bt_bruno)…enfim, entre em contato! :D E comentem! Seus comentários são incentivo para novos posts! Até a próxima!

server-side.zip

Código Completo

Demo Online

Demo Online

Posts relacionados:

  1. Criando Ext.Window para edição de dados de um grid – CRUD Local
  2. CRUD Pai-Filho: exemplo de nota fiscal
  3. Como criar uma máscara de espera com Ext JS
  4. Paginação de dados com Ext
  5. Cadastro básico com grid e formulário
  • Comentários [73]
    • Share