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!