<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/stylesheets/rss.css" type="text/css"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">
  <channel>
    <title>boa idéia software livre: RJS, até que um dia...</title>
    <link>http://blog.boaideia.inf.br/articles/2007/01/20/rjs-at%C3%A9-que-um-dia</link>
    <language>en-us</language>
    <ttl>40</ttl>
    <description>a personalidade jurídica de andré ribeiro camargo</description>
    <item>
      <title>RJS, até que um dia...</title>
      <description>&lt;p&gt;Dias atrás precisei implementar um &lt;a href="http://en.wikipedia.org/wiki/Master-detail"&gt;master-detail&lt;/a&gt; de 3 níveis utilizando &lt;a href="http://en.wikipedia.org/wiki/Drop-down_list" title="no caso html select"&gt;drop-down lists&lt;/a&gt; no painel de controle do lojista no &lt;a href="http://www.pelotascenter.com.br"&gt;PelotasCenter&lt;/a&gt;.&lt;/p&gt;


	&lt;p&gt;A história é o seguinte:&lt;/p&gt;


	&lt;p&gt;Tenho um cadastro de Entregadores e de Fretes. Os Entregadores são os meios de transporte que a empresa utiliza para despachar produtos para os consumidores. E os Fretes definem o valor que este Entregador cobra para levar a mercadoria até determinada UF(Unidade Federativa ou Estado)/Município/Bairro.&lt;/p&gt;


	&lt;p&gt;Ok?&lt;/p&gt;


	&lt;p&gt;Acontece que para promover vendas, o lojista pode oferecer descontos no valor do frete. Por exemplo: Compras acima de R$100 com entrega em Pelotas, frete grátis.&lt;/p&gt;


	&lt;p&gt;Preciso especificar para o sistema que o valor do frete para pedidos cujo total em mercadorias exceda R$100, tenha 100% de desconto no valor do frete para determinado Entregador (Moto-táxi por exemplo)&lt;/p&gt;


	&lt;p&gt;Conseguiu digerir?&lt;/p&gt;


	&lt;p&gt;Então vamos implementar&amp;#8230;&lt;/p&gt;


	&lt;p&gt;A tela ficaria assim: um drop-down para o Entregador, outro para UF, outro para Município, outro para acomodar o Bairro. Além disso precisamos de um campo para receber o valor limite do total em produtos e outro para definirmos o percentual de desconto (muito importante!)&lt;/p&gt;


	&lt;p&gt;Preciso que ao atualizar o Entregador, os outros drop-down se atualizem para refletir a UF/Município/Bairro para onde aquele entregador tem Frete cadastrado. Ao atualizar UF, mostre somente os municípios/bairros daquela UF e ao atualizar o município, mostre somente os bairros para aquela cidade.&lt;/p&gt;


	&lt;p&gt;Para descobrir quais as opções possíveis, defini no Model Reducao (que cuida das reduções nos valores de frete) métodos que baseado nos atributos do objeto calculam as opções disponíveis.&lt;/p&gt;


&lt;pre&gt;
class Reducao &amp;lt; ActiveRecord::Base
  ...
  def unidades_federativas
    fretes = Frete.find :all, :conditions =&amp;gt; ['loja_id = ? and entregador_id = ? and unidade_federativa_id != 0', loja_id, entregador_id], :group =&amp;gt; 'unidade_federativa_id'
    fretes.collect {|f| f.unidade_federativa }
  end

  def cidades
    fretes = Frete.find :all, :conditions =&amp;gt; ['loja_id = ? and entregador_id = ? and unidade_federativa_id = ? and cidade_id != 0', loja_id, entregador_id, unidade_federativa_id], :group =&amp;gt; 'cidade_id'
    fretes.collect {|f| f.cidade }
  end

  def bairros
    fretes = Frete.find :all, :conditions =&amp;gt; ['loja_id = ? and entregador_id = ? and unidade_federativa_id = ? and cidade_id = ? and bairro_id != 0', loja_id, entregador_id, unidade_federativa_id, cidade_id], :group =&amp;gt; 'bairro_id'
    fretes.collect {|f| f.bairro }
  end

end
&lt;/pre&gt;

	&lt;p&gt;Beleza, com isso resolvo o problema de saber quais opções carregar nos drop-downs, então pude colocar algo assim na partial form do controller para cada campo:&lt;/p&gt;


