43
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
Recommended version
rvm: 1.29.12
ruby: 3.0.3
rails: 7.0.0
rails new blog --css tailwind
rails g scaffold post title
rails action_text:install
rails db:create db:migrate
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
All html.erb files were included classes that will be used by Tailwind CSS
# app/models/post.rb
class Post < ApplicationRecord
validates :title, presence: true
has_rich_text :content
end
<!-- 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>
<!-- ... -->
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# ...
private
def post_params
params.require(:post).permit(:title, :content) # add content
end
end
Clicking New Post will render the new post page into the 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>
<!-- 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 %>
# 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
- 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 %>
Implement notice as a helper and allow routing then call controller to display in a view
These steps use Stimulus to handle the Javascript
These steps use Stimulus to handle the Javascript
# 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
<!-- 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>
<!-- app/views/posts/_form.html.erb --->
# config/routes.rb
get '/notice', to: 'posts#clear_message'
<!-- 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 %>
# 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()
}
}
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# ... actions
def clear_message
respond_to do |format|
format.turbo_stream
end
end
end
# config/routes.rb
Rails.application.routes.draw do
# Set Post index to landing page
root 'posts#index'
end
rails s

43