Entre os vários novos recursos do JSF 2, provavelmente dois dos mais
instigantes são os componentes compostos e o suporte Ajax. Mas sua força
é mais aparente quando ambos são combinados, facilitando a
implementação de componentes personalizados habilitados pelo Ajax com um
mínimo de trabalho.
Neste artigo, vamos mostrar como implementar um componente de
preenchimento automático que usa o Ajax para administrar sua lista de
itens de preenchimento. E ao fazê-lo, veremos como é possível integrar o
Ajax a seus próprios componentes compostos.
O código desta série é baseado no JSF 2 rodando em um contêiner corporativo, como o GlassFish ou Resin. A última parte deste artigo é um tutorial passo a passo para instalar e rodar o código do artigo com o GlassFish.
Um componente JSF personalizado de preenchimento automático
Famosos pelo campo de busca do Google, os campos de preenchimento
automático (também conhecidos como caixas de sugestão), são um item
básico de muitos aplicativos da Web. Eles também são um caso de uso
típico do Ajax. Os campos de preenchimento automático vêm com a maioria
das estruturas Ajax, como Scriptaculous e JQuery, como a coleção de
componentes de preenchimento automático AjaxDaddy mostrada na Figura 1 atesta:
Figura 1. Componentes de preenchimento automático AjaxDaddy
Este artigo vai explorar um modo de implementar um campo de
preenchimento automático habilitado por Ajax com o JSF. Veremos como
implementar o campo de preenchimento automático mostrado na Figura 2, que mostra uma lista curta de países fictícios (selecionados do artigo “Lista de países fictícios” da Wikipédia. Consulte a seção Recursos ao final do artigo para maiores informações):
Figura 2. O campo de preenchimento automático
A Figura 3 e a Figura 4 mostram o campo de preenchimento automático em ação. Na Figura 3, quando Al é digitado no campo, a lista de países é reduzida a nomes que começam com estas duas letras:
Figura 3. Itens de preenchimento que começam com Al
Da mesma forma, a Figura 4 mostra os resultados quando Bar é digitado no campo. A lista só mostra os nomes de países que começam com Bar:
Figura 4. Itens de preenchimento que começam com Bar
Usando o componente de preenchimento automático
O campo de preenchimento automático Locais é um componente composto JSF e é usado em um facelet, como mostrado na Listagem 1:
Listagem 1. O facelet
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:util="http://java.sun.com/jsf/composite/util">
<h:head>
<title>#{msgs.autoCompleteWindowTitle}</title>
</h:head> <h:body>
<div style="padding: 20px;">
<h:form>
<h:panelGrid columns="2">
#{msgs.locationsPrompt}
<util:autoComplete value="#{user.country}"
completionItems="#{autoComplete.countries}" />
</h:panelGrid>
</h:form>
</div>
</h:body>
</html>
O facelet na Listagem 1 usa o componente composto autoComplete declarando um espaço de nomes apropriado ? util ? e usando o marcador associado ao componente, <util:autoComplete>.
Note os dois atributos do <util:autoComplete> marcador da Listagem 1:
- value é a propriedade do país de um bean gerenciado cujo nome é user.
- completionItems é o conjunto inicial de itens de preenchimento do campo.
A classe User é um bean gerenciado simples, obviamente inventado apenas para esta ocasião. Seu código é mostrado na Listagem 2:
Listagem 2. A classe User
package com.corejsf; import java.io.Serializable;
import javax.inject.Named;
import javax.enterprise.context.SessionScoped;
@Named() @SessionScoped
public class User implements Serializable
{ private String country;
public String getCountry()
{ return country; }
public void setCountry(String country)
{ this.country = country; } }
Observe a anotação @Named, que junto com @SessionScoped, instancia um bean gerenciado nomeado user e o coloca no escopo da sessão na primeira vez que o JSF encontra #{user.country} em um facelet. A única referência deste aplicativo a #{user.country} ocorre na Listagem 1, onde eu especifico a country propriedade do user bean gerenciado como o valor do componente <util:autoComplete>.
A Listagem 3 mostra a classe AutoComplete que define a propriedade countries que especifiquei como a lista do componente de preenchimento automático dos itens de preenchimento:
Listagem 3. Os itens de preenchimento
package com.corejsf;
import java.io.Serializable;
import javax.enterprise.context.ApplicationScoped; import javax.inject.Named;
@Named @ApplicationScoped public class AutoComplete implements Serializable
{ public String[] getLocations()
{ return new String[]
{
"Abari", "Absurdsvanj", "Adjikistan", "Afromacoland",
"Agrabah", "Agaria", "Aijina", "Ajir", "Al-Alemand",
"Al Amarja", "Alaine", "Albenistan", "Aldestan",
"Al Hari", "Alpine Emirates", "Altruria",
"Allied States of America", "BabaKiueria", "Babalstan",
"Babar's Kingdom","Backhairistan", "Bacteria",
"Bahar", "Bahavia", "Bahkan", "Bakaslavia",
"Balamkadar", "Baki", "Balinderry", "Balochistan",
"Baltish", "Baltonia", "Bataniland, Republic of",
"Bayview", "Banania, Republica de", "Bandrika",
"Bangalia", "Bangstoff", "Bapetikosweti", "Baracq",
"Baraza", "Barataria", "Barclay Islands",
"Barringtonia", "Bay View", "Basenji",
}; } }
Isso é tudo sobre o uso do componente de preenchimento automático. Agora vejamos como ele funciona.
Como funciona o componente de preenchimento automático
O componente de preenchimento automático é um componente composto JSF
2, que como a maioria dos componentes compostos, é implementado em um
arquivo XHTML. O componente consiste em uma entrada de texto e uma caixa
de listagem, e algum JavaScript. Inicialmente, o style da caixa de listagem é display: none, que faz a caixa de listagem ficar invisível.
O componente de preenchimento automático responde a três eventos:
- keyup eventos na entrada de texto
- blur (perdendo o foco) eventos na entrada de texto
- change (seleção) eventos na caixa de listagem
Quando o usuário digita a entrada de texto, o componente de
preenchimento automático chama uma função JavaScript para cada evento keyup. Esta função reúne os eventos de batida de tecla para fazer não mais que uma chamada Ajax a cada 350 ms.
Assim, em resposta a keyup
eventos na entrada de texto, o componente de preenchimento automático
faz uma chamada Ajax, no máximo a cada 350 ms, para o servidor. (Tudo
isso para impedir os datilógrafos rápidos de inundar o servidor com
chamadas Ajax. Na prática, os eventos reunidos podem ser superavaliados
neste caso, mas oferecem uma oportunidade de ilustrar eventos reunidos
no JavaScript, que em geral é uma ferramenta útil.)
Quando o usuário seleciona um item na caixa de listagem, o componente
de preenchimento automático faz outra chamada Ajax ao servidor.
Ambas a entrada de texto e a caixa de listagem têm listeners anexados
a elas, que fazem a maior parte do trabalho significativo no servidor
durante as chamadas Ajax. Em resposta a eventos keyup, o
listener da entrada de texto atualiza os itens de preenchimento da caixa
de listagem. Em resposta aos eventos de seleção da caixa de listagem, o
listener dessa caixa copia o item selecionado para a entrada de texto e
a oculta.
Agora que temos uma boa ideia de como o componente de preenchimento
automático funciona, estamos prontos para dar uma olhada em sua
implementação.
Implementando o componente de preenchimento automático
A implementação do componente de preenchimento automático consiste nestes artefatos:
- Um componente composto
- Um punhado de funções JavaScript
- Um listener de mudança de valor que atualiza itens de preenchimento
Vamos começar com o componente composto na Listagem 4:
Listagem 4. O componente autoComplete
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:composite="http://java.sun.com/jsf/composite">
<!-- INTERFACE -->
<composite:interface>
<composite:attribute
name="value"
required="true"/>
<composite:attribute
name="completionItems"
required="true"/>
</composite:interface>
<!-- IMPLEMENATION -->
<composite:implementation>
<div id="#{cc.clientId}">
<h:outputScript library="javascript"
name="prototype-1.6.0.2.js" target="head"/>
<h:outputScript library="javascript"
name="autoComplete.js" target="head"/>
<h:inputText id="input" value="#{cc.attrs.value}"
onkeyup="com.corejsf.updateCompletionItems(this, event)"
onblur="com.corejsf.inputLostFocus(this)"
valueChangeListener=
"#{autocompleteListener.valueChanged}"/>
<h:selectOneListbox id="listbox" style="display: none"
valueChangeListener=
"#{autocompleteListener.completionItemSelected}">
<f:selectItems
value="#{cc.attrs.completionItems}"/>
<f:ajax render="input"/>
</h:selectOneListbox> <div>
</composite:implementation>
</ui:composition>
Três coisas estão acontecendo na seção de implementação da Listagem 4. Primeiro, o componente faz chamadas Ajax em resposta a keyup
eventos na entrada de texto, e oculta a caixa de listagem quando a
entrada de texto perde o foco em virtude de funções JavaScript
atribuídas a keyup e eventos blur na entrada de texto.
Segundo, o componente faz chamadas Ajax em resposta a eventos change na caixa de listagem com o marcador <f:ajax>
do JSF 2. Quando o usuário faz uma seleção na caixa de listagem, o JSF
faz uma chamada Ajax para o servidor e atualiza o valor da entrada de
texto quando a chamada Ajax retorna.
Terceiro, tanto a entrada de texto quanto a caixa de listagem têm
métodos de listener de mudança de valor anexados a elas, assim quando o
JSF fizer chamadas Ajax em resposta ao usuário que digita a entrada de
texto, o JSF invoca o listener de mudança de valor da entrada de texto
no servidor. Quando o usuário seleciona um item na caixa de listagem, o
JSF faz uma chamada Ajax para o servidor e invoca o listener de mudança
de valor da caixa de listagem.
Agrupa componentes compostos em um <div>
O componente composto da Listagem 4 agrupa sua implementação em um <div>
com o identificador de cliente do componente composto. Isso permite que
outros componentes façam referência ao componente de preenchimento
automático por meio de seu ID de cliente. Por exemplo, outro componente
pode querer executar ou apresentar um ou mais componentes de
preenchimento automático durante uma chamada Ajax.
A Listagem 5 mostra o JavaScript usado pelo componente de preenchimento automático:
Listagem 5. O JavaScript
if (!com) var com = {}
if (!com.corejsf) {
var focusLostTimeout com.corejsf =
{ errorHandler : function(data)
{
alert("Error occurred during Ajax call: " + data.description)
},
updateCompletionItems :
function(input, event) {
var keystrokeTimeout
jsf.ajax.addOnError(com.corejsf.errorHandler)
var ajaxRequest = function() {
jsf.ajax.request(input, event, {
render: com.corejsf.getListboxId(input),
x: Element.cumulativeOffset(input)[0],
y: Element.cumulativeOffset(input)[1]
+ Element.getHeight(input) })
} window.clearTimeout(keystrokeTimeout)
keystrokeTimeout = window.setTimeout(ajaxRequest, 350)
},
inputLostFocus : function(input) {
var hideListbox = function() {
Element.hide(com.corejsf.getListboxId(input))
}
focusLostTimeout = window.setTimeout(hideListbox, 200)
}, getListboxId : function(input)
{ var clientId = new String(input.name)
var lastIndex = clientId.lastIndexOf(':')
return clientId.substring(0, lastIndex) + ':listbox'
} } }
O JavaScript da Listagem 5 consiste em três funções que eu coloquei dentro de um espaço de nomes chamado com.corejsf.
Eu implementei o espaço de nomes (que é tecnicamente um objeto literal
do JavaScript) para evitar que alguém acidentalmente (ou não) derrote
quaisquer de minhas três funções.
Se essas funções não estivessem ocultas dentro de com.corejsf, alguém poderia implementar sua própria função updateCompletionItems,
substituindo assim minha implementação pela dele.
É possível que alguma
biblioteca JavaScript possa implementar uma função chamada updateCompletionItems, mas vale a pena apostar que ninguém vai aparecer com com.corejsf.updateCompletionItems. (Em retrospectiva, deixar o com para ficar com corejsf.updateCompletionItems provavelmente teria sido suficiente, mas às vezes é fácil se exceder.)
Assim, o que fazem as funções? A função updateCompletionItems() faz um pedido Ajax ao servidor ? chamando a função do JSF jsf.ajax.request() ? pedindo que o JSF só apresente o componente da caixa de listagem quando a chamada Ajax retornar. A função updateCompletionItems() também passa dois parâmetros extras para o jsf.ajax.request(): as coordenadas x e y do canto superior esquerdo da caixa de listagem. A função jsf.ajax.request() transforma esses parâmetros de função em parâmetros de pedido, que ela envia com a chamada Ajax.
O JSF chama a função inputLostFocus() quando a entrada de texto perder o foco. Esta função simplesmente oculta a caixa de listagem, usando o objeto do Protótipo Element.
Ambos updateCompletionItems() e inputLostFocus()
armazenam sua funcionalidade em uma função. Então eles programam suas
funções para executar em 350 ms e 200 ms, respectivamente. Em outras
palavras, cada função tem um trabalho a fazer, mas ela atrasa aquela
tarefa durante 350 ms ou 200 ms.
A entrada de texto é atrasada após um
evento keyup, de forma que o método updateCompletionItems()
envie o pedido Ajax uma vez a cada 350 ms no máximo. A ideia é que se o
usuário for um datilógrafo (extremamente!) rápido, seja possível evitar
inundar o servidor com chamadas Ajax.
A função inputLostFocus, chamada quando a entrada de
texto perde o foco, atrasa sua tarefa em 200 ms. Este atraso é
necessário porque o valor será copiado para fora da caixa de listagem
quando a chamada Ajax retornar, e essa caixa deve estar visível para que
isso funcione.
Finalmente, note a função getListBoxId(). Esta função
auxiliar obtém o identificador do cliente da caixa de listagem a partir
do identificador do cliente da entrada de texto. A função pode fazer
isso porque está em conluio com o componente autoComplete da Listagem 4.
O componente autoComplete atribui input e listbox como os identificadores de componente para a entrada de texto e a caixa de listagem respectivamente, assim a função getListBoxId() somente corta fora input e acrescenta listbox para ir do identificador do cliente da entrada de texto para o da caixa de listagem.
A Listagem 6 mostra a implementação do listener que reúne tudo:
Listagem 6. O listener
package com.corejsf;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List; import java.util.Map;
import javax.enterprise.context.SessionScoped;
import javax.faces.component.UIInput;
import javax.faces.component.UISelectItems;
import javax.faces.component.UISelectOne;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;
import javax.inject.Named;
@Named @SessionScoped public class
AutocompleteListener implements Serializable
{ private static String COMPLETION_ITEMS_ATTR = "corejsf.completionItems";
public void valueChanged(ValueChangeEvent e) {
UIInput input = (UIInput)e.getSource();
UISelectOne listbox = (UISelectOne)input.findComponent("listbox");
if (listbox != null) {
UISelectItems items = (UISelectItems)listbox.getChildren().get(0);
Map<String, Object> attrs = listbox.getAttributes();
List<String> newItems = getNewItems((String)input.getValue(),
getCompletionItems(listbox, items, attrs));
items.setValue(newItems.toArray());
setListboxStyle(newItems.size(), attrs);
} }
public void completionItemSelected(ValueChangeEvent e)
{ UISelectOne listbox = (UISelectOne)e.getSource();
UIInput input = (UIInput)listbox.findComponent("input");
if(input != null) {
input.setValue(listbox.getValue()); }
Map<String, Object> attrs = listbox.getAttributes();
attrs.put("style", "display: none"); }
private List<String>
getNewItems(String inputValue, String[] completionItems)
{ List<String> newItems = new ArrayList<String>();
for (String item : completionItems) {
String s = item.substring(0, inputValue.length());
if (s.equalsIgnoreCase(inputValue))
newItems.add(item); }
return newItems; }
private void setListboxStyle(int rows, Map<String, Object> attrs)
{ if (rows > 0) {
Map<String, String> reqParams = FacesContext.getCurrentInstance()
.getExternalContext().getRequestParameterMap();
attrs.put("style", "display: inline; position: absolute; left: "
+ reqParams.get("x") + "px;" + " top: " + reqParams.get("y") + "px");
attrs.put("size", rows == 1 ? 2 : rows); }
else
attrs.put("style", "display: none;"); }
private String[] getCompletionItems(UISelectOne listbox,
UISelectItems items, Map<String, Object> attrs)
{ Strings] completionItems = (String[])
attrs.get(COMPLETION_ITEMS_ATTR);
if (completionItems == null) {
completionItems = (String[])items.getValue();
attrs.put(COMPLETION_ITEMS_ATTR, completionItems);
} return completionItems; } }
O JSF invoca o método do listener valueChanged() durante chamadas Ajax em resposta a eventos keyup
na entrada de texto. Este método cria um novo conjunto de itens de
preenchimento, e então define os itens da caixa de listagem para este
novo conjunto. O método também define atributos de estilo para a caixa
de listagem, que determinam se a caixa de listagem é exibida quando a
chamada Ajax retorna.
O método setListboxStyle() na Listagem 6 usa os valores de parâmetros de pedido x e y que especifiquei quando fiz a chamada Ajax da Listagem 5.
O JSF invoca o único outro método público do listener completionItemSelected()
durante chamadas Ajax, em resposta a eventos de seleção na caixa de
listagem. Este método copia o valor da caixa de listagem para a entrada
de texto e oculta a caixa de listagem.
Note que o método valueChanged() também armazena os itens de preenchimento originais em um atributo da caixa de listagem. Como cada componente autoComplete mantém sua própria lista de itens de preenchimento, múltiplos componentes autoComplete podem coexistir pacificamente na mesma página sem prejudicar os itens de preenchimento uns dos outros.
Rodando os exemplos com GlassFish e Eclipse
O código desta série de artigos é mais bem adequado a um contêiner
JEE 6, como GlassFish ou Resin. É possível fazer as coisas funcionarem
com um contêiner de servlet como o Tomcat, mas observe a parte de “fazer
as coisas funcionarem”. Como minha meta é extrair todo o potencial do
JSF 2 e JEE 6, e não abordar questões de configuração, vamos ficar com o
GlassFish v3.
No restante deste artigo, vamos mostrar como rodar a amostra de código
usando o GlassFish v3 e Eclipse. As instruções aqui também serão
suficientes para o código do resto desta série de artigos. (Estou usando
o Eclipse 3.4.1, assim quanto mais perto for possível ficar disso ao
rodar os exemplos, melhor.)
A Figura 5 mostra a estrutura de diretórios encontrada no código deste artigo. (Ao final deste texto, visite a seção Download
para obter o código agora.) Há um diretório de preenchimento automático
que contém o aplicativo e um diretório da área de trabalho vazio para o
Eclipse.
Figura 5. Código fonte no download deste artigo
Agora que já temos o código, estamos quase prontos para rodá-lo.
Primeiro é preciso ter o plug-in do GlassFish Eclipse, que está
disponível em https://glassfishplugins.dev.java.net, mostrado na Figura 6:
Figura 6. O plug-in do GlassFish Eclipse
Basta seguir as instruções de instalação do plug-in, e estaremos prontos para começar.
Para instalar o código deste artigo, crie um projeto de Web Dinâmica no Eclipse. É possível fazer isso no menu File > New: se o projeto de Web Dinâmica não for exibido ali, selecione Other, e na caixa de diálogo resultante abra a pasta Web e selecione Dynamic Web Project, como mostrado na Figura 7:
Figura 7. Criando um projeto de Web Dinâmica
A próxima etapa é configurar o projeto. Faça as seguintes seleções na primeira tela do assistente New Dynamic Web Project, como mostrado na Figura 8:
- Em Project contents, deixe a caixa Use default desmarcada. No campo Directory, digite (ou navegue até) o diretório autoComplete da amostra de código.
- Para Target Runtime, selecione GlassFish v3 Java EE 6.
- Para Dynamic Web Module version, digite 2.5.
- Para Configuration, selecione Default Configuration for GlassFish v3 Java EE 6.
- Em EAR Membership, deixe a caixa Add project to an EAR desmarcada e digite autoCompleteEAR no campo EAR Project Name:
Figura 8. Configurando o aplicativo, etapa 1
Clique em Next, e então digite os valores mostrados na Figura 9:
- Para Context Root: digite autoComplete.
- Para Content Directory: digite web.
- Para Java Source Directory: digite src/java. Deixe a caixa Generate deployment descriptor desmarcada.
Figura 9. Configurando o aplicativo, etapa 2
Agora devemos ter um projeto autoComplete visível na tela do Project Explorer do Eclipse, como mostrado na Figura 10:
Figura 10. O projeto autoComplete
Agora selecione o projeto, clique com o botão direito nele, e selecione Run on Server, como mostrado na Figura 11:
Figura 11. Rodando no servidor do Eclipse
Selecione GlassFish v3 Java EE 6 na lista de servidores da caixa de diálogo Run On Server, mostrada na Figura 12:
Figura 12. Selecionando o GlassFish
Clique em Finish. O Eclipse deve iniciar o GlassFish, e subsequentemente o aplicativo autoComplete, como mostrado na Figura 13:
Figura 13. Rodando no Eclipse
Conclusão
Com o JSF 2 fica fácil criar poderosos componentes personalizados
habilitados pelo Ajax. Não é preciso implementar um componente ou
renderizador baseado em Java, ou declarar aquele componente ou
renderizador em XML, nem integrar JavaScript de terceiros para fazer
chamadas Ajax.
Com o JSF 2 basta criar um componente composto, com
markup quase idêntico a qualquer facelet JSF 2, e talvez acrescentar um
pequeno JavaScript ou código Java, e voilà! ? temos um componente
personalizado ideal que facilita a entrada de dados para os usuários de
seu aplicativo.
Na próxima parte do JSF fu, vamos discutir mais aspectos da implementação de componentes personalizados JSF Ajaxified, como integrar o marcador <f:ajax> para que seus componentes personalizados possam participar do Ajax iniciado por outros.
Faça download do código-fonte que usamos neste artigo – j-jsf2fu-0410-src.zip
Recursos
Aprender
- Página inicial do JSF: Encontre mais recursos sobre desenvolvimento com JSF.
- AjaxDaddy: AjaxDaddy oferece exemplos de Ajax, scripts JavaScript, e demonstrações Web 2.0.
- Blog de Roger Kitain: Roger Kitain e Ed Burns são especialistas conjuntos líderes para JSF 2.0.
- Blog de Jim Driscoll: Você encontrará diversas entradas referentes ao JSF 2.
- Blog de Ryan Lubke: Ryan Lubke trabalha na implementação de referência do JSF 2.
- Lista de países fictícios: O exemplo deste artigo usa a lista de países fictícios da Wikipédia.
- Zona de tecnologia Java do developerWorks: Encontre centenas de artigos sobre cada aspecto da programação Java.
Obter produtos e tecnologias
- JSF: Faça o download do JSF 2.0.
Discutir
- “Public Access to JSF 2.0 JSR-314-EG Discussions Now Available” (Blog de Ed Burns, java.net, março de 2009): Saiba como cadastrar-se na lista de e-mails do Grupo de Especialistas do JSF 2.
- Envolva-se na comunidade do My developerWorks.
artigo publicado originalmente no developerWorks Brasil, por David Geary
*
O autor, palestrante e consultor David Geary é o presidente da Clarity Training, Inc.
onde ensina desenvolvedores a implementarem aplicativos da Web usando
JSF e Google Web Toolkit (GWT). Ele fez parte dos Grupos de
Especialistas do JSTL 1.0 e do JSF 1.0/2.0, foi coautor do Exame de
Certificação de Desenvolvedor da Web da Sun e contribuiu para projetos
de software livre, incluindo o Apache Struts e o Apache Shale. Graphic Java Swing de David foi um dos livros de Java mais vendidos de todos os tempos e Core JSF
(coescrito com Cay Horstman) é o livro de JSF mais vendido.
David fala frequentemente em conferências e para grupos de usuários. É
frequentador assíduo do tour de NFJS desde 2003, deu cursos na Java
University e foi duas vezes votado como JavaOne rock star.