Select Dinâmico com Rails

Introdução

Formulários dinâmicos são muito comuns em aplicações web e Rails nos fornece uma maneira muito simples de implementar isto.

Neste artigo, vou te ensinar como implementar um select dinâmico com Rails e Hotwire.

Para isto, vamos criar uma aplicação Rails simples, com cadastro de autores e artigos e coleções.

O usuário poderá adicionar/remover artigos a coleção, filtrando os artigos por autor antes de adicioná-los.

Criando o Projeto

Em seu ambiente de Trabalho execute o comando abaixo para criar um novo projeto Rails:

1
rails new rails-dynamic-select --css=tailwind 

Em seguida, acesse a pasta do projeto com o comando:

1
cd rails-dynamic-select

Criando os Modelos

Os modelos da aplicação serão:

  • Author, contendo apenas o atributo name
  • Article, contendo os atributos title e a referencia para o modelo Author
  • Collection, contendo apenas o atributo title

Para criar os modelos, execute os comandos abaixo:

1
2
3
4
5
rails g scaffold Author name:string --no-jbuilder

rails g scaffold Article title:string author:references --no-jbuilder

rails g scaffold Collection title:string --no-jbuilder

Considere que uma coleção pode conter vários artigos e um artigo pode pertencer a várias coleções, portanto também precisamos criar uma tabela de relacionamento entre as duas tabelas e por fim, realizar as associações entre as classes.

Para criar a tabela de relacionamento, execute o comando abaixo:

1
rails g migration CreateArticlesCollections article:references collection:references

Em app/models/author.rb adicione o código abaixo:

1
2
3
class Author < ApplicationRecord
  has_many :articles
end

Em app/models/collection.rb adicione o código abaixo:

1
2
3
class Collection < ApplicationRecord
  has_and_belongs_to_many :articles
end

Em app/models/article.rb adicione o código abaixo:

1
2
3
4
class Article < ApplicationRecord
  belongs_to :author
  has_and_belongs_to_many :collections
end

Para completar a criação dos modelos, execute o comando abaixo para criar as tabelas no banco de dados:

1
rails db:migrate

Populando a Base de Dados

Como a intenção deste tutorial é demonstrar o select dinâmico, vamos criar alguns registros para popular o banco de dados diretamente no arquivo seed.

Abra o arquivo db/seeds.rb e adicione o código abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
author = Author.create(name: 'Albert Einstein')
Article.create(author: author, title: "Sobre a Teoria da Relatividade Especial")
Article.create(author: author, title: "A Natureza da Luz: Um Experimento de Pensamento")
Article.create(author: author, title: "Efeito Fotoelétrico: Uma Janela para a Física Quântica")
Article.create(author: author, title: "O Significado da E=mc²")
Article.create(author: author, title: "A Teoria da Relatividade Geral e a Curvatura do Espaço-Tempo")

author = Author.create(name: 'Charles Darwin')
Article.create(author: author, title: "A Origem das Espécies por Meio de Seleção Natural")
Article.create(author: author, title: "A Seleção Sexual e a Evolução das Características Secundárias")
Article.create(author: author, title: "A Descendência do Homem e a Seleção em Relação ao Sexo")
Article.create(author: author, title: "A Expressão das Emoções no Homem e nos Animais")
Article.create(author: author, title: "A Viagem do Beagle: Uma Aventura Científica")

author = Author.create(name: 'Marie Curie')
Article.create(author: author, title: "Descoberta dos Elementos Rádio e Polônio")
Article.create(author: author, title: "Radioatividade: Um Novo Fenômeno na Ciência")
Article.create(author: author, title: "Aplicações Médicas da Radioterapia")
Article.create(author: author, title: "A Vida e o Legado de Pierre Curie")
Article.create(author: author, title: "Contribuições para a Compreensão da Radioatividade")

Collection.create(title: 'Minha Coleção')

Em seguida, execute o comando abaixo para popular o banco de dados:

1
rails db:seed

Iniciando a Aplicação

Antes de iniciarmos o servidor, vamos configurar as rotas da aplicação.

Em config/routes.rb defina o root da aplicação para o controller collections e a action index:

1
2
3
4
5
6
7
8
Rails.application.routes.draw do
  root "collections#index"
  
  resources :collections
  resources :articles
  resources :authors

