
Mas o último exemplo que eu quis fazer não funcionou, era um exemplo de como fazer Paginação via AJAX no grails utilizando Javascript não com o lowpro, parecido com o que eu comentei no post sobre lowpro para o rails.
Cheguei em casa e fui tentar descobrir por que a coisa não funcionou, e foi mais fácil do que eu esperava.
Não funcionou por que eu esqueci que os templates padrão do Grails não utilizam IDs nas DIVs, por tanto o AjaxUpdater criado não funcionava …
Para fazer o exemplo funcionar é só fazer o seguinte:
copiar o lowpro.js para a pasta web-app/js
editar o template padrão (grails-app/views/layouts/main.gsp) e fazer ele ficar parecido com o seguinte:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <html>
<head>
<title><g:layoutTitle default="Grails" /></title>
<link rel="stylesheet" href="${createLinkTo(dir:'css',file:'main.css')}" />
<link rel="shortcut icon" href="${createLinkTo(dir:'images',file:'favicon.ico')}" type="image/x-icon" />
<g:layoutHead />
<g:javascript library="prototype/prototype" />
<g:javascript library="lowpro" />
<g:javascript library="application" />
</head>
<body>
<div id="spinner" class="spinner" style="display:none;">
<img src="${createLinkTo(dir:'images',file:'spinner.gif')}" alt="Spinner" />
</div>
<div class="logo"><img src="${createLinkTo(dir:'images',file:'grails_logo.jpg')}" alt="Grails" /></div>
<div id="content">
<g:layoutBody />
<div>
</body>
</html> |
Basicamente as alterações são:
Nas linhas 7 e 8 onde adicionei as bibliotecas lowpro e prototype
e mais importante, nas linhas 16 e 18 onde coloquei uma DIV com ID “content” em volta do conteúdo da página (isto vocês podem ajustar para envolver apenas o conteúdo que vocês quiserem).
Depois editando o application.js (web-app/js/application.js)
1 2 3 4 | Event.addBehavior.reassignAfterAjax = true; Event.addBehavior({ 'a.step':Remote.Link({update:'content'}) }) |
Apenas adicionei estas linhas no final do arquivo, a primeira configura o lowpro para reaplicar os comportamentos no final das chamadas AJAX, e as outras adicionam o comportamento de link AJAX em todas as tags “A” com a classe “step” que é o que o grails utiliza na paginaçao.
Agora a parte chata, no rails ele ja entende isto automaticamente, mas no grails eu tive que fazer na unha mesmo (nem é tanto trabalho assim
)
Vamos editar o metodo “list” do controller de clientes (ou qualquer controller do grails que vocês quiserem
)
1 2 3 4 5 6 | if(!params.max) params.max = 10 if(request.getHeader("X-Requested-With")=="XMLHttpRequest"){ render template:'listcontent', model:[clienteList: Cliente.list( params )] }else{ return [ clienteList: Cliente.list( params ) ] } |
A diferença básica é que se o cabeçalho X-Requested-With disser que é uma requisição AJAX eu vou retornar apenas a parte da página que me interessa (OK, isto eu teria que fazer no rails também, mas o rails iria me retornar toda a página se eu não fizesse isto, o grails não retorna nada ) ![]()
e para isto funcionar precisamos editar a view também …
eu copiei todo o conteúdo da tag body da view “list.gsp” para o arquivo “_listcontent.gsp” (precisei criar o arquivo), e na list.gsp eu coloquei no body apenas a linha: ${render(template:’listcontent’)}
Ou seja, substitui todo o conteúdo do body por esta linha, assim posso renderizar tudo em um request padrão, e apenas o conteúdo da página em um request AJAX.
Pronto, agora a paginação do Grails esta funcionando via AJAX
esta semana eu atualizo este post com o link para o zip do exemplo completo, mas apenas com isto vocês ja devem conseguir fazer a paginação via AJAX
PS.: não esqueçam para utilizar todos os recursos do Grails é interessante saber bem groovy (a linguagem utilizada), spring
, GORM (hibernate
) e sitemesh que são os frameworks utilizados por traz dos panos.
Tags: ajax, grails, groovy, javascript, lowpro, unobstrusive
Como eu disse que iria fazer, e fui cobrado no último rails podcads brasil, segue um post sobre o lowpro.
Isto não vai ser exatamente um tutorial, apenas uma coleção de dicas sobre esta excelente extensão para o prototype, o Lowpro.
O Lowpro ja foi até transformado em um plugin para o rails, o UJS Rails Plugin, mas sinceramente, eu prefiro adicionar a meia duzia de linhas de javascript no meu application.js mesmo do que usar outro plugin só pra isto
Mas seguindo o baile, tudo o que vou escrever neste post, vocês podem usar com ou sem o rails, só sera necessário o prototype e o lowpro, inclusive o lowpro ja tem até uma versão que funciona junto com o jQuery para os que não gostam do prototype
Mas seguindo com o assunto deste post, o que vocês acham de adicionar alguns recursos Javascript e até mesmo AJAX em uma página pronta sem mexer no HTML gerado? nem para adicionar Javascript nos onclick, onblur, onXXX …
Na verdade, sera necessário adicionar pelo menos 3 linhas:
1 2 3 | <script src="prototype.js" type="text/javascript" charset="utf-8"></script> <script src="lowpro.js" type="text/javascript" charset="utf-8"></script> <script src="meuarquivodescripts.js" type="text/javascript" charset="utf-8"></script> |
A última linha, ou seja o “meuarquivodescripts.js” define o arquivo onde vamos trabalhar
Um dos recursos que eu acho mais espetaculares no lowpro, é o suporte a comportamentos …
Como assim?
imagine a seguinte situação:
Tenho uma página pronta, é uma página de listagem, e no final da tabela tem um link para a criação de um novo item, hoje toda a página é atualizada quando clico neste link, mas eu quero que apenas a parte da tablea seja substituida pelo formulário, em vez de recarregar a tela toda …
Bom isto é muito fácil de fazer ![]()
E não vamos precisar de praticamente nenhum código …
Claro, precisamos de alguma forma de identificar o link de adicionar novo objeto na tabela, então, consideremos que este e todos os links que quisermos que sejam abertos por ajax no lugar da tabela, tenham a classe “my_ajax_link”, e consideremos que a tabela esta dentro de uma DIV com ID ‘minha_tabela’…
Se isto for verdade, podemos fazer o seguinte:
1 | Event.addBehavior({ 'a.my_ajax_link' : Remote.Link({update:'minha_tabela'}) }); |
Com isto, todos os links que tenham a classe “my_ajax_link” quando clicados vão ser executados via ajax, e vão substituir o conteúdo do elemento de id “minha_tabela” …
OK, mas é só isto que eu posso fazer com o tal de lowpro? E estes tais de comportamentos?
Na verdade o negócio é bem mais flexivel que isto ![]()
Vamos ver mais alguns exemplos …
Se você quiser executar algum código no momento em que a página aparecer para o usuário (não é no onload, o onload só é executado depois de toda a página carregada
)
Basta fazer algo assim:
1 2 3 | Event.onReady(function() { $$('h1').first().innerHTML += ' (Version ' + LowPro.Version + ')'; }); |
Isto vai mostrar a versão do Lowpro dentro da primeira tag H1 da página ![]()
O Event.onReady executa um código qualquer no momento em que o DOM da página estiver carregado, o que é bem antes de carregas as imagens, por exemplo, e o onload só sera executado depois de tudo carregado …
Outro exemplo bem simples:
Eu quero que quando o usuário colocar o mouse sobre um link ele fique verde, quando tirar o mouse ele volte a cor padrão, e quando clicar o link incremente um contador dizendo quantas vezes ele ja foi clicado …
Para isto precisamos criar um comportamento …
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var LinkBehavior = Behavior.create({ initialize : function() { this.count = 0; }, onmouseover : function() { this.element.addClassName('goo'); }, onmouseout : function() { this.element.removeClassName('goo'); }, onclick : function() { this.element.innerHTML = 'Clicks: ' + ++this.count; return false; } }); |
O que vai fazer os nossos links mudarem de cor é a classe “goo”, por tanto para que as cores mudem como eu disse antes, é preciso criar a classe “goo” em algum arquivo css carregado pela página.
Mas só este código apenas cria uma classe “comportamento” que define que o objeto que tiver este comportamento vai receber a classe “goo” no evento onmouseover, tera esta classe removida no evento onmouseout e vai ter o conteúdo alterado quando clicado.
Para que isto funcione com todos os links da página, precisamos adicionar este comportamento aos links da página …
1 | Event.addBehavior({ 'a' : LinkBehavior }); |
Pronto, agora todos os links vão ter o comportamento especificado pela classe.
Claro que alterar assim todos os links de uma página não é a coisa mais útil do mundo, mas se compararmos este último exemplo com o anterior, podemos ver que o lowpro, utiliza css selectors para identificar onde os behaviors devem ser aplicados …
Como assim?
Tudo bem, esta melhorando, mas eu não curti esta idéia de ter que criar uma classe para definir o que eu quero que aconteça quando o usuário clicar em um link, não tem como fazer tudo isto com uma linha de código só?
Na verdade tem
1 | Event.addBehavior({'a#bung:click' : function () {alert('Você Clicou no link');return false;}}) |
O que estamos fazendo agora é ligar uma função javascript ao evento onclick da tag a com ID bung.
E como é possível ver (talver não tão fácil por que a letra do código aqui no blog é meio pequena, o parâmetro apra a função addBehavior é um objeto, esta sendo utilizada a sintaxe de objetos JSON para passar os parâmetros, por tanto, isto quer dizer que podemos fazer algo parecido com isto:
1 2 3 4 5 | Event.addBehavior({ 'a#bung:click' : function () {alert('Você Clicou no link');return false;}, 'a.ajax_link' : Remote.Link({update:'qualquerid'}), '.mudacoreconta': LinkBehavior }) |
Ou seja, passar diversos itens a cada chamada do Event.addBehavior.
Outra coisa interessante, é que novas chamadas, vão empilhar métodos na fila se chamada, ou seja, este código:
1 2 3 | Event.addBehavior({'a#bung:click' : function () {alert('Você Clicou no link');return false;}}) Event.addBehavior({'a#bung:click' : function () {alert('Você Clicou no link novamente');return false;}}) Event.addBehavior({'a#bung:click' : function () {alert('Você Clicou no link');return false;}}) |
Vai mostrar 3 mensagens quando o link for clicado.
Uma outra coisa interessante do lowpro é que ele pode aplicar os comportamentos em objetos que forem carregados por uma chamada ajax também, para isto precisamos de mais uma linha de código (pode ser executada no inicio da aplicação)
1 | Event.addBehavior.reassignAfterAjax = true; |
Isto configura o lowpro para reaplicar os eventos após cada chamada AJAX.
Claro que podemos misturar isto com outras bibliotecas Javascript, por exemplo no meu pet project de planejamento doméstico, eu tenho o seguinte código no application.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var WindowedForm = Behavior.create({ onclick : function() { if (remote_window){ remote_window.destroy(); } remote_window = new UI.Window({ theme: 'leopard', width: 600, height: 300, focus : true, minimize : false }); remote_window.setAjaxContent(this.element.href, {method: 'get'}); remote_window.center(); remote_window.activate(); remote_window.show(true); return false; } }); remote_window = false; Event.addBehavior({ 'a.action_form' : WindowedForm }); |
Isto faz com que todos os links que tenham a classe action_form sejam abertos em uma janela modal criada pela biblioteca “prototype-ui”.
Ahh, e para finalizar com chave de ouro, uma dica para quem usa rails e will_paginate para a paginação, o código que vou mostrar agora utiliza a lowpro para fazer com que a paginação da will_paginate passe a funcionar via AJAX
1 2 3 | Event.addBehavior({ 'div.pagination a' : Remote.Link({update:'nomedadiv'}) }) |
Pronto, só isto, agora é só fazer a action correspondente retornar apenas a tabela e não a página toda quando for uma requisição AJAX, e colocar uma div com ID “nomedadiv” na volta da tabela
E por último mas não menos importante, não esqueça de alterar o código no servidor para retornar ou Javascript para ser executado, ou a parte do HTML desejado, mas sem as tags html,head,body, …
O lowpro facilita muito o trabalho no client, mas as respostas as chamadas AJAX continuam necessitando ser implementadas adequadamente.
Bom, espero que este post ajude alguem ![]()
Quaisquer dúvidas podem perguntar por aqui mesmo
Tags: ajax, Java, javascript, não intrusivo, unobstrusive
English Version here
Antes de iniciar a implementação do chat, nos precisamos instalar as dependências do juggernaut, ele precisa das gems json e eventmachine instaladas, para instalar ambas basta executar o seguinte comando:
Agora estamos prontos para começar a brincar …
Primeiro, vamos criar uma aplicação rails e instalar o plugin juggernaut com os seguitnes comandos:
O Juggernaut usa um servidor externo implementando Flash XML Push, e precisamos configurar este servidor, então vamos editar o arquivo: config/juggernaut.yml
Coloquei aqui no blog apenas as configurações que precisei alterar
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | PUSH_PORT: 8080 ... DEFAULT_CHANNELS: - "chat" ... PUSH_HELPER_HOST: "localhost" ... SECRET: "481516232342edededededed" ... LOGIN_GET_URL: "http://localhost:3000/session/login" LOGOUT_GET_URL: "http://localhost:3000/session/logout" ... SESSION_ID: "_chattest_session_id" ... BASE64: true |
Precisei alterar PUSH_PORT Por que no linux apenas o root pode ouvir em portas inferiores a 1024, e mesmo assim eu não acho que a porta 443 padrão seja uma boa escolha, ja que esta é a porta padrão do protocolo HTTPS.
A linhe com PUSH_HELPER_HOST precisa ser alterada para o mesmo nome do servidor que for utilizado para a acessar a aplicação, em desenvolvimento, localhost deve resolver, mas não esqueça de alterar esta configuração quando for fazer o deploy do seu chat.
LOGIN_GET_URL e LOGOUT_GET_URL são utilizadas para notificar a aplicação quando um usuário conecta ou desconecta, apenas o desconectar é realmente importante para nós.
A SESSION_ID precisa ser igual ao nome do cookie utilizado como marcador de sessão da aplicação, no rails 1.2.x fica no application controller, no rails 2.0 esta configuração vai para o enviroment.rb
e BASE64 é a configuração mágica que habilita o uso dos helpers do rails para geração do Javascript que vai ser utilizado.
Agora vamos começar a criar o layout da aplicação.
Crie um arquivo de nome pubic/stylesheets/public.css com o seguinte conteúdo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | body { background-color: white; } #users { float: left; width: 200px; height: 400px; border-style: inset; overflow: auto; color: white; background-color: gray; } #dasd { height: 400px; margin-left: 5px; border-style: inset; overflow: auto; color: white; background-color: gray; } #controls { clear: both; padding: 0 0 0 0; height: 55px; vertical-align: top; border-style: inset; overflow: auto; color: white; background-color: gray; } |
e o arquivo: app/views/layouts/application.rhtml com o seguinte conteúdo.
1 2 3 4 5 6 7 8 9 10 | <html> <head> <title>Chat Test</title> <%= stylesheet_link_tag 'public' %> <%= javascript_include_tag :defaults %> </head> <body> <%= yield %> </body> </html> |
Com o layout pronto (sim, esta bem feio, mas eu não sou designer eu sou um desenvolvedor, então solicite ao designer da sua equipe a criação de um novo layout para este chat
) vamos criar o código fonte necessário e o banco de dados com estes 4 comandos:
Layout pronto, arquivos criados, agora só precisamos escrever o código da aplicação ![]()
Vamos começar editando o model OnlineUser (app/model/online_user.rb) para adicionar algumas validações, elas não são realmente necessárias, são resquícios da primeira tentativa de implementação do chat, mas mesmo assim mantive elas no código
1 2 3 4 | class OnlineUser < ActiveRecord::Base validates_presence_of :username, :session_id, :last_seen validates_uniqueness_of :username, :if => Proc.new {|user| user.online } end |
Agora a view principal da aplicação no arquivo:app/views/chat/index.rhtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!-- Register with Juggernault --> <%= listen_to_juggernaut_channels [:generic],session.session_id %> <!-- The Users List --> <div id="users"> <ul id="users_list"></ul> </div> <!-- The messages pane --> <div id="dasd"></div> <!-- The controls pane (login and send messages) --> <div id="controls"><%= render :partial => 'login' %></div> <!-- An util javascript to scroll the messages window --> <script type="text/javascript"> function scrollMessages(){ $('dasd').scrollTop = $('dasd').scrollHeight; } </script> |
Como podem ver é bem simples, apenas 3 DIVs, um javascript para fazer o scroll da tela de mensagens e um comando para inicializar o juggernaut no canal “generic” e utilizando o session_id como identificador individual do usuário.
A DIV de mensagens esta com o id dasd por que eu estava testando algumas possibilidades de conflito e esqueci de mudar o ID depois
Como pode ser visto na página index, precisaremos de um partial de nome “login” e vamos também precisar de um de nome “controls”, então vamos começar codificando o partial “controls”: app/views/chat/_controls.rhtml
1 2 3 4 5 6 | <% form_remote_tag( :url => { :action => :say }, :complete => "$('message').value = '';$('message').focus();" ) do %> <%= text_field_tag( 'message', '', { :size => 90, :id => 'message'} ) %> <%= submit_tag "Send" %> <% end %> |
Bastante simples, possui apenas uma tag remote_form_tag que fara um post para a action “say”, um campo de texto para a mensagem e um botão de submit, quando o formulário é enviado o foco volta para o campo message para que o usuário possa digitar a próxima mensagem …
E o partial de login: app/views/chat/_login.rhtml
1 2 3 4 5 6 7 | <%= "#{@message}<br/>" if @message %><% form_remote_tag( :url => { :action => :login }, :complete => "$('username').value = ''", :after => "$('login').disabled = true" ) do %> <%= text_field_tag( 'username', '', { :size => 90, :id => 'username'} ) %> <%= submit_tag "Join", :id => 'login' %> <% end %> |
Bastante parecido com o anterior, mas faz o post para uma action de nome “login”, e o javascript agora desabilita o botão quando o formulário é enviado, e também possui uma mensagem que deve aparecer para o usuário informando que o apelido ja esta sendo utilizado.
Agora todas as views ja estão prontas, podemos passar para a codificação da lógica da aplicação.
Como podemos ver nas views até agora, precisaremos de um controller de nome “chat” com dois métodos: login e say
vamos analisar um pouco o código do chat controller: app/controllers/chat_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | class ChatController < ApplicationController #this method does not need to exist, but I like to see it here, it only needs to render the index.rhtml view def index end def login #creates a new OnlineUser record, this is used to store who are the users that are online now @user = OnlineUser.new @user.username = Juggernaut.html_and_string_escape params[:username] @user.session_id = session.session_id @user.online = true @user.last_seen = Time.now #if we can save, it means that there is no other user with the same nick online, so this user can join the chat if @user.save #let's save the username in the session for future reference session[:username] = @user.username #if there are online users, fill the users box for the new user know who is online @users = OnlineUser.find(:all, :conditions => ["online = true and id != ?", @user.id]) if @users.size >0 data = render_to_string(:update) do |page| @users.each {|u| page.insert_html :bottom, :users_list, %Q{<li id="user_#{u.username}">#{u.username}</li>} } end #send the javascript only to the new user Juggernaut.send_to(@user.session_id, data) end #create a javascript call to add the new user to the end of the online users list data = render_to_string(:update) do |page| page.insert_html :bottom, :users_list, %Q{<li id="user_#{@user.username}">#{@user.username}</li>} page.insert_html :bottom, :dasd, "<b>user #{@user.username} just joined the chat</b><br/>" end #add the new user to the chat channel Juggernaut.add_channel(@user.session_id, 'chat') #send the javascript to all users in the chat channel Juggernaut.send_data(data, 'chat') render(:update) do |page| page.replace_html 'controls', :partial => "controls" end else @message = 'This nick name is already in use, please choose another' render(:update) do |page| page.replace_html 'controls', :partial => "login" end end end def say #escape the message, that way the user can not harm others sending HTML ot JavaScript commands message = "#{session[:username]}: #{Juggernaut.html_and_string_escape(params[:message])}" #create a javascript to add the new message to the end of the messages screen and scroll the div to the bottom data = render_to_string(:update) do |page| page.insert_html :bottom, :dasd, "#{message}<br/>" page.call "scrollMessages" end #send the message to all users Juggernaut.send_data(data, 'chat') render :nothing => true end end |
O método say é bastante simples, então vamos começar analisando este:
Primeiro a mensagem é reconstruída adicionando o nome do usuário no inicio da mensagem enviada removendo todos os possíveis códigos HTML ou Javascript para garantir a segurança dos outros usuários.
Então é construido um Javascript utilizando o JavaScriptBuilder do rails e o método render_to_string para adicionar a nova mensagem no final da div “dasd” e fazer o scroll caso necessário.
Então este Javascript é enviado para todos os clientes que estão ouvindo o canal “chat”.
Agora um pouco sobre o método login:
Esta é quase toda a lógica que precisamos, a única parte que falta é remover um usuário da lista de usuários online de todos os participantes quando este fechar o browser ou deixar o chat por qualquer motivo, para isto vamos ao método logout do controller session: app/controllers/session_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class SessionController < ApplicationController #Called when a user disconnect (a refresh in the browser causes this to be called too) def logout #search for the user record using the session_id @u = OnlineUser.find_by_session_id(session.session_id) reset_session #if a user was found if @u username = @u.username #remove it from the database @u.destroy #remove from the online users list from all users, and tell others this user left the chat data = render_to_string(:update) do |page| page.remove "user_#{username}" page.insert_html :bottom, :dasd, "<b>User #{username} left the chat</b><br/>" end Juggernaut.send_data(data,'chat') end render :nothing => true end def login render :nothing => true end end |
O método login não faz nada, pois não precisamos desta notificação para o chat, então vamos a descrição do método logout …
E isto é tudo pessoal! (isto só fica engraçado em ingles
)
Agora só falta executar a aplicação.
Para executar esta aplicação, precisamos iniciar o servidor do Rails como padrão, e então iniciar o push_server, para fazer isto basta executar os seguintes dois comandos.
E acessar o seu chat novo em folha no endereço: http://localhost:3000/chat
Eu criei este exemplo emquanto estudava um pouco sobre o Juggernaut, é possível que existam maneiras mais fáceis de implementar isto, mas eu acredito que seja um bom ponto de partida.
Quaisquer dicas para melhorar o exemplo são muito bem vindas.
Tags: ajax, chat, example, flash, howto, push, rails, reverse, Ruby, simple, xml