Populando Estados e Cidades no Rails
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
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 %>
Post muito util !
denise
julho 5, 2009 at 9:57 pm
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
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
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
Legal, posso ajudar sim.
marczal
março 2, 2010 at 11:46 am
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
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
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
Opa! consegui fazer funcionar no rails 3.
É só adicionar este helper no seu projeto: https://github.com/rails/prototype_legacy_helper/blob/master/lib/prototype_legacy_helper.rb
E tem que adicionar uma rota no routes.rb. No meu ficou assim:
post ‘users/load_cities’, :to => ‘users#load_cities’
Fabiano
dezembro 31, 2010 at 2:12 pm