Programador ajudante e aprendiz da comunidade open source.

Manipulando Listas com jQuery e VRaptor 3

Atualizado em 13 de Fevereiro de 2012.

Constantemente manipulamos coleções de dados em nossas aplicações, seja em pequenas ou grandes quantidades. Sabemos muito bem que controlar itens de lista em nossas classes não é algo trivial, quem dirá em nossas views. Hoje iremos ver como o VRaptor nos facilita a manipulação de listas entre view e controller, e como ele junto ao jQuery nos proporcionam uma manipulação dinâmica e flexível.

Objetivo:

Fazer um cadastro de filme que contenha vários artistas que podem ser adicionados ou removidos usando o jQuery para criar a dinâmica de tela e o VRaptor para capturar e organizar os dados.


Manipulando Listas Dinâmicamente

Criando os modelos:

Vamos criar a entidade Filme que contém uma coleção de Artista.

public class Filme {

  private Long id;
  private String titulo;
  private Collection<Artista> artista;

  // getters e setters

}
public class Artista {

  private Long id;
  private String nome;

  // getters e setters

}

Vimos que nosso filme possui uma coleção de artistas.

Criando o controller:

Primeiramente vamos trabalhar da forma mais "simples" usando array para entendermos quais são as facilidades e benefícios da solução final. Desse modo precisaremos de um método que receba nosso filme e também um array de artistas. Ops, um array de artistas não terá como né? Pois como iremos enviar vários objetos instanciados para o controller?

Então vamos mandar somente os nomes dos artistas e lá no controller os criaremos de fato.

@Post("/filme")
public void salvar(Filme filme, String[] artistaNome) {

  Collection<Artista> artistas = new ArrayList<Artista>();

  for (String nome : artistaNome) {
    Artista artista = new Artista();
    artista.setNome(nome);

    artistas.add(artista);
  }

  filme.setArtistas(artistas);

  filmeDao.salvar(filme);

  result.redirectTo(this).listagem();
}

Rodamos o array criando um artista com cada nome, então o colocamos na lista. Após criarmos todos os artistas nós os setamos no filme. Ufa!

Criando a view:

Nossa view precisará do campo com o título do filme e vários campos com os nomes dos artistas.

<input type="text" name="filme.titulo" value="Matrix" />

<input type="text" name="artistaNome" value="Neo" />
<input type="text" name="artistaNome" value="Smith" />
<input type="text" name="artistaNome" value="Trinity" />

Se submetermos esse formulário teríamos o filme Matrix com os artistas Neo, Smith e Trinity.

Um problema nessa solução é que se mandarmos um artista apenas, o VRaptor não entenderá que é um vetor com apenas uma posição e o mesmo ficará null, da mesma forma se não passarmos nenhum artista, é claro. Uma forma um tanto quanto feia seria adicionar dois campos hidden com um valor dummy, por exemplo, para termos sempre um vetor mesmo que não criemos nenhum artista, e então excluiríamos estes campos fake no controller antes de salvar.

Outro problema é que precisamos no mínimo dos IDs dos artista para edição e para o relacionamento das entidades, então teríamos de passar outro vetor de IDs e ainda garantir que o primeiro ID pertencerá ao primeiro artista e assim por diante. Mas essa solução já deu o que tinha que dar e se você usá-la irá tomar umas chineladas! :P

O VRaptor sabe injetar valores diretamente nos objetos, então podemos receber diretamente uma coleção de artistas como argumento da seguinte forma:

public void salvar(Filme filme, Collection<Artista> artistas) { }

A primeira pergunta que vem a cabeça é: "posso passar vários valores value="artistas" que o VRaptor preenche a lista sozinho igual acontece com o vetor?". Não, devemos indicar em qual posição da lista o valor será injetado e ele cuidará do resto.

<input type="text" name="artistas[0].nome" value="Neo" />
<input type="text" name="artistas[1].nome" value="Smith" />
<input type="text" name="artistas[2].nome" value="Trinity" />

