Calendário do advento symfony dia oito: Interações AJAX

Anteriormente no symfony

Após sete horas de trabalho, a aplicação Askeet avançou bem. A home page exibe uma lista de perguntas, o detalhe de uma pergunta exibe as suas respostas, os usuários tem uma página de perfil e listas temáticas estão disponíveis para cada página na barra lateral. Nossa FAQ melhorada pela comunidade está na direção certa (veja a lista das ações disponíveis como as de [ontem](7.txt)), e sim, os usuários ainda não podem alterar os dados.

Se a base para a manipulação de dados na web foi por muito tempo formulários, hoje, as técnicas AJAX e a otimização da usabilidade podem modificar a forma como uma aplicação é construída. E isso aplica-se a Askeet também. Este tutorial irá lhe mostrar como adicionar interações AJAX-otimizadas para a Askeet. O objetivo é permitir que um usuário registrado declare seu interesse em uma pergunta.

Adicionando um indicador no layout

Enquanto um pedido assíncrono está pendente, os usuários de um website AJAX-powered não possuem nenhuma das pistas habituais que sua ação foi levada em conta e que o resultado será exibido logo.

Este é o motivo pelo qual toda página que contém interações AJAX deve exibir um indicador de atividade.

Para este propósito, adicione ao topo do <body> no layout.php global:

    <div id="indicator" style="display: none"></div>

Embora oculto por default, este <div> deverá ser exibido quando um pedido AJAX está pendente. Ele está vazio, mas o stylesheet main.css (armazenado no diretório askeet/web/css/) lhe dará forma e conteúdo:

    div#indicator
    {
      position: absolute;
      width: 100px;
      height: 40px;
      left: 10px;
      top: 10px;
      z-index: 900;
      background: url(/images/indicator.gif) no-repeat 0 0;
    }

Adicionando uma interação AJAX para declarar interesse

Uma interação ajax é composta de três partes: um caller (um link, botão ou qualquer controle que o usuário manipula para lançar a ação), uma ação do servidor e uma área da página para exibir o resultado da ação ao usuário.

Caller

Voltemos para as perguntas exibidas. Se você lembra do [dia quatro](4.txt), uma pergunta pode ser exibida nas listas de perguntas e no detalhe de uma pergunta

É por isso que o código para o título de uma pergunta e o bloco de interesse foi reajustado para o fragmento _interested_user.php. Abra este fragmento novamente, e adicione um link para permitir que os usuários declarem seus interesses:

    <?php use_helper('User') ?>

    <div class="interested_mark" id="mark_<?php echo $question->getId() ?>">
      <?php echo $question->getInterestedUsers() ?>
    </div>

    <?php echo link_to_user_interested($sf_user, $question) ?>

Este link fará mais do que somente redirecionar para uma outra página. De fato, se um usuário já declarou seu interesse em uma determinada pergunta, ele não poderá declará-la novamente. E se um usuário não está autenticado... bem, nós veremos este caso depois.

O link será escrito em uma função helper, que precisa ser criada em askeet/apps/frontend/lib/helper/UserHelper.php:

    <?php
    
    use_helper('Javascript');
    
    function link_to_user_interested($user, $question)
    {
      if ($user->isAuthenticated())
      {
        $interested = InterestPeer::retrieveByPk($question->getId(), $user->getSubscriberId());
        if ($interested)
        {
          // already interested
          return 'interested!';
        }
        else
        {
          // didn't declare interest yet
          return link_to_remote('interested?', array(
            'url'      => 'user/interested?id='.$question->getId(),
            'update'   => array('success' => 'block_'.$question->getId()),
            'loading'  => "Element.show('indicator')",
            'complete' => "Element.hide('indicator');".visual_effect('highlight', 'mark_'.$question->getId()),
          ));
        }
      }
      else
      {
        return link_to('interested?', 'user/login');
      }
    }
    
    ?>

A função link_to_remote() é o primeiro componente de uma interação AJAX: O Caller. Ele declara qual ação deverá ser requisitada quando o usuário clicar no link (aqui: user/interested) e que área da página deve ser atualizada com o resultado da ação (aqui: o elemento de id block_XX).

