Marczal

Blog for Ruby, Rails, Java and others programming languages

Populando Estados e Cidades no Rails

with 11 comments

Recentemento foi publicado no blog EduResende uma migration com todos os estados e cidades do brasil. Essa migration pode ser encontrada no github.

Utilizando essa migration, nesse post vou demonstrar como popular um formulário com os estados e cidades do brasil em um projeto rails.

OBS: Para esse exemplo vou usar toda a codificação padrão do rails em inglês.

Primeiro criaremos um novo projeto com o nome de states_cities com o seguinte comando:

$ rails  states_cities

Ao término da execução do comando o rails criou toda a estrutura de diretórios.

Agora criaremos dois modelos um para os estados e outro para as cidades.

$ script/generate model  state symbol:string name:string

$ script/generate model  city name:string state:references

Com nossos modelos criados podemos então associa-los, lembrando que é uma associação 1-n, ou seja, um estado possui muitas cidades e uma cidade pertence a um único estado.

Então abra o arquivo city.rb e adicione a seguinte linha de código:

belongs_to :state

Após no arquivo state.rb adicione a seguinte linha:

has_many :cities

Agora vamos criar a migration para popular os estados e cidades do brasil.

$ script/generate migration populate_states_and_cities

Nesse você pode baixar a migration citada acima, lembrando que se você baixar ela você deve trocar os nomes de cidade, Cidade, cidades para city, City, cities e também de estado, Estado para state, State. Você também pode pegar a migration desse exemplo, no link ao final da página.

Após colocar os dados da migration rode o seguinte comando:

$ rake db:migrate

Após esse comando seu banco de dados estará populado,  então podemos começar a montar o formulário de exemplo. Para isso vamos usar o generate do rails.

$ script/generate scaffold Person name:string city:references

Com esse comando o rails criará todos os arquivos necessários para você criar, deletar, alterar e mostrar seus registros.

Camos então relacionar pessoa com cidade  (1-n)

no modelo  cidade coloque a seguinte linha:

#app/modes/person.rb

has_may :people

E no modelo pessoa deve coloque a seguinte linha:

#app/modes/city.rb

belongs_to :city

Agora rode o comando abaixo para criar a tabela people no banco

$ rake db:migrate

Com os modelos configurados podemos então trabalhar com as views:

Para isso  mudaremos um pouco os arquivos gerados pelo scaffold do rails.

Primeiro abra o arquivo new.html.erb e altere para ficar da seguinte forma:

#app/views/people/new.html.erb
<h1>New person</h1>
<%= render :partial => 'form', :locals => {:type => "Create"} %>

<%= link_to 'Back', people_path %>

E no arquivo edit.html.erb altere para semelhante ao seguinte código:

#app/views/people/edit.html.erb
<h1>Editing person</h1>
<%= render :partial => 'form', :locals => {:type => "Update"} %>

 <%= link_to "Show", people_path(@notice) %>
 |
 <%= link_to "Back to all", people_path %>

Com essa novas codificações prontas podemos criar um formulário comum tanto para a edição quanto para a criação,  para isso  na pasta app/views/people crie um arquivo chamado _form.html.erb

Vamos então codificar nele nosso formulário  juntamente com a parte responsável  por mostrar os estados e em seguida as cidades respectivas. Para para popular os estados usaremos o helper collection_select e para buscar as cidades do estado selecionado usaremos o helper observe_field.

Antes de codificar o _form.html.erb inclua as bibliotecas do prototype no application.html.erb que está na pasta app/views/layout/application.html.erb da sua aplicação. Para isso adicione o seguinte código no head do arquivo.

<%= javascript_include_tag :defaults %>

Agora vamos partir para o formulário, seu código  ficará assim:

#app/views/people/_form.html

<% form_for(@person) do |f| %>
 <%= f.error_messages %>

 <%= f.label :name %>
 <%= f.text_field :name %>

 <%= label_tag :state %>
 <%= collection_select(:state, :id, State.all, :id, :name, {:prompt => true})  %>

 <%= observe_field('state_id', :frequency => 0.25, :update => 'cities_div',
 :url => {:action => :load_cities}, :with => "'state_id='+value")%>

 <%= f.label :city %>
<div id='cities_div'></div>
<%= f.submit type %>

<% end %>

Feito isso precisamos criar no arquivo people_controller.rb a seguinte action:

#app/controllers/people_controller.rb

 def load_cities
   unless params[:state_id].blank?
     @state = State.find(params[:state_id])
     @cities = @state.cities.collect { |c| [c.name, c.id] }
     render :layout => false
   end
 end