end

Em seguida, inicie o servidor com o comando abaixo:

1
./bin/dev 

Acesse a aplicação em http://localhost:3000 e você verá a tela abaixo:

Criando o Formulário Dinâmico

Agora que já temos a aplicação funcionando, vamos criar o formulário dinâmico.

Nota: É válido lembrar que existem várias formas possível de implementar formulários com Rails, principalmente, formulários com relacionamentos entre tabelas. Tenha em mente que o objetivo deste tutorial é demonstrar como implementar um select dinâmico com Rails e Hotwire.

Em app/views/collections/ crie a partial _articles_form.html.erb e adicione o código abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%= form_with(url: add_article_collection_path(collection), class: "contents") do |form| %>

  <div class="my-5">
    <%= form.label :author %>
    <%= form.collection_select :author, Author.all, :id, :name, {prompt: ''} %>
  </div>

  <div class="my-5">
    <%= form.label :article_ids %>
    <%= form.collection_select :article_ids, Article.none, :id, :title, {prompt: ''}, {data: {collection_target: 'articles'}} %>
  </div>

  <div class="inline">
    <%= form.submit 'Add Article', class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
  </div>
  
<% end %>

Observe que o formulário em questão esta enviando os dados para a action add_article do controller collections.

Neste tutorial, também faremos a remoção dos artigos da coleção.

Portanto, vamos adicionar uma rota para a action add_article, e outra para remove_article do controller collections, que será implementado em seguida.

Em config/routes.rb edite a linha resources :collections conforme o código abaixo:

1
2
3
4
resources :collections do 
  post :add_article, on: :member
  delete :remove_article, on: :member
end

Em seguida, vamos adicionar uma teste para exibir o formulário ou o total de artigos da coleção, dependendo da requisição atual do usuário.

Em app/views/collections/_collection.html.erb adicione o código a seguir, logo após o título da coleção:

1
2
3
4
5
6
7
8
<p class="my-5">
  <strong class="block font-medium mb-1">Articles: </strong>
  <% if action_name =='show' %>
  <%= render partial: 'articles_form', locals: {collection: @collection} %>
  <% else %>
    <%= collection.articles.count %>
  <% end %>
</p>

Desta forma, na ação index será exibido o número total de artigos na coleção, enquanto na ação show, será exibido o formulário para adicionarmos artigos na coleção.

Observe que estamos populando apenas o select dos autores, enquanto o select de artigos esta vazio. Isso ocorre porque o select de artigos é dinâmico e será populado de acordo com o autor selecionado.

Populando o Select de Artigos

Agora que já temos o formulário, vamos implementar a lógica para popular o select de artigos de acordo com o autor selecionado.

Para fazer vamos utilizar um controlador Stimulus que irá escutar o evento change do select de autores e enviar uma requisição para o servidor para obter os artigos do autor selecionado.

Para simplificar nosso código, iremos utilizar a Gem requestjs-rails

Em Gemfile adicione a linha abaixo:

1
gem 'requestjs-rails'

Em seguida, execute o comando abaixo para instalar a Gem:

1
bundle install

Lembre-se de reiniciar o servidor após instalar a gem.

Agora vamos criar o controlador stimulus.

Em app/javascript/controllers/, crie o arquivo collection_controller.js adicione o código abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Controller } from "@hotwired/stimulus"
import { get } from '@rails/request.js'

export default class extends Controller {

  connect() {
    // console.log("Hello from collection_controller!")
  }

  filter_articles(event) {
    let author_id = event.currentTarget.value
    let url = `/articles/filter?author_id=${author_id}`
    get(url, { responseKind: "turbo-stream"})
  }   
}

Observe que nossa url de requisição é /articles/filter/. Portanto, precisamos definir a rota para esta url.

Em config/routes.rb modifique a linha de resources :articles conforme o código abaixo:

1
2
3
resources :articles do 
  get :filter, on: :collection
end 

Agora, vamos implementar a action filter do controller articles.

Em app/controllers/articles_controller.rb adicione o código abaixo:

1
2
3
4
5
6
7
# GET /articles/filter
def filter
  @articles = Article.where(author_id: params[:author_id])
  respond_to do |format|
    format.turbo_stream
  end