&lt;pre&gt;
&amp;lt;div id="campo_reducao_entregador_id" class="campo"&amp;gt;
  &amp;lt;label for="reducao_entregador_id"&amp;gt;Entregador&amp;lt;/label&amp;gt;
  &amp;lt;select id="reducao_entregador_id" name="reducao[entregador_id]"&amp;gt;
    &amp;lt;%= options_from_collection_for_select @loja.entregadores, :id, :nome, @reducao.entregador_id.to_i  %&amp;gt;
  &amp;lt;/select&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/pre&gt;

	&lt;p&gt;Certo&amp;#8230; agora preciso fazer os drop-downs se atualizarem conforme os outros se modifiquem. Eu poderia fazer submeter o formulário a cada alteração do drop-down, reprocessando a página.&lt;/p&gt;


	&lt;p&gt;Mas esta técnica é tão&amp;#8230; jurássica.&lt;/p&gt;


	&lt;p&gt;Vamos utilizar alguma técnica mais atual, que tal um pouco de &lt;span class="caps"&gt;AJAX&lt;/span&gt;&amp;#8230; é relativamente fácil implementar isso com Rails, normalmente carregaríamos um fragmento de &lt;span class="caps"&gt;HTML&lt;/span&gt; que implementa o campo através de uma requisição &lt;span class="caps"&gt;AJAX&lt;/span&gt; enganchada no método onchange do drop-down.&lt;/p&gt;


	&lt;p&gt;Mas esta técnica é tão&amp;#8230; antiga.&lt;/p&gt;


	&lt;p&gt;Percebi neste momento que chegou a minha hora de brincar com &lt;span class="caps"&gt;RJS&lt;/span&gt; (provavelmente a maioria de vocês estão pensando: demorô! demorô!)&lt;/p&gt;


	&lt;p&gt;&lt;a href="http://www.codyfauser.com/2005/11/20/rails-rjs-templates"&gt;&lt;span class="caps"&gt;RJS&lt;/span&gt;&lt;/a&gt; é acrônimo de Remote JavaScript, uma funcionalidade integrada ao &lt;a href="http://weblog.rubyonrails.org/2006/3/28/rails-1-1-rjs-active-record-respond_to-integration-tests-and-500-other-things"&gt;Rails 1.1&lt;/a&gt; que permite escrever uma View em Ruby mas cujo resultado é código Javascript. Sinistro? Mas é isso aí mesmo&amp;#8230;&lt;/p&gt;


	&lt;p&gt;Mas antes disso, preciso capturar o evento onchange dos drop-downs para então fazer uma chamada &lt;span class="caps"&gt;AJAX&lt;/span&gt; que vai rodar o meu &lt;span class="caps"&gt;RJS&lt;/span&gt; (retornando um javascript que atualiza as opções dos drop-downs necessários).&lt;/p&gt;


	&lt;p&gt;Para capturar os eventos, utilizei o helper &lt;a href="http://api.rubyonrails.com/classes/ActionView/Helpers/PrototypeHelper.html#M000532"&gt;observe_field&lt;/a&gt;, na mesma view que monta o formulário, abaixo dos campos incluí o seguinte código:&lt;/p&gt;


&lt;pre&gt;
&amp;lt;%= observe_field :reducao_entregador_id,
  :url =&amp;gt; {:action =&amp;gt; :update_dropdown},
  :with =&amp;gt; 'reducao_entregador_id',
  :loading =&amp;gt; "Element.show('unidade_federativa_id_spinner', 'cidade_id_spinner', 'bairro_id_spinner')",
  :complete =&amp;gt; "Element.hide('unidade_federativa_id_spinner', 'cidade_id_spinner', 'bairro_id_spinner')" 
%&amp;gt;

&amp;lt;%= observe_field :reducao_unidade_federativa_id,
  :url =&amp;gt; {:action =&amp;gt; :update_dropdown},
  :with =&amp;gt; "'reducao_entregador_id='+$('reducao_entregador_id').value+'&amp;#38;reducao_unidade_federativa_id='+value",
  :loading =&amp;gt; "Element.show('cidade_id_spinner', 'bairro_id_spinner')",
  :complete =&amp;gt; "Element.hide('cidade_id_spinner', 'bairro_id_spinner')" 
%&amp;gt;

&amp;lt;%= observe_field :reducao_cidade_id,
  :url =&amp;gt; {:action =&amp;gt; :update_dropdown},
  :with =&amp;gt; "'reducao_unidade_federativa_id='+$('reducao_unidade_federativa_id').value+'&amp;#38;reducao_entregador_id='+$('reducao_entregador_id').value+'&amp;#38;reducao_cidade_id='+value",
  :loading =&amp;gt; "Element.show('bairro_id_spinner')",
  :complete =&amp;gt; "Element.hide('bairro_id_spinner')" 
%&amp;gt;
&lt;/pre&gt;

Note que os helpers vão executar uma action &amp;#8220;update_dropdown&amp;#8221; do ReducaoController, que segue abaixo:
&lt;pre&gt;
  def update_dropdown
    @reducao = Reducao.find_by_id(params[:reducao_id]) 
    @reducao = Reducao.new(:loja =&amp;gt; @loja) if @reducao.blank?

    @reducao.entregador = @loja.entregadores.find(params[:reducao_entregador_id]) unless params[:reducao_entregador_id].blank? 
    @unidades_federativas = @reducao.unidades_federativas.collect {|uf| [uf.sigla, uf.id] }
    @reducao.unidade_federativa_id = params[:reducao_unidade_federativa_id] unless params[:reducao_unidade_federativa_id].blank?
    @cidades = @reducao.cidades.collect {|cidade| [cidade.nome, cidade.id] }
    @reducao.cidade_id = params[:reducao_cidade_id] unless params[:reducao_cidade_id].blank?

    @bairros = @reducao.bairros.collect {|bairro| [bairro.nome, bairro.id] }
    @reducao.bairro_id = params[:reducao_bairro_id] unless params[:reducao_bairro_id].blank?

    headers['Content-Type'] = "text/javascript; charset=utf-8" 
  end