"Hum.... mas se o VRaptor sabe injetar os valores direto nos objetos e o objeto Filme já possui uma lista de artistas, então..." Exato! Podemos simplificar mais ainda nosso método injetando os artistas diretamente no filme:

<input type="text" name="filme.artistas[0].nome" value="Neo" />
<input type="text" name="filme.artistas[1].nome" value="Smith" />
<input type="text" name="filme.artistas[42].nome" value="Trinity" />

Veja que agora injetamos o nome do artista dentro de uma determinada posição da lista que por sua vez esta dentro do objeto filme, com isso podemos retirar o segundo argumento do nosso método salvar e usarmos somente o objeto filme. Ok, pode soltar aquele "Putz!". :D

Perceba que o terceiro campo esta com o índice 42 para demonstrar que não é necessário seguir uma ordem. Só tome cuidado para não repetir o índice, senão a posição ficará nula.

Agora que já sabemos a melhor forma de trabalhar com listas, vamos partir para a criação da nossa tela dinâmica com ajuda do jQuery.

Criando a tela de exibição:

Para apresentar uma lista de dados na view precisamos apenas da ajuda do forEach da JSTL.

Título: ${filme.titulo}

Artistas:
<c:forEach items="${filme.artistas}" var="artista">
  - ${artista.nome}
</c:forEach>

<a href="${pageContext.request.contextPath}/filme/editar/${filme.id}">Editar</a>

Tendo o filme no request, apresentamos o título do filme de forma normal acessando o seu atributo titulo. Para mostrar os artistas, fazemos uma iteração através dos mesmos e dentro do forEach pegamos cada um e apresentamos seus dados.

Criando a tela de cadastro/edição:

Vamos criar um formulário para enviar as informações do filme junto com as dos artistas.

<form action="${pageContext.request.contextPath}/filme" method="post">
  <c:if test="${filme != null &amp;&amp; filme.id != null}">
    <input type="hidden" name="filme.id" value="${filme.id}" />
  </c:if>

  Título: <input type="text" name="filme.titulo" value="${filme.titulo}" />

  <fieldset id="artista-container">
    <img src="${pageContext.request.contextPath}/img/adicionar.png" onclick="adicionar();" />

    <c:forEach items="${filme.artistas}" var="artista" varStatus="status">
      <div class="artista">
        Nome:
        <input type="text" name="filme.artistas[${status.index}].nome" value="${item.nome}" />
        <input type="hidden" name="filme.artistas[${status.index}].id" value="${artista.id}" />

        <img src="${pageContext.request.contextPath}/img/remover.png" class="button-remover" />
      </div>
    </c:forEach>
  </fieldset>
</form>

Linha 1: o formulário submete os dados para o método que espera um filme.
Linha 2-4: mantemos um campo escondido para manter o ID do filme durante a edição.
Linha 6: o título do filme é enviado e injetado no atributo titulo do filme.
Linha 8: identificamos este container para o jQuery usar como área onde estará localizado os campos dos artistas.
Linha 9: botão que chama a função para criar os campos para o preenchimento do dados de um novo artista.
Linha 11: iteramos a lista de artistas que pode estar vindo para ser editada. Perceba que temos o atributo varStatus que representa o índice da iteração.
Linha 12: fizemos um container para cada artista e os identificamos com a classe artista que servirá para localização de todos os artistas contidos no container.
Linha 14-15: campos criados dinamicamente para suportar os dados do artista. Injetamos o nome e o ID (durante a edição) do artista diretamente na lista que esta no objeto filme. Perceba que em cada iteração usamos o índice do loop para marcarmos a posição que este artista entrará na lista. Se lembra?
Linha 17: botão para o artista correspondente e um nome de classe para identificar tal botão.

Criando o script de manipulação com o jQuery:

Com nossa estrutura montada, só nos resta manipular os elementos. Teremos uma função que cria os campos do artista e uma que os removem.

Criando um artista:

Como os campos serão sempre iguais, iremos começar criando um modelo dos elementos que precisamos para a criação de um artista:

  var model =
  '<div class="artista">' +
    '<label>Nome:</label>' +
    '<input type="text" name="filme.artistas[0].nome" />' +

    '<img src="${pageContext.request.contextPath}/img/remover.png" class="button-remover" />' +
  '</div>';

A variável model mantem um clone dos dados que já possuímos para criação do artista durante a edição. Caso você sempre possua uma estrutura dessa na tela é possível reaproveitá-la através da função jQuery.clone() modificando o necessário após o clone da mesma. Mas no nosso caso não teremos essa estrutura inicialmente na tela.

Com esse modelo de dados, podemos criar a função que faz a inserção do mesmo no DOM:

function adicionar() {
  $('#artista-container').append(model);

  reorderIndexes();

Uma função bem simples que pega o valor da variável e concatena no final do container onde ficam os "artistas". Como temos uma ordem natural na lista dos artista e podemos tanto adicionar como remover um artista do início, do fim ou do meio, precisamos de garantir um reajuste no índice do name dos campos. Para isso criamos a função reorderIndexes() que será mostrada, já já.

Removendo um artista:

$('.button-remover').live('click', function() {
  $(this).parent().remove();

  reorderIndexes();
});

Aplicamos a função de remover o artista a todos elementos que tenham a classe button-remover, neste caso, todos os botões de remover. A função é acionada no click, que ao ser executada, apaga o container (artista-item) no qual o botão esta contido, removendo todos os dados do artista em questão do formulário.

O uso da função live é fundamental, pois ela consegue aplicar o bind da função mesmo nos elementos criados dinamicamente no DOM.

Reordenando os índices:

Mais fácil do que ficar calculando qual o próximo valor, é percorrer todos os artistas ao mesmo tempo em que iremos aplicando o índice correto. Veja:

var regex = /[[0-9]]/g;

$('.artista').each(function(index) {
  var $campos  = $(this).find('input'),
      $input  ,
      name    ;

  $campos.each(function() {
    $input  = $(this),
    name  = $input.attr('name');

    $input.attr('name', name.replace(regex, '[' + index + ']'));
  });
});

Veja que inicialmente criamos uma regex que busca extamente a parte do name do campo no qual mantém o índice da lista. Então percorremos todos os "artistas" (containers) para executarmos a ação de ordenação em cada um deles.

Veja que possuimos um argumento chamado index que indica a posição de cada um dos artista e é com ele que iremos fazer a reordenação, ou seja, o primeiro container receberá o índice 0, o segundo o índice 1 e assim por diante.

Para cada um desse container pegamos todos os campo e para cada um destes campos pegamos o valor do seu name e substituimos o número de seu índice pelo índice corrente da iteração do .each(). Com isso garantimos a ordem tanto na adição quanto na remoção dos artistas.

Quando trabalhamos apenas com um valor para cada entidade não há problema algum em deixarmos os names sem índices filme.artistas[].nome, já que o VRaptor irá preenchê-los na ordem que vieram da tela, porém quando temos mais de um campo, devemos manter o índice atualizado, pois só assim saberemos quais campos fazem parte da mesma entidade.

Esta estratégia de criação dinâmica foi utilizado na adição de questões do Mockr.me

Link do projeto:

http://github.com/wbotelhos/manipulando-listas-jquery-vraptor-3

  1. Fabiano 9 Abr 2013 17:10

    Boa Tarde Washington, tudo bem. Gostaria de saber se você poderia me dar uma ajuda quanto ao artigo acima. Meu problema é o Seguinte: A funcionalidade de reordenar indexes funciona muito bem para índices de 0 a 9. Porém quando o índice é maior que 9, os índices não são ordenados. Isso acontece com você também? Isso é fácil de resolver? Poderia me orientar por favor?

    Abraços e Parabéns pelo artigo!

  2. Rafael 11 Mai 2012 08:41

    Olá, bom dia.
    Primeiramente gostaria de parabenizá-lo pelo post. Achei muito bom!

    Eu tenho um relacionamento 1:n entre duas entidades "A" e "B". A entidade "A" é a dona do relacionamento. Para que eu conseguisse salvar a entidade "A" a partir da entidade "B" (através do session.save(b); ), na Classe B eu fiz:

    public void setA(A a) {
     a.setB(this)
     this.a = a;
    }
    

    O meu problema é como eu consigo passar um objeto "A" para o meu BController.
    Tentei algo do tipo: ${b.nome}

    Mas no debugger quando eu tento ler b.getA().getB() o mesmo retorna null. Como eu consigo injetar esse objeto no controller do vraptor?

    Não sei se consegui ser muito claro na minha pergunta, mas desde já agradeço sua ajuda.

    1. Washington Botelho autor 11 Mai 2012 21:33

      Oi Rafael,

      Achei meio confuso sua dúvida.
      Mas se você tem a entidade A que possui um B, você deveria sempre salvar o pai, pois isso é uma composição.
      No Hibernate configure para fazer o Cascade e então poderá fazer ${a.b.nome}.

  3. Ramir 8 Jun 2011 20:12

    Washington,
    Ao invés de só remover itens na edição, eu queria adicionar novos itens na inserção...
    você tem alguma dica de como poderia fazer isso?

  4. Daniel 11 Jan 2011 17:26

    Certeza que o exemplo com array de String funciona? Não precisa do índice nos nomes dos inputs?

    1. Washington Botelho autor 11 Jan 2011 19:49

      Oi Daniel,

      Funciona sim, não precisa do índice.

        1. Washington Botelho autor 11 Jan 2011 21:11

          Oi Daniel,

          Você fez o teste ai ou só esta falando baseado no que leu?

          Novamente, funciona.

          1. Daniel 11 Jan 2011 21:14

            esse post no guj é meu... nao rolou
            e o proprio dev do vraptor diz que nao funciona

            1. Washington Botelho autor 11 Jan 2011 21:36

              Então Daniel,

              Dei uma olhada no seu post e você usa long, sendo assim não irá funcionar, nem mesmo int.
              Aqui no post estou usando um array de String (String[] artistaNome).

              Se você quiser passar vários números, você pode passá-los como String e convertê-los em long os adicionando em outra lista posteriormente.

              1. Daniel 11 Jan 2011 21:40

                Hmmmm.. então ele só faz sem índice no caso de String... got it!

  5. Rodolfo 10 Jan 2011 19:42

    Olá Washington,

    Estou com 2 problemas, não sei se pode me ajudar:

    1) Ao acessar minha aplicação pela primeira vez dá um NullPointerException, parece que o request não é injetado corretamente... funciona apenas quando tento denovo.. eu uso o request para ler os cookies:

    @Path("/")
    public void index() {
      if (request != null) {
        for (Cookie c : request.getCookies()) {
          if (c.getName() != null && c.getName().equals(Keys.COOKIE_ESTADO)) {
            try {  
              result.include("showCidadeModal", "");
              result.use(Results.logic()).redirectTo(this.getClass()).anuncios(c.getValue());
              return;
            } catch (Exception e) {
              //TODO logar exceção
              e.printStackTrace();
            }
            break;
          }
        }
    
        result.include("showCidadeModal", "#dialog");
        result.use(Results.logic()).redirectTo(this.getClass()).anuncios("SP");
        //TODO escolher cidade
      }
    }
    

    2) Tenho uma entidade que poussi uma cidade que possui um estado, para cadastrar eu seleciono o estado em um combo e carrego as cidades e depois escolho as cidades em outro combo... até aí tranquilo, consegui gravar normalmente no banco... mas e para editar? Como faço para quando for editar o combo estado e cidade já vir preenchido? Só usando algum fução JS?

    Obrigado!!

    Parabens pelo blog!

    Att. Rodolfo

    1. Washington Botelho autor 10 Jan 2011 21:49

      Oi Rodolfo,

      Verifica request.getCookies() != null primeiro, talvez resolva.

      Na hora de editar seu formulário você precisar fazer um <c:forEach /> comparando o ID de cada estado listado com o ID que veio do banco. Quando forem iguais, você adiciona o selected="selected" para fazer a seleção. Para a cidade será o mesmo raciocínio
      Eu gosto de preencher os combos a partir de um ENUM com os nomes.

      Se sua busca for ajax, então deverá fazer tudo via Javascript usando jQuery para facilitar.

      Dicas:
      + Retire do seu código o return, pois seu método retorna void, ou seja, não retorna nada;
      + Resuma seu result de:
      result.use(Results.logic()).redirectTo(this.getClass()) para result.redirectTo(this);
      + Aproveite mais do result: result.include("elemento", valor).redirectTo();

  6. Anonimo 22 Dez 2010 18:00

    Com todo respeito, mas para uma postagem em blog e tal, esse código tá muito ruim. Nomes estranhos, código HTML no meio de JS. Péssimo.

    1. Washington Botelho autor 22 Dez 2010 19:02

      Oi "Anonimo",

      Quais nomes estão estranhos? Nomeclatura tirando o JavaBeans fica meio subjetiva, apesar de sempre buscarmos nomes que lembrem bem o conteúdo. Teria alguma dica de melhoria? (:

      Hoje, graças ao Boris Moore da Microsoft, temos já na API oficial do jQuery o conceito de Template, que nos ajudam separar o código HTML do JS. Existe um pouco de HTML na função adicionarArtista, mas ficaria inviável apresentar o assunto Template aqui apenas para exemplificar a simples criação de um botão e um campo com nome dinâmico.

      Temos também a injeção de atributos possibilitada na nova versão do jQuery que também iria complicar um pouco a leitura e o código, apesar de não deixarmos de escrever código HTML dentro do JS, evitando mais a concatenação dos atributos dinâmicos.

      Você poderia nos explicar uma forma de criarmos essa estrutura sem escrever HTML com JS e sem usar template? Fique a vontade para sugerir melhorias e enriquecer o post, ficarei grato. (:

      1. Anonimo 22 Dez 2010 19:59

        Filme tem artista(s), ponto. Se é lista ou não, está discriminado no tipo. Você não tem um tituloString, por exemplo. Outro caso é o artistaNome, ou é artistaArray (seguindo tua lógica anterior) ou artistas, ou nomesArtistas. Esse tipo de detalhe ajuda muito didaticamente, certo!?
        A questão do HTML é que no texto você citou sobre o jeito certo de fazer e creio que caiu na hora de cuspir o HTML no meio do JS, isso é impossível de manter. Que cuspa uma expression que faça binding para uma string no server que tenha o HTML, mas codificar é feio.
        Mas é só um toque!
        Valeu!

        1. Washington Botelho autor 22 Dez 2010 21:33

          Fala "Anonimo",

          Com certeza não precisaria de especificar. Coloquei como artistaList pelo simples fato do VRaptor utilizar esta convenção para listas que são retornadas para a view. Mas acatei a sua sugestão e alterei. (:

          Essa do artistaNome é algo que já parei pra pensar, visto que depende de onde você começa olhar. Se olhar do controller para a view, sim, é um artistaArray, mas olhando da view para o controller, cada campo é um artistaNome e não um artistaArray.

          Não entendi essa de bind na string do server e o que é feio codificar. Tem como você escrever um exemplo?

Em resposta:
(cancelar)
Formate seu código utilizando Markdown.