end

Observe que nossa requisição espera um responseKind do tipo turbo-stream. Portanto, precisamos criar o template filter.turbo_stream.erb para retornar os dados no formato esperado.

Em app/views/articles/ crie o arquivo filter.turbo_stream.erb e adicione o código abaixo:

1
2
3
<%= turbo_stream.update 'article_ids' do %>
  <%= options_from_collection_for_select @articles, :id, :title %>
<% end %>

Agora, vamos adicionar o controlador collection_controller ao select de autores.

Em app/views/collections/_articles_form.html.erb atualize a instrução de form_with com o data-attribute, controller : collection.

1
<%= form_with(url: add_article_collection_path(collection), class: "contents", data: {controller: 'collection'}) do |form| %>

Para testar o controller esta funcionando corretamente, descomente o código da função connect do controller collection_controller, acesse a página da coleção e inspecione o console do navegador. A mensagem Hello from collection_controller! deve ser exibida.

Agora, no select do autor, vamos adicionar o data-attribute action: change->collection#filter_articles.

1
2
3
4
<div class="my-5">
  <%= form.label :author %>
  <%= form.collection_select :author, Author.all, :id, :name, {prompt: ''}, {data: {action: 'change->collection#filter_articles'}} %>
</div>

Desta forma, quando o usuário alterar o autor, o controlador collection_controller irá enviar uma requisição para o controlador articles_controller que irá popular o select de artigos com os artigos do autor selecionado.

Com o controlador stimulus configurado corretamente, o formulário deve popular os artigos conforme esperado.

Adicionando Artigos na Coleção

Para concluir o formulário, vamos implementar a lógica para adicionar artigos na coleção.

Em app/controllers/collections_controller.rb adicione o código abaixo:

1
2
3
4
5
6
7
8
9
10
11
# POST /collections/1/add_article
def add_article
  @collection.articles << Article.find(params[:article_ids]) unless @collection.articles.include?(Article.find(params[:article_ids]))
  redirect_to collection_url(@collection)
end

# DELETE /collections/1/remove_article
def remove_article
  @collection.articles.delete(Article.find(params[:article_ids])) 
  redirect_to collection_url(@collection)
end

Além deste código, é necessário modificar mais duas linhas.

Na linha 2, precisamos adicionar o before_action :set_collection para que o método set_collection seja executado antes das actions add_article e remove_article.

1
before_action :set_collection, only: %i[ show edit update destroy add_article remove_article]

e no método collection_params precisamos adicionar o atributo article_ids.

1
2
3
def collection_params
  params.require(:collection).permit(:title, :article_ids)
end

Uma observação importante nesta etapa é que devido a implementação feita adicionar apenas um único artigo por vez a coleção, estamos permitindo o parâmetro :article_ids. Em outros cenários onde é possível vincular uma coleção de elementos em uma única vez, é recomendado utilizar uma array de elementos, como article_ids: [ ].

Listando e Removendo Artigos da Coleção

Para concluirmos este tutorial, vamos implementar a listagem e remoção de artigos da coleção.

Novamente, na partial _collection.html.erb, logo após a tag de renderização do formulário, adicione o código abaixo:

1
2
3
4
<% collection.articles.each do |article| %>
  <%= render article %>
  <%= button_to "Remover", remove_article_collection_path(collection, article_ids: article.id), method: :delete%>
<%end%>

Por fim, no Arquivo app/views/articles/_article_.html.erb, altere a linha:

1
<%= article.author_id %>

Por:

1
<%= article.author.name %>

Desta forma, ao acessar a coleção, os artigos serão listados e o usuário poderá adicionar novos registros ou removê-los da coleção.

Conclusão

Neste tutorial, aprendemos como criar um formulário com campos dinâmicos utilizando o framework Ruby on Rails e Hotwire Turbo/Stimulus.

O código fonte deste tutorial está disponível no repositorio do GitHub

Fique a vontade para personalizar o código e implementar novas funcionalidades, assim como customizar os estilos da aplicação.


Repósitório no Github

lucasgeron/rails-dynamic-select


Gostou deste Projeto? Deixe seu Feedback
Compartilhar Dynamic Hitcount Badge
Postado em: 08 de Set, 2023 | Por: Lucas Geron

Leia também