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
- ajax.gif (17.0 kB) - added by andreia on 06/17/07 20:35:15.
- div_login_form.gif (22.1 kB) - added by andreia on 06/17/07 20:36:49.
- indicator.gif (1.5 kB) - added by andreia on 06/17/07 20:37:16.
- pager_navigation_day7.gif (13.7 kB) - added by andreia on 06/17/07 20:37:42.



