DEV Community

Cover image for Rails 7.0 demo with Hotwire and Tailwind
AquaDrehz
AquaDrehz

Posted on

Rails 7.0 demo with Hotwire and Tailwind

A new Rails was released before 2022. This release makes Rails stand out from another framework significantly by getting rid of the most painful issue by replacing NodeJS with Hotwire as a default UI/UX. But still, allow accessing NodeJS with Import map for additional approach.

This article would explain an implementation step-by-step so you can compare it to Rails 6 app more preciously

In this demo, I've forked the original one which uses the important component like the following

Pre-requisite

Recommended version

rvm: 1.29.12
ruby: 3.0.3
rails: 7.0.0
Enter fullscreen mode Exit fullscreen mode

1) Initial app project

  • Create a new app named 'blog'
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 ActionText
rails action_text:install
Enter fullscreen mode Exit fullscreen mode
  • Migrate Rails DB
rails db:create db:migrate
Enter fullscreen mode Exit fullscreen mode

2) Add Rich Text area

Add content: as Rich Text Area from ActionText to Model, View, and Controller
All html.erb files were included classes that will be used by Tailwind CSS

  • Model - Posts
# app/models/post.rb
class Post < ApplicationRecord
  validates :title, presence: true

  has_rich_text :content
end
Enter fullscreen mode Exit fullscreen mode
  • View - Posts Templates
<!-- 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
  • Controller - Posts
# 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

3) Apply Turbo Frame to Posts pages

Clicking New Post will render the new post page into the index page

  • View - Post 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
  • View - Post 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

4) Apply Turbo Stream on the view

  • Add CRUD into Controller
# 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
  • Create Turbo Streme template files
    • app/views/posts/create.turbo_stream.erb
    • app/views/posts/update.turbo_stream.erb
    • app/views/posts/destroy.turbo_stream.erb
<!-- app/views/posts/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 %>
Enter fullscreen mode Exit fullscreen mode
<!-- app/views/posts/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
<!-- app/views/posts/destroy.turbo_stream.erb -->
<%= notice_stream(message: :delete, status: 'green') %>
<%= turbo_stream.remove @post %>
Enter fullscreen mode Exit fullscreen mode

5) Implement Notification - Displaty notice

Implement notice as a helper and allow routing then call controller to display in a view
These steps use Stimulus to handle the Javascript

  • Create helper to be called from
# 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
  • Add Turbo Frame to main application file
<!-- app/views/layouts/application.html.erb -->
 <%= turbo_frame_tag :notice, class: 'w-full' do %>
 <% end %>
Enter fullscreen mode Exit fullscreen mode
  • Create Notice template in Post
<!-- 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

6) Implement Notification - Clear notice

  • Create clear notification route
<!-- app/views/posts/_form.html.erb --->
# config/routes.rb
get '/notice', to: 'posts#clear_message'
Enter fullscreen mode Exit fullscreen mode
  • Add clear notification in Posts template
<!-- app/views/posts/_form.html.erb -->
  <%= 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
  • Trigger clear notification after config interval (5000 ms)
# app/javascript/controllers/notice_controller.js
import { Controller } from "@hotwired/stimulus"
import { FetchRequest } from "@rails/request"

// 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
  • Add Action to Post controller
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
 # ... actions

 def clear_message
  respond_to do |format|
    format.turbo_stream
  end
 end
end
Enter fullscreen mode Exit fullscreen mode

7) Config landing page

  • Redirect landing page to Posts index page
# config/routes.rb
Rails.application.routes.draw do
  # Set Post index to landing page
  root 'posts#index'
end
Enter fullscreen mode Exit fullscreen mode
  • Start Rails server for verification
rails s
Enter fullscreen mode Exit fullscreen mode

Rails 7 demo SPA screenshot

  • This app feature
  • Display all Post on a single page
  • Display Comment on each post when expanding
  • CRUD Post
  • CRUD Comment underneath Post
  • Notification when create, update and delete when successful or failed

Resources

Read more

Resource Attribute

Blender
Number art

Top comments (2)

Collapse
 
bryanbeshore profile image
Bryan Beshore

Do you know how to get the forms plugin to work with rails 7?

github.com/tailwindlabs/tailwindcs...

Looking to leverage tailwindui.com for a new rails app if possible. I do not believe that plugins with rails new app + tailwind gem work.

Collapse
 
lucasayb profile image
Lucas Yamamoto

Amazing post! Really helpful and informative. The result is awesome, while still being simple.