DEV Community

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

Posted on

7 1

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' ```ssh

rails new blog --css tailwind


- Generate scaffold of the blog post
```ssh


rails g scaffold post title


Enter fullscreen mode Exit fullscreen mode
  • Install ActionText ```ssh

rails action_text:install


- Migrate Rails DB
```ssh


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 ```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>
<!-- ... -->


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 ```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>


Enter fullscreen mode Exit fullscreen mode
  • View - Post new page ```erb

<%= turbo_frame_tag :new_post do %>


New post

<%= 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" %>

<% 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


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 ```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 %>


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 ```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 %>


Enter fullscreen mode Exit fullscreen mode
  • Create Notice template in Post ```erb

<%= notice %>



### 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'


Enter fullscreen mode Exit fullscreen mode
  • Add clear notification in Posts template ```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()
  }
}


Enter fullscreen mode Exit fullscreen mode
  • 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


Enter fullscreen mode Exit fullscreen mode
  • 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)
Enter fullscreen mode Exit fullscreen mode

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

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.

SurveyJS custom survey software

JavaScript Form Builder UI Component

Generate dynamic JSON-driven forms directly in your JavaScript app (Angular, React, Vue.js, jQuery) with a fully customizable drag-and-drop form builder. Easily integrate with any backend system and retain full ownership over your data, with no user or form submission limits.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay