Back-End

22 jun, 2011

Criando um componente Autocomplete

Publicidade

Salve, pessoal!

Estou voltando a escrever depois de um longo período de jejum nas publicações.

Hoje veremos aqui como desenvolver um componente Autocomplete bem simples e prático. Creio que todos conheçam o componente que a Adobe disponibiliza.

Contudo, o nosso componente irá utilizar os recursos existentes no Flex 4: “Skin”.  O Autocomplete terá como base tres componentes padrões do Flex: PopUpAnchor, List e TextInput

Requisitos:

  • Ter Flex/Flash Builder ou outra IDE.
  • Ter SDK 4
  • Conhecimento Mxml.
  • Conhecimento mínimo sobre Skin

Nivel de dificuldade: 7.5

Passo 1 – Criando o Skin

Bom pessoal, iremos dar inicio ao nosso componente pelo Skin.

Uma breve descrição dos componentes:

  • PopUpAnchor: A função do popupAnchor em nosso componente será simplesmente para abir e fechar a lista dos dados que serão definidos no AutoComplete para filtragem. Este componente possui muitos recursos interessantes que vale a pena ser estudo mais afundo.
  • List: Responsável por armazenar os dados que deverão ser exibidos ao usuário enquanto relializa a filtragem.
  • TextInput: Responsável por capturar os caracteres de parâmetros de busca dos dados. 

Veja abaixo o código integral do Skin.

<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx">

<fx:Metadata>
[HostComponent("br.com.fabielprestes.autoComplete.AutoComplete")]
</fx:Metadata>

<s:states>
<s:State name="Normal"/>
<s:State name="Disable"/>
</s:states>

<s:PopUpAnchor id="puaListaDados" displayPopUp="false" top="0" bottom="0" left="0" right="0"
popUpWidthMatchesAnchorWidth="true" popUpPosition="below" >
<s:List id="listDados" right="0" left="0" labelField="label" visible.Disable="false"
visible.Normal="true"
enabled.Disable="false" enabled.Normal="true"/>
</s:PopUpAnchor>

<s:TextInput id="txtFiltro" right="0" left="0" enabled.Disable="false" enabled.Normal="true"/>

</s:Skin>

Até aqui nada demais, certo?!

Passo 2: Criando o HostComponent

Agora vamos dar inicio a criação do nosso HostComponent que é o responsável por realizar as regras de negócios do componente. Vou dividir este passo em pequenos blocos para explicar melhor cada parte.

Bloco A: Propriedades Required

O bloco abaixo apenas define quais são os componentes padrões/obrigatórios que os SKINS deverão ter ao utilizar o AutoComplete como HostComponente.

/* ============================================================== 
* INICIO: SkinPart obrigaorios
* ============================================================== */
[SkinPart(required="true")]
public var listDados:List;

[SkinPart(required="true")]
public var txtFiltro:TextInput;

[SkinPart(required="true")]
public var puaListaDados:PopUpAnchor;
/* ==============================================================
* INICIO: SkinPart obrigaorios
* ============================================================== */

Bloco B: Propriedades/Variáveis Auxiliares

Abaixo, todas as variáveis que serão utilizadas no comportamento CORE do componente. Parte serão para controle comportamental e outras de armazenamento.

/* ============================================================== 
* INICIO: Variaveis auxiliares para tratar o CORE do componente
* ============================================================== */
/**
* @private
* Indica se a lista das opções do AutoComplete está visivel para o usuário ou não
*/
private var _isListaVisivel:Boolean = false;

/**
* @private
* Armazena a listagem de TODOS dos DADOS que estarão disponiveis para serem filtrados.
*/
private var _dataProvider:ArrayCollection;

/**
* @private
* Armazena a listagem dos DADOS apos a aplicação do filtro com base no digitado no componente TXT_FILTRO.
* Estes dados filtrados serão exibidos para o usuário.
*/
private var _dataProviderFiltered:ArrayCollection;

/**
* @private
* Indica quando os DADOS da listagem foram alterados. Sempre que esta propriedade for alterada o componente
* irá invalidar as propriedades do AutoComplete.
*/
private var _dataProviderChanged:Boolean = false;

/**
* @private
* Define qual item da LISTA está selecionado.
*/
private var _itemSelecionado:Object;

/**
* @private
* Indica quando um Item da lista foi Selecionado. Tambem serve para indicar se o usuário deseja selecionar algum
* algum item que está contido na lista.
*/
private var _itemSelecionadoAlterado:Boolean = false;

/**
* @private
* Equivalente ao labelField do LIST, DROPDOWN, ETC...
*/
private var _palavraChave:String = "label";

/**
* @private
* Armazena qual PROPRIEDADE do Objeto contido no data provider servirá de base para a ordenção dos DADOS .
*/
private var _palavraOrenacao:String;

/**
* @private
* Indica se o usuário alterou a propriedade BASE de ordenação nos dados do DataProvider.
*/
private var _palavraOrenacaoAlterado:Boolean = false;

/**
* @private
* Indica qual o STATE atual em que o componente se encontra.
*/
private var _stateAtual:String = "Normal";

/**
* @private
* Reponsavel por armazenar um SKIN para o componente TXT FILTRO.
*/
private var _textInputSkin:Class;
/* ==============================================================
* FIM: Variaveis auxiliares para tratar o CORE do componente
* ============================================================== */

Pronto, descrito detalhadamente quais as funções e responsabilidades de cada variável no componente.

Bloco C: Implementando o Construtor.

Neste pequeno bloco será realizado algumas codificações dentro do método construtor do componente, como: Criação dos Custons States, configuração do skin padrao e amarrado o evento de criação final do componente.

public function AutoComplete() {
super();

this.setStyle("skinClass", AutoCompleteSkin);

this.addEventListener(FlexEvent.CREATION_COMPLETE, trateCreationComplete);

this.states = this.states.concat(
new State({name: "Normal"}),
new State({name: "Disable"})
);
}

Bloco D: Sobrescrevendo Métodos

Este é um dos trechos mais importantes do Componente, onde são sobrescritos alguns métodos internos para customizar o comportamento para a nossa necessidade.

São eles:

  • set Enabled: Este método é sobrescrito para realizar a troca do state dos componentes internos do AutoComplete.
  • set currentState: Sobrescrito apenas para setar a variável auxiliar “_stateAtual”;
  • getCurrentSkinState: Este método sobrescrivi apenas por questão de um controle maior e possivel alterações.
  • updateDisplayList: ResponsÁvel por interceptar o ciclo de vida do componente. É onde será utilizada a variável auxiliar “textInputSkin” para redefinir o skin do txtFiltro. Basicamente é feito uma verificação a fim de constatar uma alteração no skin atual do componente para o informado na variável, caso tenha diferença deve-se aplicar o novo skin, ou apenas limpar o skin.
  • commitProperties: Pode-se dizer que é aqui que a maior parte do CORE do componente acontece, onde é verificado se a propriedade “_palavraChave” foi alterada. Em caso de afirmação, altera-se no componente LIST. Também responsável por atualizar o dataProvider do componente LIST. Se o usuário alterou a propriedade “palavraOrdenação”. Se o usuário definiu algum ITEM a ser selecionado como default no componente. Ou seja, tem muitas funcionalidades e todas estão devidamente comentadas.
  • partAdded: Onde é feita a amarração dos EVENTOS nos componentes internos e algumas configurações defaults.
  • partRemoved: Responsável por remover os eventos amarrados nos componentes internos.
override public function set enabled(value:Boolean):void{
if(value){
currentState = "Normal";
} else {
currentState = "Disable";
}
}

override public function set currentState(value:String):void {
super.currentState = value;
_stateAtual = value;
}

override protected function getCurrentSkinState():String {
return _stateAtual;
}

override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void{
super.updateDisplayList(unscaledWidth, unscaledHeight);

if(_textInputSkin != null && this.txtFiltro.getStyle("skinClass") != _textInputSkin){
this.txtFiltro.setStyle("skinClass", _textInputSkin);
this.txtFiltro.validateNow();
}
}

override protected function commitProperties():void{
super.commitProperties();

/* Verifica se a propriedade 'labelField' é diferente da definida pelo usuário.
* Se Sim: Altera a mesma*/
if(_palavraChave != listDados.labelField){
listDados.labelField = _palavraChave;
}

/* Verifica se o dataprovider do componente foi alterado.
* Se Sim: Faz o vinculo do novo dataProvider e realiza o refresh do filter function */
if(_dataProviderChanged){
_dataProviderFiltered = new ArrayCollection(_dataProvider.source as Array);
_dataProviderFiltered.filterFunction = functionFiltroDados;

listDados.dataProvider = _dataProviderFiltered;

_dataProviderChanged = false;
}

if(_palavraOrenacaoAlterado && _palavraOrenacao != null && _dataProviderFiltered){
_dataProviderFiltered = FuncoesArray.sort(_dataProviderFiltered, palavraOrenacao);
}

/* Verifica se o Item selecionado foi alterado. Isso acontece quando o usuario
* informa um valor/objeto que contenha no DataProvider. Se existir o item selecionado irá
* ser exibido no componente textinput */
if(_itemSelecionadoAlterado && _dataProviderFiltered){
if(itemSelecionado is String){
this.txtFiltro.text = itemSelecionado as String;
} else if(itemSelecionado is Object){
this.txtFiltro.text = itemSelecionado[palavraChave];

for each(var obj:Object in _dataProvider){
if(itemSelecionado[palavraChave] == obj[palavraChave]){
_itemSelecionado = obj;
break;
}
}
}

this.txtFiltro.selectRange(this.txtFiltro.text.length, this.txtFiltro.text.length);

_itemSelecionadoAlterado = false;
_dataProviderFiltered.refresh()
}
}

override protected function partAdded(partName:String, instance:Object):void {
super.partAdded(partName, instance);

/* Verifica o tipo da instancia do PartAdicionado */
if (instance == txtFiltro) {
/* Adiciona os eventos ao componente Filtro para pesquisa */
txtFiltro.addEventListener(TextOperationEvent.CHANGE, trateTxtFiltroTextAlterado);
txtFiltro.addEventListener(KeyboardEvent.KEY_DOWN, trateTxtFiltroKeyDown);
txtFiltro.addEventListener(MouseEvent.CLICK, trateTxtFiltroClicked);
txtFiltro.addEventListener(FocusEvent.FOCUS_OUT, trateTxtFiltroFocusOut);
} else if (instance == listDados) {
/* Adiciona os eventos ao componente Filtro para pesquisa */
listDados.addEventListener(KeyboardEvent.KEY_UP, trateListDadosKeyUp);
listDados.addEventListener(MouseEvent.CLICK, trateListDadosClicked, false, 10);
}
}

override protected function partRemoved(partName:String, instance:Object):void {
super.partRemoved(partName, instance);

/* Verifica o tipo da instancia do PartAdicionado */
if (instance == txtFiltro) {
/* Remove os eventos */
txtFiltro.removeEventListener(TextOperationEvent.CHANGE, trateTxtFiltroTextAlterado);
txtFiltro.removeEventListener(KeyboardEvent.KEY_DOWN, trateTxtFiltroKeyDown);
txtFiltro.removeEventListener(MouseEvent.CLICK, trateTxtFiltroClicked);
txtFiltro.addEventListener(FocusEvent.FOCUS_OUT, trateTxtFiltroFocusOut);
} else if (instance == listDados) {
/* Adiciona os eventos ao componente Filtro para pesquisa */
listDados.removeEventListener(KeyboardEvent.KEY_UP, trateListDadosKeyUp);
listDados.removeEventListener(MouseEvent.CLICK, trateListDadosClicked);
}
}

Bloco E: Implementação do addEventListeners

No trecho abaixo, seguem todas as implementações dos métodos que foram amarrados aos componentes internos do AutoComplete. Trata-se de um outro ponto importante para o funcionamento do AutoComplete.

Eventos:

  • trateCreationComplete: Chamado quando o AutoComplete foi totalmente criado. Aqui é onde recuperamos o “systemManager” da aplicação para então adicionarmos um event listener de MouseUP. Isso foi feito para saber quando o usuário clicou em algum ponto da aplicação para então disparar o método que fecha o AutoComplete caso esteja visível.
  • trateSandrboxDown: Evento amarado no “SystemManager” descrito acima. Apenas chama o método que inicia o processo de fechamento do AutoComplete.
  • trateListDadosClicked: Fica escutando quando o usuário clicou em algum item do dataProvider do componente de LIST.
  • trateListDadosKeyUp: Fica escutando os eventos de teclado no componente LIST.
  • trateTxtFiltroClicked: Fica escutando quando o usuário clicou no componente filtro. Aqui é definido o FOCUS no componente e depois definido o range na posição final do FILTRO.
  • trateTxtFiltroKeyDown: Fica escutando quando o usuário pressionou a tecla DOWN com focus no componente TXT FILTRO. Este evento faz com que o primeiro item da lista de dados seja selecionado para movimentação do teclado.
  • trateTxtFiltroFocusOut: Responsável por escutar e tratar quando o componente perder o Focus. Inicia o processo de fechamento do componente.
  • trateTxtFiltroTextAlterado: Responsável por escutar e tratar cada troca de DATA do TextInput. Aqui da inicio ao processo de filtragem dos dados no componente LIST.
/* ============================================================== 
* INICIO: Tratando os eventos dos EventListeners dos skin part
* ============================================================== */
/**
* @private
* Fica escutando quando o componente foi criado por complete. Sempre chamado no final do seu ciclo de criação.
* @param evt
*/
private function trateCreationComplete(evt:FlexEvent):void{
/* Adiciona um Evento Diretamente no System Manager */
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_UP, trateSandrboxDown);
}

/**
* @private
* Fica escutando quando o usuario clicou em algum lugar da aplicação.
* @param evt
*/
private function trateSandrboxDown(evt:MouseEvent):void{
fecharListaDados();
}

/**
* Fica escutando quando o usuario clicou em algum item do dataProvider.
* Este evento faz com que o item selecionado seja armazenado na propriedade itemSelecionado
* e a lista de opções do dataProvider seja fechada.
*
* @param evt
*/
private function trateListDadosClicked(evt:MouseEvent):void{
this.itemSelecionado = this.listDados.selectedItem;
fecharListaDados();

this.dispatchEvent(new AutoCompleteEvent(AutoCompleteEvent.ITEM_SELECIONADO_EVENT));
}

/**
* Fica escutando quando o usuario pressionou a tecla ENTER com focus no componente LIST DADOS
* Este evento faz com que o primeiro item da lista de dados seja selecionado para movimentação do teclado.
*
* @param evt
*/
private function trateListDadosKeyUp(evt:KeyboardEvent):void{
/* Verifica se o usuario pressionou a tecla enter
* SE SIM: Recupera o Objeto e fecha o componente
* SE NAO: Passa direto */
if(evt.keyCode == Keyboard.ENTER){
itemSelecionado = this.listDados.selectedItem;
fecharListaDados();
this.dispatchEvent(new AutoCompleteEvent(AutoCompleteEvent.ITEM_SELECIONADO_EVENT));
} else if(evt.keyCode == Keyboard.ESCAPE){
this.fecharListaDados();
this.txtFiltro.setFocus();

evt.stopImmediatePropagation();
}
}

/**
* Fica escutando quando o usuario clicou no componente filtro.
* Aqui é definido o FOCUS no componente e depois definido o range na posição final do FILTRO.
* @param evt
*/
private function trateTxtFiltroClicked(evt:MouseEvent):void{
this.txtFiltro.setFocus();
if(this.txtFiltro.text != null){
this.txtFiltro.selectRange(0,this.txtFiltro.text.length);
}
}

/**
* Fica escutando quando o usuario pressionou a tecla DOWN com focus no componente TXT FILTRO
* Este evento faz com que o primeiro item da lista de dados seja selecionado para movimentação do teclado.
*
* @param evt
*/
private function trateTxtFiltroKeyDown(evt:KeyboardEvent):void{
if(evt.keyCode == Keyboard.DOWN){
abrirListaDados();

this.listDados.setFocus();
this.listDados.selectedIndex = 0;
} else if(evt.keyCode == Keyboard.ESCAPE){
this.fecharListaDados();
this.txtFiltro.setFocus();
}
else if(evt.keyCode == Keyboard.ENTER){
this.fecharListaDados();
this.txtFiltro.setFocus();

this.dispatchEvent(new AutoCompleteEvent(AutoCompleteEvent.FILTRO_ENTER_EVENT));
}
evt.stopImmediatePropagation();
evt.preventDefault();
}

/**
* Responsavel por escutar e tratar quando o componente perder o Focus.
*
* @param evt
*/
protected function trateTxtFiltroFocusOut(evt:FocusEvent):void{
if (evt.target != txtFiltro.textDisplay && evt.target != txtFiltro && evt.target != listDados){
fecharListaDados();
}
}

