Capítulo 12

AJAX e efeitos visuais

12.1 - O que é AJAX?

AJAX (Asynchronous Javascript and XML) é um conjunto de técnicas de desenvolvimento Web para executar tarefas do lado do cliente, por exemplo modificar pedaços de uma página sem ter que carregar ela inteira. Na verdade o mecanismo é muito simples. De acordo com alguma ação um javascript envia uma requisição ao servidor como se fosse em background. Na resposta dessa requisição vem um XML que o javascript processa e modifica a página segundo essa resposta.

É o efeito que tanto ocorre no gmail e google maps.

Há várias ferramentas para se trabalhar com Ajax em Java. DWR e Google Web Toolkit são exemplos famosos de ferramentas que te auxiliam na criação de sistemas que usam AJAX.

Nós utilizaremos o VRaptor no servidor para nos ajudar com as requisições Ajax. No cliente, usaremos a biblioteca JQuery que é totalmente escrita em JavaScript, portanto independente de linguagem do servidor.

Há vários frameworks javascript disponíveis no mercado além do JQuery. Apenas para citar os mais famosos: Prototype/Script.aculo.us, Yahoo User Interface (YUI), Dojo. Usaremos o JQuery devido a sua extrema simplicidade de uso, muito boa para quem não domina javascript mas quer usar recursos de Ajax.

12.2 - Um pouco de JQuery

A biblioteca JQuery é baseada em um conceito de encadeamento (chaining). As chamadas de seus métodos são encadeadas uma após a outra, o que cria código muito simples de ser lido.

O ponto de partida do JQuery é a funcão $ que seleciona elementos DOM a partir de seletores CSS. Para selecionar um nó com id "teste" por exemplo, fazemos:

$('#teste')

ou para selecionar os elementos que possuem a classe produto:

$('.produto')

Seletores

O JQuery suporta XPath e CSS 3, com seletores avançadíssimos (além dos clássicos id e class). Veja mais aqui: http://docs.jquery.com/Selectors

Depois de selecionar o(s) elemento(s) desejado(s), podemos chamar métodos para as mais variadas coisas. O Hello World do JQuery mostra como exibir e esconder um div especifico:

$('#meuDiv').show()
$('#meuDiv').hide()

Um outro ponto forte do JQuery é que ele possui diversos plugins que fazem várias coisas interessantes como validação de formulários, autocomplete, efeitos visuais, drag 'n' drop, etc. Uma lista de plugins disponíveis pode ser encontrada em http://plugins.jquery.com/. Uma documentação mais completa sobre o JQuery se encontra em http://docs.jquery.com/ e uma documentação visual em http://www.visualjquery.com/.

Mais sobre javascript

Nesse curso não daremos foco no desenvolvimento de código javascript, boas práticas e características dessa linguagem, apenas usaremos plugins do JQuery. Mas o código javascript é uma parte bastante importante do desenvolvimento Web, e precisamos ter com ele os mesmos cuidados que temos com o código java.

Um conteúdo aprofundado sobre javascript, além de CSS e HTML, pode ser encontrado no curso WD-43 | Desenvolvimento Web com HTML, CSS e JavaScript.

Seus livros de tecnologia parecem do século passado?

Conheça a Casa do Código, uma nova editora, com autores de destaque no mercado, foco em ebooks (PDF, epub, mobi), preços imbatíveis e assuntos atuais.
Com a curadoria da Caelum e excelentes autores, é uma abordagem diferente para livros de tecnologia no Brasil. Conheça os títulos e a nova proposta, você vai gostar.

Casa do Código, livros para o programador.

12.3 - Validando formulários com o JQuery

Quando fizemos a validação do formulário de adição de produtos, precisávamos enviar uma requisição para o servidor, executando algumas lógicas para decidir se os dados estavam corretos, para então responder para o usuário que os seus dados não estão válidos. Isso pode ser evitado se fizermos a validação do lado do cliente.