Feito isso precisamos criar um arquivo em app/views/people com o nome de load_cities.html.erb e adicionar
o seguinte código:

#app/views/people/load_cities.html.erb
<% if @state %>
<%= select(:person, :city_id, @cities) %>
<% end %>

Para melhor visualizar nosso cadastro vamos deixar os arquivos index e show da seguinte maneira

#app/views/people/index.html.erb
<h1>Listing people</h1>
<table>
<tr>
<th>Name</th>
<th>City</th>
<th>State</th>
</tr>
<% @people.each do |person| %>
<tr>
<td><%=h person.name %></td>
<td><%=h person.city.name %></td>
<td><%=h person.city.state.name %></td>
<td><%= link_to 'Show', person %></td>
<td><%= link_to 'Edit', edit_person_path(person) %></td>
<td><%= link_to 'Destroy', person, :confirm => 'Are you sure?', :method => :delete %></td>
</tr>
<% end %></table>
<%= link_to 'New person', new_person_path %>
#app/views/people/show.html.erb

  <b>Name:</b>
  <%=h @person.name %>

  <b>City:</b>
  <%=h @person.city.name %>

  <b>City:</b>
  <%=h @person.city.state.name %>

<%= link_to 'Edit', edit_person_path(@person) %> |
<%= link_to 'Back', people_path %>

Agora podemos iniciar o servidor e testar o formulário de cadastro.

$ script/server

http://localhost:3000/people

Se tudo ocorreu bem, no momento que você escolher o estados, todas as cidades do estado selecionado apareceram logo abaixo.

Cadastre algumas Pessoas para testar.

Agora tente editar um dos registros. Você notará que a cidade e o estado não aparecem no formulário de edição. Para corrigir isso altere a action edit para:

  #app/controllers/people_controller.erb

  # GET /people/1/edit
  def edit
    @person = Person.find(params[:id])
    @state = State.find(@person.city.state)
    @cities = @state.cities.collect{|c| [c.name, c.id]}
  end

E no arquivo app/views/people/_form.html.erb deixe a div ‘cities_div” como o seguinte código:

  #app/views/people/_form.html.erb
<div id='cities_div'>
    <%= render :file => 'people/load_cities' %></div>

Agora ao tentar editar um registro você verá a cidade e o estado do registro.  Porém no controller ficamos com algumas repetições de código, vamos
alterá-lo para evitar essa repetição.

Seu novo controller deve ficar da seguinte maneira:

  #app/controllers/people_controller.rb
  class PeopleController < ApplicationController
  # GET /people
  # GET /people.xml
  def index
    @people = Person.all

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @people }
    end
  end

  # GET /people/1
  # GET /people/1.xml
  def show
    @person = Person.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @person }
    end
  end

  # GET /people/new
  # GET /people/new.xml
  def new
    @person = Person.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @person }
    end
  end

  # GET /people/1/edit
  def edit
    @person = Person.find(params[:id])
    find_state_and_cities(@person.city.state)
  end

  # POST /people
  # POST /people.xml
  def create
    @person = Person.new(params[:person])

    respond_to do |format|
      if @person.save
        flash[:notice] = 'Person was successfully created.'
        format.html { redirect_to(@person) }
        format.xml  { render :xml => @person, :status => :created, :location => @person }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @person.errors, :status => :unprocessable_entity }
      end
    end
  end

  # PUT /people/1
  # PUT /people/1.xml
  def update
    @person = Person.find(params[:id])

    respond_to do |format|
      if @person.update_attributes(params[:person])
        flash[:notice] = 'Person was successfully updated.'
        format.html { redirect_to(@person) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @person.errors, :status => :unprocessable_entity }
      end
    end
  end

  # DELETE /people/1
  # DELETE /people/1.xml
  def destroy
    @person = Person.find(params[:id])
    @person.destroy

    respond_to do |format|
      format.html { redirect_to(people_url) }
      format.xml  { head :ok }
    end
  end

  def load_cities
     find_state_and_cities(params[:state_id])
     render :layout => false
  end

private
 def find_state_and_cities(state)
   unless state.blank?
     @state = State.find(state)
     @cities = @state.cities.collect{|c| [c.name, c.id]}
   end
 end
end

Tudo parece funcionar bem agora, mas temos mais um problema.

Inserir uma validação no model person:

  #app/models/person.rb
  validates_presence_of :name

E agora tente criar um novo registro, mas antes selecione o estado e a cidade e deixe o nome em branco.

