📖 Ruby on Rails Toàn tập - Views, Helpers và Hotwire
75 phút

Views, Helpers và Hotwire

ERB Templates

Layout

<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for(:title) || "My App" %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>
  <body>
    <nav>
      <%= link_to "Home", root_path %>
      <%= link_to "Posts", posts_path %>
      <% if current_user %>
        <%= link_to "Logout", logout_path, method: :delete %>
      <% end %>
    </nav>

    <% flash.each do |type, msg| %>
      <div class="flash flash-<%= type %>"><%= msg %></div>
    <% end %>

    <main>
      <%= yield %>
    </main>
  </body>
</html>

Partials

<!-- app/views/posts/_post.html.erb -->
<div class="post" id="<%= dom_id(post) %>">
  <h2><%= link_to post.title, post %></h2>
  <p><%= truncate(post.body, length: 200) %></p>
  <p>By <%= post.user.display_name %></p>
  <p>
    <%= link_to "Edit", edit_post_path(post) %> |
    <%= link_to "Delete", post, data: { turbo_method: :delete, turbo_confirm: "Sure?" } %>
  </p>
</div>

<!-- app/views/posts/index.html.erb -->
<h1>Posts</h1>
<div id="posts">
  <%= render @posts %>
</div>

<!-- Collection rendering -->
<%= render partial: "post", collection: @posts %>

View Helpers

Common helpers

<%= link_to "Edit", edit_post_path(@post) %>
<%= link_to "Delete", @post, method: :delete, data: { confirm: "Sure?" } %>

<%= image_tag "logo.png", alt: "Logo", class: "logo" %>
<%= image_tag @user.avatar.variant(resize_to_limit: [100, 100]) %>

<%= form_with(model: @post) do |form| %>
  <% if form.object.errors.any? %>
    <div class="errors">
      <h3><%= pluralize(form.object.errors.count, "error") %> prohibited saving:</h3>
      <ul>
        <% form.object.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div>
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :body %>
    <%= form.text_area :body %>
  </div>

  <div>
    <%= form.label :published %>
    <%= form.check_box :published %>
  </div>

  <%= form.submit %>
<% end %>

<%= truncate(post.body, length: 100, separator: ' ') %>
<%= pluralize(@posts.count, "post") %>
<%= number_to_currency(1000) %>
<%= time_ago_in_words(post.created_at) %>

Custom Helpers

# app/helpers/posts_helper.rb
module PostsHelper
  def published_badge(post)
    if post.published?
      content_tag(:span, "Published", class: "badge badge-success")
    else
      content_tag(:span, "Draft", class: "badge badge-warning")
    end
  end

  def post_excerpt(post, length: 200)
    truncate(strip_tags(post.body), length: length)
  end

  def author_avatar(user, size: 40)
    if user.avatar.attached?
      image_tag user.avatar.variant(resize_to_fill: [size, size]), class: "avatar"
    else
      content_tag(:div, user.name[0].upcase, class: "avatar avatar-initial")
    end
  end
end

Hotwire - Turbo

Turbo Frames

<!-- app/views/posts/index.html.erb -->
<h1>Posts</h1>

<!-- Search form inside frame -->
<%= turbo_frame_tag "posts_search" do %>
  <%= form_with url: posts_path, method: :get, data: { turbo_frame: "posts_search" } do |f| %>
    <%= f.search_field :q, value: params[:q], placeholder: "Search..." %>
  <% end %>

  <div id="posts">
    <%= render @posts %>
  </div>
<% end %>

Turbo Streams

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def create
    @post = Post.new(post_params)

    if @post.save
      respond_to do |format|
        format.turbo_stream
        format.html { redirect_to @post, notice: "Post created" }
      end
    else
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    @post = Post.find(params[:id])
    @post.destroy

    respond_to do |format|
      format.turbo_stream { render turbo_stream: turbo_stream.remove(@post) }
      format.html { redirect_to posts_url }
    end
  end
end
<!-- app/views/posts/create.turbo_stream.erb -->
<%= turbo_stream.prepend "posts", @post %>
<%= turbo_stream.update "new_post_form", "" %>
<%= turbo_stream.replace "flash", partial: "shared/flash" %>

Turbo Broadcasts

class Post < ApplicationRecord
  after_create_commit -> { broadcast_prepend_to "posts", target: "posts" }
  after_update_commit -> { broadcast_replace_to "posts" }
  after_destroy_commit -> { broadcast_remove_to "posts" }
end

Stimulus Controllers

// app/javascript/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["menu"]
  static values = { open: Boolean }

  toggle() {
    this.menuTarget.classList.toggle("hidden")
    this.openValue = !this.openValue
  }

  hide(event) {
    if (!this.element.contains(event.target)) {
      this.menuTarget.classList.add("hidden")
      this.openValue = false
    }
  }
}
<div data-controller="dropdown" data-action="click@window->dropdown#hide">
  <button data-action="click->dropdown#toggle">Menu</button>
  <div data-dropdown-target="menu" class="hidden">
    <a href="#">Option 1</a>
    <a href="#">Option 2</a>
  </div>
</div>

Forms với FormBuilder

# app/views/posts/_form.html.erb
<%= form_with(model: post, class: "post-form") do |form| %>
  <%= render "shared/errors", object: post %>

  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title, class: "form-control" %>
  </div>

  <div class="field">
    <%= form.label :body %>
    <%= form.text_area :body, rows: 10, class: "form-control" %>
  </div>

  <div class="field">
    <%= form.label :category_id %>
    <%= form.collection_select :category_id, Category.all, :id, :name,
                                { prompt: "Select category" },
                                { class: "form-select" } %>
  </div>

  <div class="actions">
    <%= form.submit class: "btn btn-primary" %>
  </div>
<% end %>

Bài tập thực hành

Hãy tạo views với Turbo Frames và Stimulus!

📝 Bài tập (1)

  1. Tạo UI tương tác với Turbo và Stimulus