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
- Hotwire - as UI/UX and JavaScript framework (default)
- Tailwind CSS - as CSS framework
- ActionText - as rich text content for text box
Pre-requisite
Recommended version
rvm: 1.29.12
ruby: 3.0.3
rails: 7.0.0
1) Initial app project
- Create a new app named 'blog'
rails new blog --css tailwind
- Generate scaffold of the blog post
rails g scaffold post title
- Install ActionText
rails action_text:install
- Migrate Rails DB
rails db:create db:migrate
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
- 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>
<!-- ... -->
<!-- app/views/posts/_post.html.erb -->
<!-- ... -->
<!-- add field :content -->
<p class="my-5">
<%= @post.content %>
</p>
<!-- ... -->
<!-- app/views/posts/show.html.erb -->
<!-- ... -->
<!-- add field :content -->
<p class="my-5 inline-block">
<%= @post.content %>
</p>
<!-- ... -->
- Controller - Posts
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# ...
private
def post_params
params.require(:post).permit(:title, :content) # add content
end
end
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>
- 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 %>
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
- 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 %>
<!-- 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 %>
<!-- app/views/posts/destroy.turbo_stream.erb -->
<%= notice_stream(message: :delete, status: 'green') %>
<%= turbo_stream.remove @post %>
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
- Add Turbo Frame to main application file
<!-- app/views/layouts/application.html.erb -->
<%= turbo_frame_tag :notice, class: 'w-full' do %>
<% end %>
- 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>
6) Implement Notification - Clear notice
- Create clear notification route
<!-- app/views/posts/_form.html.erb --->
# config/routes.rb
get '/notice', to: 'posts#clear_message'
- 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 %>
- 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()
}
}
- 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
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
- Start Rails server for verification
rails s
- 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
- Finished Codebase repo - demo-blog-hotwire:initial_completed
- Original workshop article by Alef Ojeda de Oliveira
Read more
- More Rails 7 Feature in detail
- Rails, Hotwire, CableReady, and StimulusReflex are BFFs
- Hotwire with previous Rails
- Rails 7 and Javascript
- Official Rails 7 release note
Latest comments (2)
Amazing post! Really helpful and informative. The result is awesome, while still being simple.
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.