
Rails Concerns: Aprenda a utilizar facilmente
Você já utilizou Concerns no seu APP Rails? O Ruby On Rails é um framework que preza pela qualidade, simplicidade e reutilização de códigos, utilizar os Concerns é uma boa maneira de aumentar a quantidade de tudo isto nos seus Models.
Neste artigo você vai aprender a dominar o uso de Concerns através de um um exemplo prático passo a passo, além disso vai aprender também como usar o Bootstrap, Devise e etc no seu APP Rails, então vem com a gente \o/
O que são concerns, porque e quando usar?
Os Concerns servem para compartilhar recursos entre seus models (métodos, chamadas de callbacks e etc) de maneira simples (basta incluir uma linha em todos os models que quiser incluir os recursos), então usar os concerns ajuda a deixar seu código mais limpo.
A sugestão é que você use um concern sempre que mais de um model precise de métodos similares, como por exemplo, uma validação especial no texto, enviar um email depois que o record é criado, incluir comentários e etc.
O que vamos criar
Vamos criar um pequeno APP onde as pessoas podem publicar um pensamento (uma frase) e receber observações (comentários) sobre ele, para exercitar o uso de Concerns vamos incluir algumas possíveis reações a esses pensamentos (como no Facebook, só que no nosso caso teremos as seguintes reações: Concordo, Amei e Discordo).
No final do APP você será convidado(a) a realizar um pequeno desafio, onde vai usar o mesmo concern para incluir as reações também nas observações (comentários) recebidos naquele pensamento.
Ferramentas
- * Rails 5.2
- * Ruby 2.5
- * jQuery
- * twitter-bootstrap-rails (gem)
- * devise (gem)
- * devise-bootstrapped (gem)
- * acts_as_commentable (gem)
Obs: Caso você não tenha o Rails instalado, acesse esse tutorial 🙂
Passo a passo da criação do APP
Instalando o Bootstrap e preparando o layout
Primeiro vamos gerar nosso APP Rails, instalar o Bootstrap e deixar os links de login e logout preparados (apenas para dar uma aparência mais interessante ao projeto).
1 – Para começar crie o projeto com a versão 5.2 do Rails rodando no console:
1 |
rails new ShareThoughts |
2 – Usaremos o jQuery e twitter-bootstrap-rails, então adicione ao seu Gemfile:
1 2 |
gem "twitter-bootstrap-rails" gem "jquery-rails" |
3 – Para incluir o bootstrap e o Jquery no javascript, substitua o conteúdo de app/assets/javascripts/applications.js por:
1 2 3 4 5 |
//= require jquery //= require rails-ujs //= require twitter/bootstrap //= require turbolinks //= require_tree . |
4 – Instale as gems rodando:
1 |
bundle install |
5 – Rode o generate do Bootstrap:
1 |
rails generate bootstrap:install static |
6 – Por fim, substitua o código do arquivo app/views/layouts/application.html.erb por:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
<html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <%= csrf_meta_tags %> <%= stylesheet_link_tag "application", :media => "all" %> <%= javascript_include_tag "application" %> <!-- Le HTML5 shim, for IE6-8 support of HTML elements --> <!--[if lt IE 9]> <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.2/html5shiv.min.js" type="text/javascript"></script> <![endif]--> </head> <body> <div class="navbar navbar-default navbar-static-top"> <div class="container"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-responsive-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">ShareThoughts</a> <div class="navbar-collapse collapse navbar-responsive-collapse"> <ul class="nav navbar-nav"> <% if user_signed_in? %> <li> <%= link_to('Logout', destroy_user_session_path, method: :delete) %> </li> <% else %> <li> <%= link_to('Login', new_user_session_path) %> </li> <% end %> </ul> </div> </div> </div> <div class="container"> <div class="row"> <div class="col-lg-12"> <%= bootstrap_flash %> <%= yield %> </div> </div> </div> </body> </html> |
Incluindo a autenticação
Nessa parte iremos incluir o Devise (uma gem que ajuda na autenticação) e criaremos nossas telas de login, signUp e etc já com as classes do Bootstrap (usando o generate: rails generate devise:views:bootstrapped).
1 – Adicione a sua Gemfile:
1 2 |
gem 'devise' gem 'devise-bootstrapped' |
2 – Para instalar a gem e o install do devise, execute:
1 |
bundle install && rails g devise:install |
3 – Gere o model e as views do devise:
1 2 |
rails g devise User rails generate devise:views:bootstrapped |
4 – Crie o Db e rode as migrations:
1 |
rails db:create db:migrate |
5 – Para finalizar esta etapa, adicione o seguinte callback ao arquivo app/controllers/application_controller.rb
1 |
before_action :authenticate_user! |
Incluindo os “Pensamentos” no APP:
Vamos criar o controller e as views principais do APP usando o Scaffold, nessa parte o usuário já vai poder criar, editar e excluir seus “pensamentos”.
1 – Crie o Scaffold de Thought rodando no console:
1 2 3 |
rails g scaffold Thought title body:text user:references --no-stylesheets rails db:migrate rails g bootstrap:themed Thoughts -f |
2 – Adicione a seguinte linha em config/routes.rb para definir o nosso root ‘/’:
1 |
root to: 'thoughts#index' |
3 – Agora vamos ajustar o conteúdo da view app/views/thoughts/index.html.erb, substituía o conteúdo dela por:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<%- model_class = Thought -%> <div class="page-header"> <h1><%=t '.title', :default => model_class.model_name.human.pluralize.titleize %></h1> </div> <table class="table table-striped"> <thead> <tr> <th><%= model_class.human_attribute_name(:title) %></th> <th>author</th> <th><%= model_class.human_attribute_name(:created_at) %></th> <th></th> </tr> </thead> <tbody> <% @thoughts.each do |thought| %> <tr> <td><%= thought.title %></td> <td><%= thought.user.email %></td> <td><%=l thought.created_at %></td> <td> <%= link_to t('.show', :default => t("helpers.links.show")), thought_path(thought), :class => 'btn btn-default btn-xs' %> <% if thought.user == current_user %> <%= link_to t('.edit', :default => t("helpers.links.edit")), edit_thought_path(thought), :class => 'btn btn-default btn-xs' %> <%= link_to t('.destroy', :default => t("helpers.links.destroy")), thought_path(thought), :method => :delete, :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) }, :class => 'btn btn-xs btn-danger' %> <% end %> </td> </tr> <% end %> </tbody> </table> <%= link_to t('.new', :default => t("helpers.links.new")), new_thought_path, :class => 'btn btn-primary' %> |
4 – Substitua o conteúdo da view app/views/thoughts/_form.html.erb por:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<%= form_for @thought, :html => { :class => "form-horizontal thought" } do |f| %> <% if @thought.errors.any? %> <div id="error_expl" class="panel panel-danger"> <div class="panel-heading"> <h3 class="panel-title"><%= pluralize(@thought.errors.count, "error") %> prohibited this thought from being saved:</h3> </div> <div class="panel-body"> <ul> <% @thought.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> </div> <% end %> <div class="form-group"> <%= f.label :title, :class => 'control-label col-lg-2' %> <div class="col-lg-10"> <%= f.text_field :title, :class => 'form-control' %> </div> <%=f.error_span(:title) %> </div> <div class="form-group"> <%= f.label :body, :class => 'control-label col-lg-2' %> <div class="col-lg-10"> <%= f.text_area :body, :class => 'form-control' %> </div> <%=f.error_span(:body) %> </div> <div class="form-group"> <div class="col-lg-offset-2 col-lg-10"> <%= f.submit nil, :class => 'btn btn-primary' %> <%= link_to t('.cancel', :default => t("helpers.links.cancel")), thoughts_path, :class => 'btn btn-default' %> </div> </div> <% end %> |
5 – Ao criar um novo pensamento, precisamos referenciar o usuário da sessão atual como dono então no controller Thoughts (app/controller/thoughts_controller.rb), substitua a action create por:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def create @thought = Thought.new(thought_params) @thought.user = current_user respond_to do |format| if @thought.save format.html { redirect_to @thought, notice: 'Thought was successfully created.' } format.json { render :show, status: :created, location: @thought } else format.html { render :new } format.json { render json: @thought.errors, status: :unprocessable_entity } end end end |
Adicionando os comentários
Nessa parte vamos preparar a base do APP para receber comentários nos pensamentos, para fazer isso vamos usar a gem “acts_as_commentable”.
1 – Podendo criar pensamentos, vamos agora adicionar a funcionalidade de realizar observações, então primeiro adicione ao seu Gemfile:
1 |
gem 'acts_as_commentable' |
2 – Instale a gem e rode o generate:
1 2 |
bundle install rails g comment |
3 – Vá até a migration gerada no passo anterior (db/migrates/…_create_comments.rb) e defina a versão para 4.2 como no exemplo abaixo:
1 |
class CreateComments < ActiveRecord::Migration[4.2] |
4 – Rode as migrations:
1 |
rails db:migrate |
5 – No Model Thought (app/models/thought.rb) adicione a seguinte linha:
1 |
acts_as_commentable |
6 – Gere o controller Observations rodando:
1 |
rails g controller observations |
7 – Coloque no controller gerado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class ObservationsController < ApplicationController before_action :set_thought def create observation = @thought.comments.create( comment: params[:comment], user: current_user ) observation.save redirect_to thought_path(@thought.id) end private def set_thought @thought = Thought.find(params[:thougth_id]) end end |
8 – Precisamos de uma rota para criar uma Observation, em config/routes.rb adicione:
1 |
post '/thought/:thougth_id/observation/', to: 'observations#create', as: 'create_observation' |
9 – Tendo a possibilidade de realizar observações, precisamos recuperá-las quando acessarmos um pensamento, em app/controller/thoughts_controller.rb substitua o método show por:
1 2 3 |
def show @observations = @thought.comments.recent end |
Preparando as “reações”:
1 – Crie o model React rodando no console:
1 2 |
rails g model React reactable:references{polymorphic} user:references reaction:integer rails db:migrate |
2 – No model react adicione:
1 |
enum reaction: %i[agree love disagree] |
Criando nosso Concerns:
Finalmente chegou o momento de criarmos o nosso concern, o objetivo dele é permitir que adicionemos reações a qualquer model simplesmente incluindo ele.
1 – Em app/models/concerns crie um arquivo chamado reactable.rb e adicione o seguinte código
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
module Reactable extend ActiveSupport::Concern included do has_many :reacts, as: :reactable end def react(reaction, current_user) React.create(reactable: self, user: current_user, reaction: reaction) end def react?(current_user) set_react(current_user) @react.present? ? @react : false end def update_react(reaction, current_user) set_react(current_user) @react.update(reaction: reaction) end def remove_reaction(current_user) set_react(current_user) @react.destroy end private def set_react(current_user) @react = React.find_by(reactable: self, user: current_user) end end |
Obs: Em “included” estamos dizendo que todo model que possuir esse concern vai ter o “has_many :reacts, as: :reactable”, além disso também vamos disponibilizar para o model os métodos: react, react?, update_react e remove_reaction.
2 – Inclua a linha ao model Thought:
1 |
include Reactable |
Reactable agora faz parte do nosso model Thought.
Percebendo que a reação não pertencia a alma de nosso model, então a extraímos para um módulo dentro de Concerns.
Além da vantagem de diminuir nosso model, agora podemos reutilizar nosso código em qualquer outro model com apenas uma linha -> include ModuleNameÉ muito fácil não?
3 – Crie um controller chamado Reacts:
1 |
rails g controller Reacts |
4 – Atualize o conteúdo do controller para:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class ReactsController < ApplicationController before_action :set_reactable def react if @reactable.react?(current_user) @reactable.update_react(params[:reaction], current_user) else @reactable.react(params[:reaction], current_user) end end def remove_reaction @reactable.remove_reaction(current_user) end private def set_reactable reactable_type = params[:reactable_type] @reactable = reactable_type.constantize.find(params[:reactable_id]) end end |
5 – Para criar nossas views rode:
1 2 |
touch app/views/reacts/react.js.erb touch app/views/reacts/remove_reaction.js.erb |
6 – Inclua as rotas do controller react em routes.rb:
1 2 |
get 'react/:reactable_type/:reactable_id/:reaction', to: 'reacts#react' get 'react-delete/:reactable_type/:reactable_id', to: 'reacts#remove_reaction' |
7 – No arquivo remove_reaction.js.erb coloque:
1 2 3 4 5 6 7 8 9 10 |
$('#t-concordo, #t-amei, #t-discordo').attr({class:"btn btn-default btn-circle btn-lg"}) $('#t-concordo').attr({ href:"/react/<%= @reactable.class.name %>/<%= @reactable.id %>/agree" }); $('#t-amei').attr({ href:"/react/<%= @reactable.class.name %>/<%= @reactable.id %>/love" }); $('#t-discordo').attr({ href:"/react/<%= @reactable.class.name %>/<%= @reactable.id %>/disagree" }); |
8 – No arquivo app/views/reacts/react.js.erb coloque:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
$('#t-concordo, #t-amei, #t-discordo').attr({class:"btn btn-default btn-circle btn-lg"}) $('#t-concordo').attr({ href:"/react/<%= @reactable.class.name %>/<%= @reactable.id %>/agree" }); $('#t-amei').attr({ href:"/react/<%= @reactable.class.name %>/<%= @reactable.id %>/love" }); $('#t-discordo').attr({ href:"/react/<%= @reactable.class.name %>/<%= @reactable.id %>/disagree" }); <% if @reactable.react?(current_user) %> switch("<%= @reactable.react?(current_user).reaction %>") { case 'agree': $('#t-concordo').attr({ href:"/react-delete/<%= @reactable.class.name %>/<%=@reactable.id %>", class:"btn btn-info btn-circle btn-lg" }); break; case 'love': $('#t-amei').attr({ href:"/react-delete/<%= @reactable.class.name %>/<%=@reactable.id %>", class:"btn btn-danger btn-circle btn-lg" }); break; case 'disagree': $('#t-discordo').attr({ href:"/react-delete/<%= @reactable.class.name %>/<%=@reactable.id %>", class:"btn btn-warning btn-circle btn-lg" }); break; } <% end %> |
9 – Adicione o seguinte trecho a applications.css:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
.row{ align-items: center; display: flex; } .margin-bottom { margin-bottom:30px; } .btn-circle { width: 30px; height: 30px; text-align: center; padding: 6px 0; font-size: 12px; line-height: 1.428571429; border-radius: 15px; } .btn-circle.btn-lg { width: 50px; height: 50px; padding: 12px 0; font-size: 18px; line-height: 1.33; border-radius: 25px; } |
10 – Substitua o código do arquivo app/views/thoughts/show.html.erb por:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
<div class="container"> <div class="page-header"> <div class="row"> <div class="col-lg-8"> <h1><%= @thought.title %></h1> </div> <div class="col-lg-4 text-right"> <%= link_to t('.back', :default => t("helpers.links.back")), thoughts_path, :class => 'btn btn-default' %> <% if @thought.user == current_user %> <%= link_to t('.edit', :default => t("helpers.links.edit")), edit_thought_path(@thought), :class => 'btn btn-default' %> <%= link_to t('.destroy', :default => t("helpers.links.destroy")), thought_path(@thought), :method => 'delete', :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) }, :class => 'btn btn-danger' %> <% end %> </div> </div> </div> <div class="row"> <div class="col-lg-8"> <blockquote> <p><%= @thought.body %></p> <footer>written by <cite title="Source Title"><%= @thought.user.email %></cite></footer> </blockquote> <p> <div id="comment-react"> <%= link_to "/react/#{@thought.class.name}/#{@thought.id}/agree", id:'t-concordo', title:'concordo', class:'btn btn-default btn-circle btn-lg', remote:true do %> <i class="glyphicon glyphicon-thumbs-up"></i> <% end %> <%= link_to "/react/#{@thought.class.name}/#{@thought.id}/love", id:"t-amei", title:"amei", class:"btn btn-default btn-circle btn-lg", remote:true do %> <i class="glyphicon glyphicon-heart"></i> <% end %> <%= link_to "/react/#{@thought.class.name}/#{@thought.id}/disagree", id:'t-discordo', title:"discordo", class:"btn btn-default btn-circle btn-lg", remote:true do %> <i class="glyphicon glyphicon-thumbs-down"></i> <% end %> </div> </p> </div> </div> <hr /> <div class="container"> <%= form_with url: create_observation_path(@thought.id), local: true, class:"row form-inline margin-bottom" do |form| %> <div class="col-md-6"> <%= form.text_area :comment, class:"form-control", style:"min-width:100%;", rows:"3"%> </div> <div class="col-md-2 align-right"> <button type="submit" class="btn btn-info">make an observation</button> </div> <% end %> </div> <h3>Observations(<%= @observations.length %>)</h3> <% if @observations.length > 0 %> <% @observations.each do |observation| %> <div class="row"> <div class='col-md-8'> <div class="panel panel-default"> <div class="panel-body"> <p><strong><%= observation.user.email %>:</strong> <p><%= observation.comment %></p> </div> </div> </div> </div> <% end %> <% end %> </div> <script> <% if @thought.react?(current_user) %> switch("<%= @thought.react?(current_user).reaction %>") { case 'agree': $('#t-concordo').attr({ href:"/react-delete/<%= @thought.class.name %>/<%=@thought.id %>", class:"btn btn-info btn-circle btn-lg" }); break; case 'love': $('#t-amei').attr({ href:"/react-delete/<%= @thought.class.name %>/<%=@thought.id %>", class:"btn btn-danger btn-circle btn-lg" }); break; case 'disagree': $('#t-discordo').attr({ href:"/react-delete/<%= @thought.class.name %>/<%=@thought.id %>", class:"btn btn-warning btn-circle btn-lg" }); break; } <% end %> </script> |
11 – Pronto, agora é só levantar a aplicação e reagir!!!
1 |
rails s |
Resultado do APP
Desafio de fixação
Altere o APP para incluir as reações nas observações (comentários), com isso você vai fixar melhor o uso de concerns e também vai aprender mais sobre as views e o JS no APP.
Depois de fazer o desafio, comenta ai em baixo dizendo o que achou e deixe o link para o projeto no seu Github \o/
Conclusão
Como podemos perceber neste breve projeto, usar os Concerns é uma boa prática de desenvolvimento que reforça o padrão DRY (Não se repita) e o Ruby on Rails é um framework que valoriza muito está prática.
Então realize o desafio proposto, deixa um comentário ai em baixo dizendo se gostou e aproveite esse conhecimento para tornar seus projetos ainda melhores \o/

