Rails 7.0 demo with Hotwire and Tailwind
Tags: rails, javascript, hotwire, tailwindcss
Published: 2022-01-07 07:39:44

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](https://world.hey.com/dhh/modern-web-apps-without-javascript-bundling-or-transpiling-a20f2755). 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](https://hotwired.dev/) - as UI/UX and JavaScript framework (default)
- [Tailwind CSS](https://tailwindcss.com/) - as CSS framework
- [ActionText](https://edgeguides.rubyonrails.org/action_text_overview.html#installation) - as rich text content for text box

### Pre-requisite
Recommended version
```yaml
rvm: 1.29.12
ruby: 3.0.3
rails: 7.0.0
```


### 1) Initial app project
- Create a new app named 'blog'
```ssh
rails new blog --css tailwind
```

- Generate scaffold of the blog post
```ssh
rails g scaffold post title
```

- Install ActionText
```ssh
rails action_text:install
```

- Migrate Rails DB
```ssh
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
```ruby
# app/models/post.rb
class Post < ApplicationRecord
  validates :title, presence: true

  has_rich_text :content
end
```
- View - Posts Templates
```erb
<!-- 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>
<!-- ... -->
```
```erb
<!-- app/views/posts/_post.html.erb -->
<!-- ... -->
<!-- add field :content -->
 <p class="my-5">
   <%= @post.content %>
 </p>
<!-- ... -->
```
```erb
<!-- app/views/posts/show.html.erb -->
<!-- ... -->
<!-- add field :content -->
 <p class="my-5 inline-block">
   <%= @post.content %>
 </p>
<!-- ... -->
```
- Controller - Posts
```ruby
# 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
```erb
<!-- 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
```erb
<!-- 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
```ruby
# 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
```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 %>
```
```erb
<!-- 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 %>
```
```erb
<!-- 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
```ruby
# 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
```erb
<!-- app/views/layouts/application.html.erb -->
 <%= turbo_frame_tag :notice, class: 'w-full' do %>
 <% end %>
```
- Create Notice template in Post
```erb
<!-- 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
```ruby
<!-- app/views/posts/_form.html.erb --->
# config/routes.rb
get '/notice', to: 'posts#clear_message'
```
- Add clear notification in Posts template
```erb
<!-- 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)
```ruby
# 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
```ruby
# 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
```ruby
# config/routes.rb
Rails.application.routes.draw do
  # Set Post index to landing page
  root 'posts#index'
end
```
- Start Rails server for verification
```ssh
rails s
```

![Rails 7 demo SPA screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1dn2im2e89qu4qym8rak.png)

- 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](https://github.com/aquadrehz/demo-blog-hotwire/tree/initial_completed)
- [Original workshop article by Alef Ojeda de Oliveira](https://dev.to/nemuba/blog-demo-using-rails-7-hotwire-rails-tailwindcss-stimulus-railsrequestjs-2c7a)

#### Read more
- [More Rails 7 Feature in detail](https://rubyonrails.org/2021/12/15/Rails-7-fulfilling-a-vision)
- [Rails, Hotwire, CableReady, and StimulusReflex are BFFs](https://dev.to/hopsoft/rails-hotwire-cableready-and-stimulusreflex-are-bffs-4a89)
- [Hotwire with previous Rails](https://blog.cloud66.com/taking-rails-to-the-next-level-with-hotwire/)
- [Rails 7 and Javascript](https://noelrappin.com/blog/2021/09/rails-7-and-javascript/)
- [Official Rails 7 release note](https://edgeguides.rubyonrails.org/7_0_release_notes.html)

#### Resource Attribute
[Blender](https://www.freepik.com/free-photo/man-adds-frozen-berriess-focused-blender-pot-with-before-making-make-tasty-smoothie-drink-refresh-summer-time-unfocused-glass-with-frozen-berries-front-near_11333837.htm#query=blender&position=2&from_view=search)
[Number art](https://www.freepik.com/free-vector/colorful-number-collection-with-flat-design_2303710.htm#query=7&position=16&from_view=search)