Além de ser bem mais rápido, evita que seja feita uma requisição desnecessária para o servidor.

Existe um plugin do JQuery chamado Validation (http://bassistance.de/jquery-plugins/jquery-plugin-validation/) que torna a validação do lado do cliente bem simples. Para usá-lo você precisa importar os javascripts no seu jsp:

<script type="text/javascript" src=".../jquery-1.3.2.min.js"></script>
<script type="text/javascript" src=".../jquery.validate.min.js"></script>

Você também precisa falar qual vai ser o form que vai ser validado. Para isso você pode adicionar um id ao seu formulário, e usar um seletor do JQuery para torná-lo "validável":

<form id="produtosForm" action="<c:url value="/produtos"/>" 
  method="POST">
...

<script type="text/javascript">
  $('#produtosForm').validate();
</script>

Assim você pode adicionar as restrições aos seus campos, lembrando que o nome do produto é obrigatório e tem que ter o tamanho maior que 3, a descrição também é obrigatória e o tamanho tem que ser menor que 40, e o preço maior que zero:

<form action="<c:url value="/produtos"/>" method="POST">
  <fieldset>
    <legend>Adicionar produtos</legend>
        
    <label for="nome">Nome:</label>
    <input id="nome" class="required" minlength="3"
      type="text" name="produto.nome" value="${produto.nome }"/>

    <label for="descricao">Descrição:</label>
    <textarea id="descricao" class="required" maxlength="40" 
      name="produto.descricao">${produto.descricao }</textarea>

    <label for="preco">Preço:</label>
    <input id="preco" min="0" 
      type="text" name="produto.preco" value="${produto.preco }"/>

    <button type="submit">Enviar</button>
  </fieldset>
</form>

Se você não gosta de colocar mais atributos nos seus campos de formulário, você ainda pode definir todas as regras de validação de uma vez só, usando os names dos inputs, dentro da parte rules:

<script type="text/javascript">
  $('#produtosForm').validate({
    rules: {
      "produto.nome": {
        required: true,
        minlength: 3
      },
      "produto.descricao": {
        required: true,
        maxlength: 40
      },
      "produto.preco": {
        min: 0.0
      }
    }
  });
</script>

12.4 - Criando a busca de produtos

Como todo loja virtual que se preze, precisamos criar uma funcionalidade de busca de produtos. Para isso vamos criar uma lógica que efetua a busca no banco, dentro do ProdutosController:

@Resource
public class ProdutosController {
  //...
  
  public List<Produto> busca(String nome) {
    return dao.busca(nome);
  }

}

Repare que o nosso dao ainda não possui o método busca, mas podemos usar o eclipse para criar esse método pra nós. Para isso, coloque o cursor em cima do erro de compilação e aperte Ctrl+1. No menu que apareceu selecione Create method 'busca(String)' in type 'ProdutoDao'.

busca-produtos-ctrl1.png

Agora podemos modificar o método criado no ProdutoDao para realmente buscar os produtos que contém a string passada no nome. Para isso usaremos a API de Criteria do Hibernate:

@Component
public class ProdutoDao {

  //...
  public List<Produto> busca(String nome) {
    return session.createCriteria(Produto.class)
      .add(Restrictions.ilike("nome", nome, MatchMode.ANYWHERE))
      .list();
  }    

}

Traduzindo essa chamada: "crie uma Criteria de produtos, com a restrição de que o nome contenha a string passada em qualquer lugar, ignorando maiúsculas e minúsculas".

Agora precisamos criar o jsp que mostra os resultados da busca, em /WEB-INF/jsp/produto/busca.jsp. Podemos aproveitar o jsp de listagem para mostrar a tabela:

<h3>Resultados da busca pelo nome <b>"${nome }"</b></h3>
<%@ include file="lista.jsp" %>

Precisamos também passar o nome que foi buscado, então podemos mudar o método busca do ProdutosController:

public List<Produto> busca(String nome) {
  result.include("nome", nome);
  return dao.busca(nome);
}

Por último, vamos criar um formulário para poder acessar essa busca. Abra o arquivo header.jspf e modifique o div menu:

<div id="menu">
  <ul>
    <li><a href="<c:url value="/produtos/novo"/>">Novo Produto</a></li>
    <li><a href="<c:url value="/produtos"/>">Lista Produtos</a></li>
    <li><form action="<c:url value="/produto/busca"/>">
      <input name="nome"/>
    </form>
    </li>
  </ul>
</div>
busca-sem-jquery.png

Para que não fique um input perdido no menu, sem que as pessoas saibam o que ele faz, podemos usar um plugin do JQuery bem simples chamado Puts (http://github.com/cairesvs/Puts). Esse plugin coloca um texto em cima do input, e quando a pessoa clica nele, o texto some. Assim podemos colocar o texto "Busca de produtos por nome" no nosso input. Para usar esse plugin, precisamos baixar o javascript e importá-lo na nossa página, e então usar o método puts() para colocar nosso texto.

<script type="text/javascript" 
  src="<c:url value="/javascripts/jquery.puts.js"/>"></script>
...
<li><form action="<c:url value="/produto/busca"/>">
  <input id="busca" name="nome"/>
</form>
<script type="text/javascript">
  $("#busca").puts("Busca produtos por nome");
</script>
</li>

Assim temos o seguinte resultado:

busca-com-jquery.png

12.5 - Melhorando a busca: Autocomplete

Podemos ainda melhorar nossa busca, mostrando dicas de produtos já existentes enquanto o usuário está digitando. Queremos um resultado parecido com este:

busca-autocomplete.png

Para isso vamos usar um plugin do JQuery chamado AutoComplete (http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/) que faz autocomplete de campos de texto. Esse plugin faz uma requisição ajax para uma URL que retorna um JSON com os dados que vão aparecer no autocomplete.

Por causa desse plugin, precisamos criar uma lógica que gere um JSON para nós. No VRaptor, gerar JSON é bem simples:

import static br.com.caelum.vraptor.view.Results.*;
//...
  @Get("/produtos/busca.json")
  public void buscaJson(String nome) {
    result.use(json()).from(dao.busca(nome)).serialize();
  }

Ou seja, você passa para o método from() o que você quer serializar em JSON. Pode ser um objeto qualquer, mas no nosso caso vai ser uma lista. Ao chamarmos a URI desse método no browser, passando um nome qualquer, por exemplo http://localhost:8080/goodbuy/produtos/busca.json?nome=a é retornado um JSON parecido com:

{"list": [
  {
    "id": 1,
    "nome": "Sabonete",
    "descricao": "Um sabonete com perfume de lavanda",
    "preco": "3.53"
  },
  {
    "id": 3,
    "nome": "Sapato",
    "descricao": "um sapato de couro",
    "preco": "132.00"
  }
]}

Mas como vamos buscar os produtos apenas por nome, não precisamos de todas essas informações, só precisamos de uma lista de nomes, talvez com o preço. Para isso podemos excluir as outras propriedades:

import static br.com.caelum.vraptor.view.Results.*;
//...
  @Get @Path("/produtos/busca.json")
  public void buscaJson(String nome) {
    result.use(json()).from(dao.busca(nome))
      .exclude("id", "descricao")
      .serialize();
  }

Gerando o seguinte json:

{"list": [
  {
    "nome": "Sabonete",
    "preco": "3.53"
  },
  {
    "nome": "Sapato",
    "preco": "132.00"
  }
]}

Podemos também remover o {"list": } usando o método .withoutRoot():

result.use(json()).withoutRoot()
    .from(dao.busca(nome))
  .exclude("id", "descricao")
  .serialize();

que gera o json:

[
  {
    "nome": "Sabonete",
    "preco": "3.53"
  },
  {
    "nome": "Sapato",
    "preco": "132.00"
  }
]

Agora estamos prontos para usar o plugin de autocomplete. Para isso, abra o arquivo "header.jspf", e adicione o javascript e o css do plugin. Use o mesmo lugar do plugin Puts:

<link 
  href="<c:url value="/javascripts/jquery.autocomplete.css"/>"
  rel="stylesheet" type="text/css" media="screen" />
<script type="text/javascript" 
  src="<c:url value="/javascripts/jquery.autocomplete.min.js"/>"></script>
...
<script type="text/javascript">
  $("#busca").puts("Busca produtos por nome");
  $("#busca").autocomplete('/goodbuy/produtos/busca.json');
</script>

Isso deveria ser o suficiente, mas o plugin AutoComplete espera que você mande os dados separados por pipe(|) ou um dado por linha. Como queremos usar JSON, precisamos configurar isso no plugin:

$("#busca").autocomplete('/goodbuy/produtos/busca.json', {
    dataType: "json", // pra falar que vamos tratar um json
    parse: function(produtos) { // para tratar o json
      // a função map vai iterar por toda a lista, 
      // e transformar os dados usando a função passada
      return $.map(produtos, function(produto) {
        return  {
          // todos os dados do produto
          data: produto, 
          // o valor lógico do produto
          value: produto.nome,
          // o que vai aparecer ao selecionar
          result: produto.nome 
        };
      });
    },
    // o que vai aparecer na lista de autocomplete
    formatItem: function(produto) { 
      return produto.nome + "(" + produto.preco + ")";
    }
});

Além disso, o plugin passa como parâmetro da requisição o que você digitou no input, numa variável chamada q então você precisa modicar a lógica de busca para o parâmetro se chamar q:

@Get("/produtos/busca.json")
public void buscaJson(String q) {
  result.use(json()).withoutRoot()
      .from(dao.busca(q))
    .exclude("id", "descricao")
    .serialize();
}

Para saber mais: c:url dentro de javascript

Repare que estamos passando o nome de contexto (goodbuy) nas URLs do javascript acima. Isso é uma péssima prática, pois o nome de contexto depende de como você fez o deploy de sua aplicação. Do jeito que fizemos, somos obrigados a sempre fazer o deploy da aplicação com o nome "goodbuy".

Para corrigir isso, podemos usar a tag c:url dentro do javascript:

$("#busca").autocomplete('<c:url value="/produtos/busca.json"/>');

É possível usar qualquer tag JSTL ou Expression Language dentro dos javascripts das suas jsps. Mas cuidado, pois seus javascripts podem ficar mais difíceis de entender.

Agora é a melhor hora de aprender algo novo

Se você gosta de estudar essa apostila aberta da Caelum, certamente vai gostar dos cursos online que lançamos na plataforma Alura. Você estuda a qualquer momento com a qualidade Caelum.

Conheça a Alura.

12.6 - Exercícios

  1. Modifique os formulários de adição e edição de produtos para incluir a validação do lado do cliente. Os javascripts do plugin já estão importados no projeto base. Se preferir use a outra forma de validação, passando as opções para o método validate().

    <form id="produtosForm" action="<c:url value="/produtos"/>" 
      method="POST">
      <fieldset>
        <legend>Adicionar produtos</legend>
      
        <label for="nome">Nome:</label>
          <input id="nome" class="required" minlength="3"
            type="text" name="produto.nome" 
            value="${produto.nome }"/>
    
        <label for="descricao">Descrição:</label>
          <textarea id="descricao" class="required" 
            maxlength="40" name="produto.descricao"> ${produto.descricao } </textarea>
    
        <label for="preco">Preço:</label>
          <input id="preco" min="0" type="text"
            name="produto.preco" value="${produto.preco }"/>
    
        <button type="submit">Enviar</button>
      </fieldset>
    </form>
    
    <script type="text/javascript">
      $('#produtosForm').validate();
    </script>
    
  2. Tente adicionar um produto inválido.

    erro-validacao.png
  3. (Opcional) As mensagens de erro estão em inglês. Tente procurar na documentação do plugin como fazer para que as mensagens fiquem em português.

  4. Crie um método em ProdutosController para a listagem de produtos.

    @Resource
    public class ProdutosController {
      //...
    
      public List<Produto> busca(String nome) {
        result.include("nome", nome);
        return dao.busca(nome);
      }
    
    }
    
  5. Use o atalho Ctrl+1 para criar o método busca no ProdutoDao.

    busca-produtos-ctrl1.png
  6. Implemente o método busca no ProdutoDao.

    @Component
    public class ProdutoDao {
    
      //...
      public List<Produto> busca(String nome) {
        return session.createCriteria(Produto.class)
          .add(Restrictions.ilike("nome", nome, MatchMode.ANYWHERE))
          .list();
      }    
    
    }
    
  7. Crie o JSP de resultado da busca, em /WEB-INF/jsp/produto/busca.jsp

    <h3>Resultados da busca pelo nome <b>"${nome }"</b></h3>
    <%@ include file="lista.jsp" %>
    
  8. Abra o arquivo header.jspf e modifique o menu para incluir um formulário de busca:

    <div id="menu">
      <ul>
        <li><a href="<c:url value="/produtos/novo"/>">Novo Produto</a></li>
        <li><a href="<c:url value="/produtos"/>">Lista Produtos</a></li>
        <li><form action="<c:url value="/produto/busca"/>">
          <input name="nome"/>
        </form>
        </li>
      </ul>
    </div>
    
  9. Faça buscas usando esse formulário.

    busca-simples.png
  10. Modifique o formulário de busca para usar o plugin Puts e deixar uma mensagem dentro do input.

    <div id="menu">
      <ul>
        <li><a href="<c:url value="/produtos/novo"/>">Novo Produto</a></li>
        <li><a href="<c:url value="/produtos"/>">Lista Produtos</a></li>
        <li><form action="<c:url value="/produto/busca"/>">
          <input id="busca" name="nome"/>
        </form>
        <script type="text/javascript">
          $("#busca").puts("Busca produtos por nome");
        </script>
        </li>
      </ul>
    </div>
    
  11. Recarregue a página atual e veja que a mensagem apareceu no input.

    busca-com-jquery.png
  12. Crie o método de busca que retorna JSON no ProdutosController.

    @Get("/produtos/busca.json")
    public void buscaJson(String q) {
      result.use(json()).withoutRoot()
          .from(dao.busca(q))
        .exclude("id", "descricao")
        .serialize();
    }
    
  13. Adicione o código javascript para que o autocomplete funcione.

    $("#busca").autocomplete('/goodbuy/produtos/busca.json', {
      dataType: "json",
      parse: function(produtos) {
        return $.map(produtos, function(produto) {
          return  {
            data: produto,
            value: produto.nome,
            result: produto.nome
          };
        });
      },
      formatItem: function(produto) {
        return produto.nome + "(" + produto.preco + ")";
      }
              
    });
    
  14. Recarregue a página e teste o autocomplete digitando algo no campo de busca.

    busca-autocomplete.png

12.7 - Para saber mais: Representation

É possível usar a mesma lógica para a busca normal e a busca por json, usando a view representation() do VRaptor.

public void busca(String nome) {
  result.include("nome", nome);
  result.use(representation())
          .from(dao.busca(q), "produtos")
        .exclude("id", "descricao")
        .serialize();
}

Assim, se a requisição for feita passando o content-type "application/json", será a lista serializada em json, se for passado o content type "application/xml", será serializada em xml, senão o VRaptor redireciona para a jsp respectiva, com a variável ${produtos} disponível.