Dois manipuladores de eventos (loading e complete) são adicionados e associados à funções javascript prototype. A biblioteca prototype fornece ferramentas javascript muito acessíveis para aplicar efeitos visuais em páginas web com simples chamadas de função. Seu único defeito é a falta de documentação, mas o código é de fácil compreensão.

Nós escolhemos utilizar um helper ao invés de um partial porque esta função contém muito mais código PHP do que código HTML.

Não esqueça de adicionar o id id="block_<?php echo $question->getId() ?>" no fragmento question/_list.

    <div class="interested_block" id="block_<?php echo $question->getId() ?>">
      <?php include_partial('interested_user', array('question' => $question)) ?>
    </div>

Nota: Estas funções somente funcionarão se você definir corretamente o alias sf na configuração do seu servidor web, como explicado durante o dia um.

Área do resultado

O atributo update do helper javascript link_to_remote() especifica a área do resultado. Neste caso, o resultado da ação user/interested substituirá o conteúdo do elemento com id block_XX. Se você está confuso, dê uma olhada no que a integração do fragmento nos templates fará:

    ...
    <div class="interested_block" id="block_<?php echo $question->getId() ?>">
      <!-- between here -->
      <?php use_helper('User') ?>
      <div class="interested_mark" id="mark_<?php echo $question->getId() ?>">
        <?php echo $question->getInterestedUsers() ?>
      </div>
      <?php echo link_to_user_interested($sf_user, $question) ?>
      <!-- and there -->
    </div>
    ...

A área do resultado é a parte entre os dois comentários. A ação, uma vez executada, substituirá este conteúdo.

O interesse do segundo id (mark_XX) é puramente visual. O manipulador de evento complete do helper link_to_remote destacará o <div> interested_mark do interesse clicado... após a ação retornar um número incrementado do interesse.

Ação do servidor

O caller AJAX aponta para a ação user/interested. Esta ação deve criar um novo registro na tabela Interest para a pergunta e usuário atual. Aqui está como fazer isso com o symfony:

<?php
    public function executeInterested()
    {
      $this->question = QuestionPeer::retrieveByPk($this->getRequestParameter('id'));
      $this->forward404Unless($this->question);

      $user = $this->getUser()->getSubscriber();

      $interest = new Interest();
      $interest->setQuestion($this->question);
      $interest->setUser($user);
      $interest->save();
    }
?>

Lembre que o método ->save() do objeto Interest foi modificado para incrementar o campo interested_user do User relacionado. Assim, o número de usuários interessados na pergunta atual será magicamente incrementado na tela após a chamada da ação.

E o que deve exibir o template interestedSuccess.php resultante?

    <?php include_partial('question/interested_user', array('question' => $question)) ?>

Ele exibe novamente o fragmento _interested_user.php do módulo question. Este é o grande motivo de termos escrito este fragmento em primeiro lugar. Nós também devemos desabilitar o layout para este template (modules/user/config/view.yml):

    interestedSuccess:
      has_layout: off

Teste final

O desenvolvimento da definição dos interesses pelo usuário utilizando AJAX está agora finalizado. Você pode testá-lo entrando com um login/senha existente na página de login, exibindo a lista de perguntas e então clicando no link 'interested?'. O indicador aparecerá enquando o pedido é passado para o servidor. Então, o número é incrementado e destacado quando o servidor responder. Note que o link 'interested?' inicial modificou agora para o texto 'interested!' sem link, graças ao nosso helper link_to_user_interested:

Se você deseja mais exemplos sobre o uso dos helpers AJAX, você pode ler o tutorial do shopping cart drag-and-drop, assitir o screencast associado ou ler o capítulo relacionado no livro.

Adicionando um formulário 'sign-in' inline

Nós previamente dissemos que somente usuários registrados poderiam declarar interesse em uma pergunta. Isto significa que, se um usuário não autenticado clicar em um link 'interested?', a página de login deve ser exibida primeiro.

Mas espere. Porque o usuário deveria carregar uma nova página de login e perder contato com a questão em que ele está interessado? Uma idéia melhor seria termos um formulário de login aparecendo dinamicamente na página. Isso é o que nós vamos fazer.

Adicionando um formulário de login oculto no layout

