Blog Demo using Rails 7 + Hotwire Rails + TailwindCss + Stimulus + @rails/request.js

nemuba

Alef Ojeda de Oliveira

Posted on October 31, 2021

Blog Demo using Rails 7 + Hotwire Rails + TailwindCss + Stimulus + @rails/request.js

Hi Devs, let's get your hands dirty!

We will develop a demo blog to learn and understand how the new Ruby on Rails frameworks (Hotwire, Stimulus) work, in this project we will be using the beta version of Rails 7.0.2alpha and taking advantage of it to use Tailwindcss which can be installed from the beginning of the project.!

  • setup initial:
ruby: 3.0.2
rails: 7.0.0-alpha2
Enter fullscreen mode Exit fullscreen mode
  • Create new project:
rails new blog --css tailwind
Enter fullscreen mode Exit fullscreen mode
  • Generate scaffold of the blog post:
rails g scaffold post title
Enter fullscreen mode Exit fullscreen mode
  • Install framework rails(ActionText):
rails action_text:install
Enter fullscreen mode Exit fullscreen mode
  • Run migration in database:
rails db:create db:migrate
Enter fullscreen mode Exit fullscreen mode
  • Config model, controller and views to add rich_textarea in posts:
# app/models/post.rb
class Post < ApplicationRecord
  validates :title, presence: true

  has_rich_text :content # add rich_text
end
Enter fullscreen mode Exit fullscreen mode
# app/views/posts/_form.html.erb
<!-- .... -->
<!-- add field :content -->
<div class="my-5">
  <%= form.label :content %>
  <%= form.rich_text_area :content, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
</div>
<!-- .... -->
Enter fullscreen mode Exit fullscreen mode
# app/views/posts/_post.html.erb
<!-- .... -->
<!-- add field :content -->
 <p class="my-5">
   <%= @post.content %>
 </p>
<!-- .... -->
Enter fullscreen mode Exit fullscreen mode
# app/views/posts/show.html.erb
<!-- .... -->
<!-- add field :content -->
 <p class="my-5 inline-block">
   <%= @post.content %>
 </p>
<!-- .... -->
Enter fullscreen mode Exit fullscreen mode
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# ...
 private
   def post_params
     params.require(:post).permit(:title, :content) # add content
   end
end
Enter fullscreen mode Exit fullscreen mode
  • The idea is to create a SPA using Hotwire Rails, so let's configure the blog's index page:
# app/views/posts/index.html.erb

<div class="w-full">
  <div class="flex justify-between items-center">
    <h1 class="text-white text-lg font-bold text-4xl">Posts</h1>
    <%= link_to 'New post', new_post_path,
      class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium",
      data: { 'turbo-frame': 'new_post' }
    %>
  </div>

  <%= turbo_frame_tag :new_post %>

  <div class="min-w-full">
    <%= turbo_frame_tag :posts do %>
      <%= render @posts %>
    <% end %>
  </div>
</div>

Enter fullscreen mode Exit fullscreen mode
  • By clicking on the new post button we will render the new page to register the post:
<!-- link #app/views/posts/index.html.erb -->
<%= link_to 'New post', new_post_path,
      class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium",
      data: { 'turbo-frame': 'new_post' }
%>

<!-- turbo-frame :new_post #app/views/posts/index.html.erb -->
<%= turbo_frame_tag :new_post %>
Enter fullscreen mode Exit fullscreen mode
  • Let's configure the blog's new page:
<!-- app/views/posts/new.html.erb -->
<%= turbo_frame_tag :new_post do %>
  <div class="w-full bg-white p-4 rounded-md mt-4">
    <h1 class="text-lg font-bold text-4xl">New post</h1>

    <%= render "form", post: @post %>

    <%= link_to 'Back to posts', posts_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
  </div>
<% end %>

Enter fullscreen mode Exit fullscreen mode
  • Now we are going to configure the CRUD actions using Turbo Stream, for that we must configure the posts controller, so let's start:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  # ...
  def create
    @post = Post.new(post_params)

    respond_to do |format|
      if @post.save
        format.turbo_stream # add format turbo_stream
        format.html { redirect_to posts_path }
        format.json { render :show, status: :created, location: @post }
      else
        format.turbo_stream # add format turbo_stream
        format.html { render posts_path, status: :unprocessable_entity }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  def update
    respond_to do |format|
      if @post.update(post_params)
        format.turbo_stream # add format turbo_stream
        format.html { redirect_to posts_path, notice: "Post was successfully updated." }
        format.json { render :show, status: :ok, location: @post }
      else
        format.turbo_stream # add format turbo_stream
        format.html { render posts_path, status: :unprocessable_entity }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @post.destroy
    respond_to do |format|
      format.turbo_stream # add format turbo_stream
      format.html { redirect_to posts_url, notice: "Post was successfully destroyed." }
      format.json { head :no_content }
    end
  end
  # ...
