RJS, até que um dia...

Posted by André Ribeiro Camargo Sat, 20 Jan 2007 14:09:00 GMT

Dias atrás precisei implementar um master-detail de 3 níveis utilizando drop-down lists no painel de controle do lojista no PelotasCenter.

A história é o seguinte:

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.

Ok?

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.

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)

Conseguiu digerir?

Então vamos implementar…

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!)

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.

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.

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

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

  def bairros
    fretes = Frete.find :all, :conditions => ['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 => 'bairro_id'
    fretes.collect {|f| f.bairro }
  end

end

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:

<div id="campo_reducao_entregador_id" class="campo">
  <label for="reducao_entregador_id">Entregador</label>
  <select id="reducao_entregador_id" name="reducao[entregador_id]">
    <%= options_from_collection_for_select @loja.entregadores, :id, :nome, @reducao.entregador_id.to_i  %>
  </select>
</div>

Certo… 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.

Mas esta técnica é tão… jurássica.

Vamos utilizar alguma técnica mais atual, que tal um pouco de AJAX… é relativamente fácil implementar isso com Rails, normalmente carregaríamos um fragmento de HTML que implementa o campo através de uma requisição AJAX enganchada no método onchange do drop-down.

Mas esta técnica é tão… antiga.

Percebi neste momento que chegou a minha hora de brincar com RJS (provavelmente a maioria de vocês estão pensando: demorô! demorô!)

RJS é acrônimo de Remote JavaScript, uma funcionalidade integrada ao Rails 1.1 que permite escrever uma View em Ruby mas cujo resultado é código Javascript. Sinistro? Mas é isso aí mesmo…

Mas antes disso, preciso capturar o evento onchange dos drop-downs para então fazer uma chamada AJAX que vai rodar o meu RJS (retornando um javascript que atualiza as opções dos drop-downs necessários).

Para capturar os eventos, utilizei o helper observe_field, na mesma view que monta o formulário, abaixo dos campos incluí o seguinte código:

<%= observe_field :reducao_entregador_id,
  :url => {:action => :update_dropdown},
  :with => 'reducao_entregador_id',
  :loading => "Element.show('unidade_federativa_id_spinner', 'cidade_id_spinner', 'bairro_id_spinner')",
  :complete => "Element.hide('unidade_federativa_id_spinner', 'cidade_id_spinner', 'bairro_id_spinner')" 
%>

<%= observe_field :reducao_unidade_federativa_id,
  :url => {:action => :update_dropdown},
  :with => "'reducao_entregador_id='+$('reducao_entregador_id').value+'&reducao_unidade_federativa_id='+value",
  :loading => "Element.show('cidade_id_spinner', 'bairro_id_spinner')",
  :complete => "Element.hide('cidade_id_spinner', 'bairro_id_spinner')" 
%>

<%= observe_field :reducao_cidade_id,
  :url => {:action => :update_dropdown},
  :with => "'reducao_unidade_federativa_id='+$('reducao_unidade_federativa_id').value+'&reducao_entregador_id='+$('reducao_entregador_id').value+'&reducao_cidade_id='+value",
  :loading => "Element.show('bairro_id_spinner')",
  :complete => "Element.hide('bairro_id_spinner')" 
%>
Note que os helpers vão executar uma action “update_dropdown” do ReducaoController, que segue abaixo:
  def update_dropdown
    @reducao = Reducao.find_by_id(params[:reducao_id]) 
    @reducao = Reducao.new(:loja => @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

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.

Os elementos “unidade_federativa_id_spinner”, “cidade_id_spinner” e “bairro_id_spinner” são gifs animados que estou utilizando para indicar o progresso da operação. O campo de município ficou assim:
<div id="campo_reducao_cidade_id" class="campo">
  <label for="reducao_cidade_id">Cidade</label>
  <select id="reducao_cidade_id" name="reducao[cidade_id]">
    <option value="0">Todos</option>
    <%= options_from_collection_for_select @reducao.cidades, :id, :nome, @reducao.cidade_id.to_i  %>
  </select>
  <%= image_tag "progress.gif", :id => 'cidade_id_spinner', :style => 'display:none;' %>
</div>
Ok, agora preciso gerar o javascript que vai fazer o serviço sujo. Neste momento entra o RJS:
# 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

Caso você já tenha mexido com RJS, vais pensar: o que é html_select_options?

Não encontrei uma forma de manipular os <select /> diretamente via RJS, então escrevi uma. Adicione o código abaixo no public/javascripts/application.js
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);
    });
  }
}

E era isso… Está funcionando que é uma beleza :-)

Quem quiser trocar idéias a respeito da implementação, pode escrever para meu e-mail.

Em tempo, “trocar idéias” != “implementar para terceiros”. Tá bom? :-P

Posted in , ,

Comments are disabled