Abra o layout global (em askeet/apps/frontend/templates/layout.php), e adicione (entre o header e o div content):

    <?php use_helper('Javascript') ?>

    <div id="login" style="display: none">
      <h2>Please sign-in first</h2>
      
      <?php echo link_to_function('cancel', visual_effect('blind_up', 'login', array('duration' => 0.5))) ?>
      
      <?php echo form_tag('user/login', 'id=loginform') ?>
        nickname: <?php echo input_tag('nickname') ?><br />
        password: <?php echo input_password_tag('password') ?><br />
        <?php echo input_hidden_tag('referer', $sf_params->get('referer') ? $sf_params->get('referer') : $sf_request->getUri()) ?>
        <?php echo submit_tag('login') ?>
      </form>
    </div>

Mais uma vez, este formulário é oculto por default. A tag oculta referer contém o parâmetro do pedido referer se ele existir, ou então, a URI atual.

Exibindo o formulário quando um usuário não autenticado clica no link interested.

Você lembra do helper User que nós escrevemos anteriormente? Nós lidaremos agora com o caso onde o usuário não é autenticado. Abra novamente o arquivo askeet/lib/helper/UserHelper.php e modifique a linha

    return link_to('interested?', 'user/login');

pela seguinte:

    return link_to_function('interested?', visual_effect('blind_down', 'login', array('duration' => 0.5)));

Quando o usuário não está autenticado, o link na palavra 'interested?' chamará o efeito javascript (blind_down) da biblioteca prototype que revelará o elemento de id login - que é o formulário que nós acrescentamos agora ao layout.

Login do usuário

A ação user/login já foi escrita durante o quinto dia, e reformulada durante o dia seis. Nós precisamos modificá-la novamente?

<?php
    public function executeLogin()
    {
      if ($this->getRequest()->getMethod() != sfRequest::POST)
      {
        // display the form
        $this->getRequest()->getParameterHolder()->set('referer', $this->getRequest()->getReferer());
    
        return sfView::SUCCESS;
      }
      else
      {
        // handle the form submission
        // redirect to last page
        return $this->redirect($this->getRequestParameter('referer', '@homepage'));
      }
    }
?>

Não há necessidade. Ela trabalha perfeitamente como está, a manipulação do referer redirecionará o usuário à página onde ele estava quando foi clicado no link.

Teste a funcionalidade AJAX agora. Para um usuário não registrado, será exibido um formulário de login sem deixar a página atual. Se o nickname e a senha forem reconhecidos, a página será atualizada e o usuário poderá clicar no link 'interested?' que ele pretendia antes.

Nota: Em muitas interações AJAX como esta, o template da ação do servidor é um simples include_partial. Isso é porque um resultado inicial é exibido freqüentemente quando a página inteira é carregada pela primeira vez, e porque a parte que é atualizada pela ação AJAX também é parte do template inicial.

Até amanhã

A parte mais difícil em projetar interações AJAX é definir corretamente o caller, a ação do servidor e a área do resultado.

Uma vez que você os conhece, o symfony lhe fornece helpers para fazer o restante. Para estar seguro que você entendeu como ela funciona, confira como nós implementamos o mesmo mecanismo para declarar interesse pelas relevâncias das respostas. Neste caso, a ação AJAX chamada é user/vote, o partial _answer.php é dividido em duas partes (criando assim um partial_user_vote.php), e dois helpers link_to_user_relevancy_up() e link_to_user_relevancy_down() são criados no helper User. O módulo User também ganha uma ação vote e um template voteSuccess.php. Não se esqueça de setar o layout para off também neste template.

Askeet está começando a se parecer com uma aplicação web 2.0. E estamos apenas no começo: em poucos dias, nós acrescentaremos mais algumas interações AJAX. Amanhã nós aproveitaremos a ocasião para fazer uma revisão geral das técnicas MVC no symfony e implementar uma biblioteca externa.

Se você encontrou algum problema tentando seguir o tutorial de hoje, poderá ainda fazer o download do código fonte completo com a tag release_day_8 no repositório SVN da askeet. Se você não encontrou nenhum problema, venha para o forum askeet ajudar a responder as dúvidas de outros.

Attachments