Não perca nenhum conteúdo
Receba nosso resumo semanal com os novos posts, cursos, talks e vagas o/
Primeira vez no OneBitCode? Curtiu esse conteúdo?
O OneBitCode tem muito mais para você!
O OneBitCode traz conteúdos de qualidade, e em português, sobre programação com foco em Ruby on Rails e também JavaScript.
Além disso, aqui sempre levamos à você conteúdos valiosos sobre a carreira de programação, dicas sobre currículos, portfólios, perfil profissional, soft skills, enfim, tudo o que você precisa saber para continuar evoluindo como Programador(a)!
Fique por dentro de todos os conteúdos o/
Nossas redes sociais:
📹 • https://youtube.com/Onebitcode [Live todas as terças-feiras às 19h)
💻 • https://linkedin.com/company/onebitcode
🙂 • https://facebook.com/onebitcode
📱 • https://instagram.com/one_bit_code
🐦 • https://twitter.com/onebitcode
Nossos cursos:
🥇 • Programador Full Stack Javascript em 8 Semanas
💎 • https://onebitcode.com/dating-violence-in-texas/
⚙ • https://onebitcode.com/guangzhou-hook-up/
🐞 • best trans dating apps
Espero que curta nossos conteúdos e sempre que precisar de ajuda, fala com a gente!
Estamos aqui para você 🙂
Bem-vindo à família OneBitCode o/
Excelente conteúdo, não conhecia e esse tutorial serviu muito, agora irei estudar mais sobre o assunto!
Baooo 🙂
Excelente artigo parabéns viu exercitar um pouco aqui tbm.
\o/
Leonardo Scorza sempre arrasando com os conteúdo de Ruby.
Blocked host: bcb7-177-37-231-41.ngrok.ioTo allow requests to bcb7-177-37-231-41.ngrok.io, add the following to your environment configuration:
No meu aparece esse erro!