
Neste tutorial você vai aprender como construir aplicações web modernas sem usar muito JavaScript enviando HTML em vez de JSON pela rede (over the wire).
Isso torna as primeiras páginas mais rápidas ao carregar, mantendo a renderização no servidor (server-side) e permitindo uma experiência de desenvolvimento mais simples e produtiva como o Rails sempre trouxe em suas tecnologias, sem sacrificar a velocidade ou a capacidade de resposta associadas a uma aplicação tradicional de página única (Single Page Application ou SPA).
Exemplo inspirado em um Screecast do Go Rails 🤘
-
Ruby
-
Ruby On Rails
-
Redis
-
SQLite
-
Hotwire para Rails
-
TurboRails
-
StimulusJS
-
WebSocket
-
ActionCable
O que é o Hotwire?
O coração do Hotwire é a gem Turbo. Um conjunto de técnicas complementares, para acelerar a navegação das páginas e envios de formulários, dividindo páginas complexas em componentes e transmitindo atualizações parciais das páginas através do WebSocket (o qual consiste em ActionCable, Channels e Streaming Data).
Tudo isso com a possibilidade de não escrever nenhum JavaScript, apenas se for necessário escrever algum código customizado e para esse caso usaremos o Stimulus que torna isso muito fácil com uma abordagem centrada em HTML para estados e navegação entre a rede (wire) e que foi projetado desde o início para se integrar perfeitamente com aplicativos híbridos nativos para iOS e Android.
O que vamos criar?
Uma aplicação web responsiva para se adequar as telas de computador e celular aonde poderemos criar, visualizar, atualizar, dar like e retweetar o tweet se igualando quase a uma SPA sem sacrificar velocidade e capacidade de armazenamento das respostas usando Rails com Hotwire.
Então bora codar!
1 – Primeiramente vamos criar nosso projeto Rails, para isso, rode no terminal (isso pode demorar um pouco, é uma boa hora para um café):
1 |
rails new tweets_app |
2 – Para adicionar e instalar o Hotwire no nosso projeto, rode no terminal:
1 |
bundle add hotwire-rails |
3 – Vamos instalar as configurações iniciais, rode no terminal:
1 |
rails hotwire:install |
4 – É um ótimo momento para nós analisarmos com calma quais foram essas configurações iniciais que o hotwire fez, visitando o primeiro arquivo então app/javascripts/packs/application.js
, ele deve estar parecido com esse:
1 2 3 4 5 6 7 8 9 |
import Rails from "@rails/ujs" import "@hotwired/turbo-rails" import * as ActiveStorage from "@rails/activestorage" import "channels" Rails.start() ActiveStorage.start() import "controllers" |
-
Podemos observar que não importamos mais o
Turbolinks
e agora importamos oturbo-rails
, pois o turbo rails substitui o turbolinks e no final da linha vemos que importamos oscontrollers
e esse ultimo está relacionado com os arquivos doStimulus
o qual podemos checar se esta instalado nopackagase.json
5 – Se formos checar como ficou o Gemfile
vamos ver ele mais ou menos dessa forma:
1 2 3 4 |
... # Use Redis adapter to run Action Cable in production gem 'redis', '~> 4.0' ... |
6 – Podemos observar que a gem redis
foi adicionada no projeto isso porque o ActionCable
precisa dele para guardar alguns dados temporários durante a navegação do WebSocket
. Porém só instalar o redis não é suficiente para usar ele, precisamos checar se ele esta configurado corretamente, vá no arquivo config/cable.yml
ele deve se parecer dessa forma:
1 2 3 4 |
development: adapter: redis url: redis://localhost:6379/1 ... |
7 – Agora sim, temos certeza de que no nosso projeto a configuração do hotwire está OK. Vamos gerar nossas views
, controllers
, models
e migrations
para a tabela tweets
com as colunas body
e likes
, para isso rode no terminal:
1 |
rails g scaffold tweets body:text likes:integer |
8 – Agora que temos tudo gerado precisamos mandar essas alterações para o banco de dados, para isso rode no terminal:
1 |
rails db:create db:migrate |
9 – Se tudo der certo podemos rodar o servidor e checar se está tudo OK, para isso vamos rodar o servidor e depois visitar a página de tweets que será http://localhost:3000/tweets, então rode no terminal:
1 |
rails s |
10 – Agora vamos colocar os tweets e o formulário juntos para ter tudo na mesma página, para isso edite o arquivo app/views/tweets/index.html.erb
1 2 3 4 5 6 7 8 9 |
<%= turbo_stream_from :tweets %> <%= turbo_frame_tag :tweet_form do %> <%= render 'tweets/form', tweet: @tweet %> <% end %> <%= turbo_frame_tag :tweets do %> <%= render @tweets %> <% end %> |
11 – Edite o formulário para que não apareça a opção adicionar likes, pois vamos implementar os likes quando os tweets já estiverem criados, então vamos deixar o arquivo app/views/tweets/_form.html.erb
dessa forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<%= form_with(model: tweet, id: dom_id(tweet)) do |form| %> <% if tweet.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(tweet.errors.count, "error") %> prohibited this tweet from being saved:</h2> <ul> <% tweet.errors.each do |error| %> <li><%= error.full_message %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= form.label :body %> <%= form.text_area :body %> </div> <div class="actions"> <%= form.submit %> </div> <% end %> |
12 – Agora vamos listar os tweets, para isso vamos criar o arquivo app/views/tweets/_tweet.html.erb
e adicionar nele o seguinte código:
1 2 3 4 5 6 7 |
<div style="background: lightgrey; width: 300px; padding: 10px;"> <%= tweet.body %> <br> <%= link_to :edit, edit_tweet_path(tweet) %> <%= button_to "likes (#{tweet.likes || 0})", tweet_path(tweet, like: true), method: :put %> </div> <br> |
13 – Precisamos validar o campo body (que não pode ser nulo) e vamos dizer para o broadcast para após criar o tweet exibir ele na tela como o primeiro tweet, para isso vamos editar o arquivo app/models/tweet.rb
e colocar o seguinte código:
1 2 3 4 5 |
class Tweet < ApplicationRecord validates_presence_of :body after_create_commit { broadcast_prepend_to :tweets } end |
14 – Agora para carregar os tweets na ordem correta, e para quando criarmos o tweet apareça o novo tweet ou alguma mensagem de erro no formulário para indicar que algo deu errado, vamos editar o arquivo app/controllers/tweets_controller.rb
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 |
class TweetsController < ApplicationController def index @tweets = Tweet.all.order(created_at: :desc) @tweet = Tweet.new end def create @tweet = Tweet.new(tweet_params) respond_to do |format| if @tweet.save format.html do redirect_to tweets_path end format.json { render :show, status: :created, location: @tweet } else format.turbo_stream do render turbo_stream: turbo_stream.replace( @tweet, partial: 'tweets/form', locals: { tweet: @tweet } ) end format.html { render :new, status: :unprocessable_entity } format.json { render json: @tweet.errors, status: :unprocessable_entity } end end end private def set_tweet @tweet = Tweet.find(params[:id]) end end |
15 – Agora já podemos testar o formulário, e mais legal ainda, podemos também fazer o teste pelo terminal que a lista de tweets deve atualizar em tempo real:
-
Agora analisando o fluxo das requisições pelo terminal podemos observar ele transmitindo os dados para os métodos por
TURBO_STREAM
e é isso que nos permite renderizar o formatoturbo_stream
, redirecionar e renderizar outra página viahtml
sem precisar atualizar.Então, usando esse método ele manda o HTML atual dos tweets já criados para o método
create
nesse nosso exemplo, depois que ele inserir com sucesso o tweet ele monta o HTML do novo tweet peloActionCable
, com isso ele redireciona o backend para a página de tweets mas não retorna HTML e sim um status http 302 (found), dessa forma a página não atualiza, e para finalizar ele transmite para a página de tweets os dados do novo tweet e coloca ele aonde for indicado, se aaction
for prepend como é nosso caso ele vai colocar antes dos tweets, se fosse append por exemplo ele colocaria no final dos tweets.
16 – Dando continuidade, vamos agora codificar a parte de editar o twitter, dar like e excluir certo? Para isso vamos configurar o broadcast
para os casos de update
e destroy
deixando o arquivo app/models/tweet.rb
dessa forma:
1 2 3 4 5 6 7 |
class Tweet < ApplicationRecord after_create_commit { broadcast_prepend_to :tweets } after_update_commit { broadcast_replace_to :tweets } after_destroy_commit { broadcast_remove_to :tweets } validates_presence_of :body end |
17 – Agora vamos criar um link para editar e quando clicar a ideia é remover o tweet para colocar um formulário no mesmo lugar com os dados do tweet, para fazer isso primeiramente vamos identificar no arquivo app/views/tweets/edit.html.erb
o id do frame que ele vai precisar substituir, renderizar o formulário e ainda mostrar um link de cancelar, deixando ele dessa forma:
1 2 3 4 |
<%= turbo_frame_tag dom_id(@tweet) do %> <%= render 'form', tweet: @tweet %> <%= link_to :cancel, tweet_path(@tweet) %> <% end %> |
18 – Agora ele já sabe que deve substituir o mesmo frame que estiver com o id do tweet, mas nós não identificamos os ids dos tweets na nossa lista de tweets, então para identificar basta deixar o arquivo app/views/tweets/tweet.html.erb
dessa forma:
1 2 3 4 5 6 7 8 9 |
<%= turbo_frame_tag dom_id(tweet) do %> <div style="background: lightgrey; width: 300px; padding: 10px;"> <%= tweet.body %> <br> <%= link_to :edit, edit_tweet_path(tweet) %> <%= button_to "likes (#{tweet.likes || 0})", tweet_path(tweet, like: true), method: :put %> </div> <br> <% end %> |
19 – Agora vamos arrumar os requests e o que eles devem fazer lá no arquivo app/controllers/tweets_controller.rb
, deixaremos ele dessa forma:
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 |
class TweetsController < ApplicationController before_action :set_tweet, only: %i[show edit update destroy] def index @tweets = Tweet.all.order(created_at: :desc) @tweet = Tweet.new end def create @tweet = Tweet.new(tweet_params) respond_to do |format| if @tweet.save format.html do redirect_to tweets_path end format.json { render :show, status: :created, location: @tweet } else format.turbo_stream do render turbo_stream: turbo_stream.replace( @tweet, partial: 'tweets/form', locals: { tweet: @tweet } ) end format.html { render :new, status: :unprocessable_entity } format.json { render json: @tweet.errors, status: :unprocessable_entity } end end end def edit; end def update if params[:like] == 'true' @tweet.increment!(:likes) respond_to do |format| format.html { redirect_to @tweet } format.json { render :show, status: :ok, location: @tweet } end else respond_to do |format| if @tweet.update(tweet_params) format.html { redirect_to @tweet } format.json { render :show, status: :ok, location: @tweet } else format.turbo_stream do render turbo_stream: turbo_stream.replace( @tweet, partial: 'tweets/form', locals: { tweet: @tweet } ) end format.html { render :edit, status: :unprocessable_entity } format.json { render json: @tweet.errors, status: :unprocessable_entity } end end end end def show; end def destroy @tweet.destroy respond_to do |format| format.html { redirect_to tweets_url } format.json { head :no_content } end end private def set_tweet @tweet = Tweet.find(params[:id]) end def tweet_params params.require(:tweet).permit(:body, :likes) end end |
20 – Agora quando clicar em cancelar e quando terminar de atualizar o tweet, queremos que suma o formulário e apareça o tweet de volta atualizado ou não, para isso precisamos deixar o arquivo app/views/tweets/show.html.erb
dessa forma:
1 2 3 4 5 6 7 8 9 |
<%= turbo_frame_tag dom_id(@tweet) do %> <div style="background: lightgrey; width: 300px; padding: 10px;"> <%= @tweet.body %> <br> <%= link_to :edit, edit_tweet_path(@tweet), method: :put %> <%= button_to "likes (#{@tweet.likes || 0})", tweet_path(@tweet, like: true), method: :put %> </div> <br> <% end %> |
21 – Nossa aplicação já está quase pronta, mas ainda falta uma coisinha, deletar os tweets, para fazer isso é mais fácil do que parece, basta adicionar os links de deletar nos arquivos app/views/tweets/_tweet.html.erb
e app/views/tweets/show.html.erb
, e eles vão ficar dessa forma respectivamente:
1 2 3 4 5 6 7 8 9 10 |
<%= turbo_frame_tag dom_id(tweet) do %> <div style="background: lightgrey; width: 300px; padding: 10px;"> <%= tweet.body %> <br> <%= link_to :edit, edit_tweet_path(tweet) %> <%= button_to "likes (#{tweet.likes || 0})", tweet_path(tweet, like: true), method: :put %> <%= button_to :delete, tweet_path(tweet), method: :delete %> </div> <br> <% end %> |
1 2 3 4 5 6 7 8 9 10 |
<%= turbo_frame_tag dom_id(@tweet) do %> <div style="background: lightgrey; width: 300px; padding: 10px;"> <%= @tweet.body %> <br> <%= link_to :edit, edit_tweet_path(@tweet), method: :put %> <%= button_to "likes (#{@tweet.likes || 0})", tweet_path(@tweet, like: true), method: :put %> <%= button_to :delete, tweet_path(@tweet), method: :delete %> </div> <br> <% end %> |
Conclusão
Através desse artigo foi possível compreender como funciona o “framework mágico” , através das requisições via turbo stream agregado com manipulação e navegação via WebSocket.
Nele você ainda teve a possibilidade de desenvolver na prática uma pequena aplicação simples, mas que já lhe permite aplicar esse conhecimento de maneira aprofundada em muitos outros projetos legais e acredite até mesmo projetos grandes com o conhecimento que você adquiriu aqui tenho certeza que será capaz de lidar com eles sem problemas.
Caso haja alguma dúvida, ou até mesmo alguma parte que não saiu como esperado, você pode acessar o , ou utilize os comentários para que nós possamos nos comunicar.
Aviso Importante (Criando um E-commerce profissional com Rails)
E ai Programador(a), tudo bem?
Leonardo Scorza por aqui 🙂
Em breve nós vamos abrir uma nova turma do Novo Bootcamp, nele nós ensinamos como criar um E-commece Profissional usando Ruby On Rails, React e React Native.
Vou deixar um link a baixo para você se inscrever para ser avisado assim que uma nova turma abrir, basta deixar seu email 🤘
-> Me avise sobre a próxima turma: casual dating rochester ny
Ótimo conteúdo! Super útil 🤘
Muito obrigado pelo comentário chefe! ficamos muito felizes em saber que estamos influenciando positivamente seu conhecimento!
Opa que Ide é essa que vc ta usando ?
Não uso IDE, eu uso VIM, mas não mostrei ele em nenhum print se quiser posso ensinar em um post futuro como configurar o VIM e o terminal de uma maneira muito produtiva, se interessa?
Muito bom, quanto menos js melhor hahahha
ok, entendi bem o conceito, porém o exemplo aqui tá dando alguns bugs…
outro detalhe, sobre o blog OneBitCode…quando estou logado, não aparece o botão para anexar imagem, quando não está logado, aparece…rs
Destroy, resolvido…uma identação errada tweets_controller
nice que ótimo que entendeu o conceito esse era com certeza meu maior objetivo meu chefe, sobre o erro que vc teve parabéns por descobrir e ao mesmo tempo solucionar! desculpa ter deixado passar esse erro de indentação…
Como seria pra atualizar os likes conforme for aumentando?
atualizando aquele botão para fazer o acréscimo via stream replace no controller