end
Enter fullscreen mode Exit fullscreen mode
  • That done, now rails expects that we have new templates or pages for each action that has been added to the format.turbo_stream, so we should create one for each action (create, update, destroy):
<!-- add pages ->
<!-- app/views/posts/create.turbo_stream.erb -->
<!-- app/views/posts/update.turbo_stream.erb -->
<!-- app/views/posts/destroy.turbo_stream.erb -->
Enter fullscreen mode Exit fullscreen mode
  • create.turbo_stream.erb

     <% if @post.errors.present? %>
       <%= notice_stream(message: :error, status: 'red') %>
       <%= form_post_stream(post: @post) %>
    <% else %>
       <%= notice_stream(message: :create, status: 'green') %>
    
       <%= turbo_stream.replace :new_post do %>
          <%= turbo_frame_tag :new_post %>
       <% end %>
    
       <%= turbo_stream.prepend 'posts', partial: 'post', locals: { post: @post } %>
    
     <% end %>
    
    • update.turbo_stream.erb
   <% if @post.errors.present? %>
     <%= notice_stream(message: :error, status: 'red') %>
     <%= form_post_stream(post: @post) %>
   <% else %>
     <%= notice_stream(message: :update, status: 'green') %>
     <%= turbo_stream.replace dom_id(@post), partial: 'post', locals: { post: @post } %>
   <% end %>
Enter fullscreen mode Exit fullscreen mode
  • destroy.turbo_stream.erb
  <%= notice_stream(message: :delete, status: 'green') %>
  <%= turbo_stream.remove @post %>
Enter fullscreen mode Exit fullscreen mode
  • To finish we need to add helpers to add and remove notifications and also render the form when you hear errors, so let's add the helpers:
# app/helpers/posts_helper.rb
module PostsHelper
  NOTICE = {
    create: 'Post created successfully',
    update: 'Post updated successfully',
    delete: 'Post deleted successfully',
    error: 'Something went wrong'
  }.freeze

  def notice_stream(message:, status:)
    turbo_stream.replace 'notice', partial: 'notice', locals: { notice: NOTICE[message], status: status }
  end

  def form_post_stream(post:)
    turbo_stream.replace 'form', partial: 'form', locals: { post: post }
  end
end

Enter fullscreen mode Exit fullscreen mode
  • To add the notification we need to add a turbo frame:
<!-- app/views/layouts/application.html.erb -->
 <%= turbo_frame_tag :notice, class: 'w-full' do %>
 <% end %>

<!-- app/views/posts/_notice.html.erb -->
<p class="animate-pulse opacity-80 w-full py-2 px-3 bg-<%= status %>-50 mb-5 text-<%= status %>-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>

Enter fullscreen mode Exit fullscreen mode
  • After we add the notifications to each action, it would be nice if it disappeared after a while, so I'll create a route to be called just to remove the notification using @rails/request.js, so let's do it:
# config/routes.rb

get '/notice', to: 'posts#clear_message'
Enter fullscreen mode Exit fullscreen mode
  • First let's configure the form, adding a controller stimulus to intercept the request, with this we'll be able to call the route always after submitting a request in the form:
  <%= turbo_frame_tag dom_id post do %>
    <%= form_with(
      model: post, 
      id: 'form',
      class: "contents",
      html: {
        data: { controller: 'notice', action: 'submit->notice#clear' }
      }
    ) do |form| %>

   <!-- fields  --- >

   <% end %>
<% end %>
Enter fullscreen mode Exit fullscreen mode
import { Controller } from "@hotwired/stimulus"
import { FetchRequest } from "@rails/request.js"
// Connects to data-controller="notice"
export default class extends Controller {
  clear(event) {
    event.preventDefault()

    setTimeout(async () => {
      const request = new FetchRequest("get", '/notice', { responseKind: "turbo-stream" })
      await request.perform()
    }, 5000)

    event.target.requestSubmit()
  }
}

Enter fullscreen mode Exit fullscreen mode
  • Now we'll add action in posts controller:
class PostsController < ApplicationController
 # ... actions

 def clear_message
  respond_to do |format|
    format.turbo_stream
  end
 end
end
Enter fullscreen mode Exit fullscreen mode
  • Finally, we'll add the action template, which will always remove the notification:
<!-- app/views/posts/clear_message.turbo_stream.erb -->
<%= turbo_stream.replace 'notice' do %>
  <%= turbo_frame_tag :notice %>
<% end %>

Enter fullscreen mode Exit fullscreen mode

Wow !
We've reached the end, I hope you managed to explain everything, if you have any questions just send me a message here!

source code: Link Github

Show, see you later!

Twitter: AlefOjeda
GitHub: nemubatuba

💖 💪 🙅 🚩
nemuba
Alef Ojeda de Oliveira

Posted on October 31, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related