Quando o erro retornar você verá que a cidade e o estado que você escolheu não estão mais lá, precisamos corrigir isso então altere a action create deixando-a da seguinte forma:

#app/controllers/people_controller.rb
 # POST /people
  # POST /people.xml
  def create
    @person = Person.new(params[:person])
    find_state_and_cities(params[:state][:id])

    respond_to do |format|
      if @person.save
        flash[:notice] = 'Person was successfully created.'
        format.html { redirect_to(@person) }
        format.xml  { render :xml => @person, :status => :created, :location => @person }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @person.errors, :status => :unprocessable_entity }
      end
    end
  end

Ao editar algum registro e alguma validação o mesmo problema ocorrerá, por isso devemos alterar a action update para:


  # PUT /people/1
  # PUT /people/1.xml
  def update
    @person = Person.find(params[:id])
    find_state_and_cities(params[:state][:id])

    respond_to do |format|
      if @person.update_attributes(params[:person])
        flash[:notice] = 'Person was successfully updated.'
        format.html { redirect_to(@person) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @person.errors, :status => :unprocessable_entity }
      end
    end
  end

Agora tudo funciona de acordo!

Caso queira baixar essa aplicação de exemplo ela está disponível no github.

<% if @state %>
<%= select(:person, :city_id, @cities) %>
<% end %>

Written by marczal

junho 30, 2009 às 1:03 am

11 Respostas

Subscribe to comments with RSS.

  1. Post muito util !

    denise

    julho 5, 2009 at 9:57 pm

  2. Muito bom esse post! Bem explicado e não dá erro em nenhum momento!! Muito obrigada.

    Yana Gottschall

    outubro 31, 2009 at 8:52 pm

  3. Muito legal! Parabéns!

    Tenho uma pergunta, como seria para uma edição? pois ele teria que retornar a cidade salva por exemplo.

    Abraços

    Diego Nogueira

    fevereiro 18, 2010 at 12:56 pm

    • Olá, Obrigado pelo comentário

      Não sei se entendi bem sua pergunta, mas para a edição, no exemplo, é criado duas variáveis no controller as quais são acessadas na view, essas variáveis são carregadas durante a execução da action edit. São elas @state e @cities

      Segue o código para carregar essas variáveis:

      private
      def find_state_and_cities(state)
      unless state.blank?
      @state = State.find(state)
      @cities = @state.cities.collect{|c| [c.name, c.id]}
      end
      end

      Se vc verifcar na view temos o seguinte código.
      true}) %>

      o qual apartir da variável @state deixa o estado selecionado corretamente na view. Pois @state contém a váriavel state_id, que é a id gerada pelo collection_select.

      também temos o código
      #app/views/people/load_cities.html.erb

      que carrega todos as cidades e deixa selecionada a cidade certa. Pois, quando esse código é transformado em html seu id torna-se person_city_id

      Dessa forma a seleção da cidade fica correta porque no model Person temos a variável city_id. Então através da variável @person o rails consegue selecionar automaticamente a cidade correta chamado @person.city_id

      Espero ter ajudado.
      Abraços.

      marczal

      fevereiro 18, 2010 at 10:25 pm

  4. Opa cara show de bola teu post, eu montei um blog para usuários tirarem suas duvidas se voce puder ajudar algum deles ficaria grato.

    Amigos da Web

    março 1, 2010 at 4:45 pm

  5. Não acredito que isso seria útil para mim Diego kkkk

    Primeiro resultado no google ein kkkk
    http://www.google.com.br/search?q=estado+rails+3

    Jonatas Teixeira

    outubro 27, 2010 at 7:06 pm

  6. olá!
    Parabéns, ficou muito bom e claro o tutorial. Gostei!
    tentei dar uma implementada, fazer o mesmo esquema para País, Estados e cidades, mas não deu certo, parece que o observe_field não encherga a cidade mudando.
    tem algum esquema diferente quando tem 2 observe_field na mesma pagina?
    Valeu!

    Hamilton Mota

    novembro 29, 2010 at 5:18 pm

    • Olá Hamilton,

      Então você deve colocar o observer dos estados, que será necessário para carregar as cidades no partial que carrega os estados.

      Veja o exemplo que fiz no github no branch countries.

      Essa versão está com alguns problemas para validação que ainda não corrigi. Mas e coisa simples.

      Qualquer dúvida avisa.

      marczal

      dezembro 1, 2010 at 8:45 am

  7. Com o Rails 3 isso não funciona. A maioria das funções do prototype não funciona mais, incluindo a função observe_field. comofas?

    Fabiano

    dezembro 31, 2010 at 12:55 pm


Deixe um comentário