/**
* Responsavel por escutar e tratar cada troca de DATA do TextInput.
* Aqui da inicio ao processo de filtragem dos dados no componente LIST
*
* @param evt
*/
protected function trateTxtFiltroTextAlterado(evt:TextOperationEvent):void {
if (txtFiltro.text != '') {
abrirListaDados();
}
if(_dataProviderFiltered){
_dataProviderFiltered.refresh();
}
}
/* ==============================================================
* FIm: Tratando os eventos dos EventListeners dos skin part
* ============================================================== */

Bloco F: Implementação do métodos CORE

/* ============================================================== 
* INICIO: Tratando os métodos de core do sistema
* ============================================================== */
/**
* Realiza o Filtro na lista dos dados do componente.
*
* @param item a ser comparado no filtro.
* @return
*/
private function functionFiltroDados(item:Object):Boolean {
return functionFiltroPadrao(txtFiltro.text, item);
}

/**
*
* @param palavraFiltro String que será a base do da busca do filtro.
* @param item Item que deve ser comparado com a palavraFiltro
* @return
*/
private function functionFiltroPadrao(palavraFiltro:String, item:Object):Boolean {
var match:Array = String(item[palavraChave]).match(new RegExp(palavraFiltro, 'i'));
return (match && match.length > 0);
}

/**
* Responsavel por definir a lista de dados que deverão ser exibidos no componente
* @param value
*/
public function set dataProvider(value:ArrayCollection):void{
_dataProvider = value;
_dataProviderChanged = true;

invalidateProperties();
}

/**
* Retorna a lista dos dados que foi definida para se exibida no componente
* @return Lista dos dados.
*/
public function get dataProvider():ArrayCollection{
return _dataProvider;
}

/**
* Metodo responsavel por abrir a LISTA que exibe todos os dados filtrados.
*/
public function abrirListaDados():void{
if (!_isListaVisivel) {
_isListaVisivel = true;
puaListaDados.displayPopUp = true;
}
}

/**
* Metodo responsavel por fechar a LISTA que exibe todos os dados filtrados.
*/
public function fecharListaDados():void{
if (_isListaVisivel) {
_isListaVisivel = false;
puaListaDados.displayPopUp = false;
}
}

public function set itemSelecionado(value:Object):void{
if(value){
_itemSelecionado = value;
_itemSelecionadoAlterado = true;

invalidateProperties();
} else {
limpar();
}
}

public function get itemSelecionado():Object{
return _itemSelecionado;
}

public function set palavraChave(value:String):void{
_palavraChave = value;

invalidateProperties();
}

public function get palavraChave():String{
return _palavraChave;
}

public function set palavraOrenacao(value:String):void{
_palavraOrenacao = value;
_palavraOrenacaoAlterado = true;

invalidateProperties();
}

public function get palavraOrenacao():String{
return _palavraOrenacao;
}

public function limpar():void{
_itemSelecionado = null;
txtFiltro.text = "";
}

/**
* Define um SKIN para o campo de filtro;
* @param value
*/
public function set txtFiltroSkin(value:Class):void{
_textInputSkin = value;

invalidateDisplayList();
}

public function get txtFiltroSkin():Class{
return _textInputSkin;
}
/* ==============================================================
* FIM: Tratando os metodos de core do sistema
* ============================================================== */

Aí está o nosso HostComponent completo.

Passo 3: Classe Event

Apesar de termos criado uma Skin e um HostComponent, ainda ficará faltando um ponto chave: a comunicação via event do nosso AutoComplete como restante de nossa aplicação.

package br.com.klarix.components.autoCompleteV2 {

import flash.events.Event;

public class AutoCompleteEvent extends Event {

public static const ITEM_SELECIONADO_EVENT:String = "itemSelecionadoEvent";
public static const FILTRO_FOCUS_OUT_EVENT:String = "filtroFocusOutEvent";
public static const FILTRO_ENTER_EVENT:String = "filtroEnterEvent";

public function AutoCompleteEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false) {

super(type, bubbles, cancelable);
}
}
}

Trabalho finalizado! Agora aproveito pra dizer que este componente ainda pode ser melhorado, mas serve muito bem como ponto de partida para suas customizações.

Caso tenham sugestões de melhoria ou dúvidas, deixem seus comentários!

Download aqui.