Hotwire

Episode #275 by Teacher's Avatar David Kimura

Summary

Hotwire is the newest magic which takes a different approach to building modern web applications without using much JavaScript.
javascript rails stimulusjs 27:21

Resources

Summary

# Terminal
bundle add hotwire-rails
rails hotwire:install
rails g scaffold tickets title
rails g model comment ticket:references
rails action_text:install

# app/javascript/packs/application.js
import Rails from "@rails/ujs"
// import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"

Rails.start()
// Turbolinks.start()
ActiveStorage.start()

require("trix")
require("@rails/actiontext")

# layouts/application.html.erb
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= yield :head %>
    <%= turbo_include_tags %>
    # if using StimulusJS in Webpacker
    <%# stimulus_include_tags %>

# models/ticket.rb
class Ticket < ApplicationRecord
  has_rich_text :content
  has_many :comments, dependent: :destroy

  broadcasts
  # after_create_commit -> { broadcast_append_to self }
  # after_destroy_commit -> { broadcast_remove_to self }
  # after_update_commit -> { broadcast_replace_to self }
end

# models/comment.rb
class Comment < ApplicationRecord
  belongs_to :ticket
  has_rich_text :content
  broadcasts_to :ticket
  # after_create_commit -> { broadcast_append_to ticket }
  # after_destroy_commit -> { broadcast_remove_to ticket }
  # after_update_commit -> { broadcast_replace_to ticket }
end

# tickets_controller.rb
  def ticket_params
    params.require(:ticket).permit(:title, :content)
  end

# views/tickets/_form.html.erb
  <div class="field">
    <%= form.label :content %>
    <%= form.rich_text_area :content %>
  </div>

# views/tickets/show.html.erb
<%= turbo_stream_from @ticket %>

<%= turbo_frame_tag 'ticket' do %>
  <%= render @ticket %>
  <%= link_to 'Edit', edit_ticket_path(@ticket) %> |
  <%= link_to 'Back', tickets_path, 'data-turbo-frame': :_top %>
<% end %>

<div id='comments'>
  <h2>Comments</h2>
  <%= render @ticket.comments %>
</div>

<%# link_to 'New Comment', new_ticket_comment_path(@ticket) %>
<%= turbo_frame_tag 'new_comment', src: new_ticket_comment_path(@ticket), target: :_top %>

# views/comments/_comment.html.erb
<%= content_tag :div, id: dom_id(comment) do %>
  <em>Someone said <%= time_ago_in_words(comment.created_at) %> ago</em>
  <%= comment.content %>
  <hr>
<% end %>

# views/comments/new.html.erb
<%= turbo_frame_tag 'new_comment' do %>
  <h2>New Comment</h2>
  <%= form_with model: [@ticket, @comment],
        data: { controller: 'reset_form', action: 'turbo:submit-end->reset_form#reset' } do |form| %>
      <div class='field'>
        <%= form.rich_text_area :content %>
        <%= form.submit 'Post', 'data-reset_form-target': 'button' %>
      </div>
  <% end %>
<% end %>

# comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_ticket

  def new
    @comment = @ticket.comments.new
  end

  def create
    @comment = @ticket.comments.create!(comment_params)
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to @ticket }
    end
  end

  private

  def set_ticket
    @ticket = Ticket.find(params[:ticket_id])
  end

  def comment_params
    params.require(:comment).permit(:content)
  end

end

# config/routes.rb
Rails.application.routes.draw do
  resources :tickets do
    resources :comments, only: [:new, :create]
  end
  root to: 'tickets#index'
end

# views/tickets/_ticket.html.erb
<%= content_tag :div, id: dom_id(ticket) do %>
  <h1><%= ticket.title %></h1>
  <p><%= ticket.content %></p>
<% end %>

# views/comments/create.turbo_stream.erb
<%# turbo_stream.append 'comments', @comment %>

# assets/javascripts/controllers/reset_form_controller.js
import { Controller} from "stimulus"

export default class extends Controller {
  static targets = ["button"]

  reset() {
    this.element.reset()
    this.buttonTarget.disabled = false
  }
}