&lt;/pre&gt;

	&lt;p&gt;Basicamente o serviço da Action é carregar (nas variáveis @unidades_federativas, @cidades e @bairros) as opções que deverão ser incluídas no drop-down.&lt;/p&gt;


Os elementos &amp;#8220;unidade_federativa_id_spinner&amp;#8221;, &amp;#8220;cidade_id_spinner&amp;#8221; e &amp;#8220;bairro_id_spinner&amp;#8221; são gifs animados que estou utilizando para indicar o progresso da operação. O campo de município ficou assim:
&lt;pre&gt;
&amp;lt;div id="campo_reducao_cidade_id" class="campo"&amp;gt;
  &amp;lt;label for="reducao_cidade_id"&amp;gt;Cidade&amp;lt;/label&amp;gt;
  &amp;lt;select id="reducao_cidade_id" name="reducao[cidade_id]"&amp;gt;
    &amp;lt;option value="0"&amp;gt;Todos&amp;lt;/option&amp;gt;
    &amp;lt;%= options_from_collection_for_select @reducao.cidades, :id, :nome, @reducao.cidade_id.to_i  %&amp;gt;
  &amp;lt;/select&amp;gt;
  &amp;lt;%= image_tag "progress.gif", :id =&amp;gt; 'cidade_id_spinner', :style =&amp;gt; 'display:none;' %&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/pre&gt;

Ok, agora preciso gerar o javascript que vai fazer o serviço sujo. Neste momento entra o &lt;span class="caps"&gt;RJS&lt;/span&gt;:
&lt;pre&gt;
# atualiza unidades federativas
unless params[:reducao_unidade_federativa_id]
  page.html_select_options.clear 'reducao_unidade_federativa_id'
  page.html_select_options.add 'reducao_unidade_federativa_id', 'Todos', 0
  page.html_select_options.load 'reducao_unidade_federativa_id', @unidades_federativas, @reducao.unidade_federativa_id
end 

# atualiza cidades
unless params[:reducao_cidade_id]
  page.html_select_options.clear 'reducao_cidade_id'
  page.html_select_options.add 'reducao_cidade_id', 'Todos', 0
  page.html_select_options.load 'reducao_cidade_id', @cidades, @reducao.cidade_id
end

# atualiza bairros
unless params[:reducao_bairro_id]
  page.html_select_options.clear 'reducao_bairro_id'
  page.html_select_options.add 'reducao_bairro_id', 'Todos', 0
  page.html_select_options.load 'reducao_bairro_id', @bairros, @reducao.bairro_id
end
&lt;/pre&gt;

	&lt;p&gt;Caso você já tenha mexido com &lt;span class="caps"&gt;RJS&lt;/span&gt;, vais pensar: o que é html_select_options?&lt;/p&gt;


Não encontrei uma forma de manipular os &amp;lt;select /&amp;gt; diretamente via &lt;span class="caps"&gt;RJS&lt;/span&gt;, então escrevi uma. Adicione o código abaixo no public/javascripts/application.js
&lt;pre&gt;
var HtmlSelectOptions = {}

HtmlSelectOptions = {
  clear: function(dom_id) {
    with($(dom_id)) {
      while (hasChildNodes()) removeChild(lastChild);
      selectedIndex = 0;
    }
  },
  add: function(dom_id, text, value, selected) {
    var option = document.createElement('option');
    option.appendChild(document.createTextNode(text));
    option.setAttribute('value', value);
    if (selected) option.setAttribute('selected', '');
    $(dom_id).appendChild(option);
  },
  load: function(dom_id, options, selected) {
    if (options == null) return;
    options.each(function(item, index) {
      HtmlSelectOptions.add(dom_id, item[0], item[1], item[1] == selected);
    });
  }
}
&lt;/pre&gt;

	&lt;p&gt;E era isso&amp;#8230; Está funcionando que é uma beleza :-)&lt;/p&gt;


	&lt;p&gt;Quem quiser &lt;strong&gt;trocar idéias a respeito da implementação&lt;/strong&gt;, pode escrever para meu e-mail.&lt;/p&gt;


	&lt;p&gt;Em tempo, &amp;#8220;trocar idéias&amp;#8221; != &amp;#8220;implementar para terceiros&amp;#8221;. Tá bom? :-P&lt;/p&gt;</description>
      <pubDate>Sat, 20 Jan 2007 12:09:00 -0200</pubDate>
      <guid isPermaLink="false">urn:uuid:7cccb74d-2381-4355-805b-024d68e46f73</guid>
      <author>André Ribeiro Camargo</author>
      <link>http://blog.boaideia.inf.br/articles/2007/01/20/rjs-at%C3%A9-que-um-dia</link>
      <category>trabalho</category>
      <category>nerd</category>
      <category>rails</category>
    </item>
  </channel>
</rss>

