Hotwire Introduction

Episode #369 by Teacher's Avatar David Kimura

Summary

Hotwire is an alternative approach to building modern web applications without using much JavaScript by sending HTML instead of JSON over the wire. With Rails 7, we get Hotwire added in by default. In this episode, we look at some of the features with Hotwire and how to use them.
rails hotwire javascript turbo stimulusjs 23:56

Resources

Hotwire - https://hotwired.dev/
Episode Source Code - https://github.com/driftingruby/369-hotwire-introduction
This episode is sponsored by Honeybadger

Download Source Code

Summary

# Terminal
bin/rails action_text:install
bin/rails g model post title
bin/rails g stimulus search
bin/rails g stimulus search-active

# models/post.rb
class Post < ApplicationRecord
  has_rich_text :content
end

# welcome_controller.rb
class WelcomeController < ApplicationController
  def index
    @posts = params[:query] ? Post.where("title like ?", "%#{params[:query]}%") : []
    respond_to do |format|
      format.html {}
      format.turbo_stream {
        render turbo_stream: turbo_stream.replace("results", partial: "welcome/results")
      }
    end
  end

  def show
    @post = Post.find(params[:id])
    render turbo_stream: turbo_stream.replace("show_content", partial: "welcome/show")
  end
end

# javascript/controllers/search_controller.js
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="search"
export default class extends Controller {
  connect() {
    this.element.setAttribute("data-action", "keyup->search#search")
  }

  search() {
    let params = new URLSearchParams()
    params.append("query", this.element.value)

    fetch(`/?${params}`, {
      method: "GET",
      headers: {
        Accept: "text/vnd.turbo-stream.html"
      }
    })
      .then(r => r.text())
      .then(html => Turbo.renderStreamMessage(html))
  }
}

# javascript/controllers/search_active_controller.js
import { Controller } from "@hotwired/stimulus"
import { List } from "immutable"

// Connects to data-controller="search-active"
export default class extends Controller {
  connect() {
    this.element.setAttribute("data-action", "click->search-active#clicked")
  }

  clicked() {
    let links = document.querySelectorAll("#results a.active")
    Array.from(links).forEach(link => {
      link.classList.remove("active")
    })

    this.element.classList.add("active")
  }
}

# views/welcome/index.html.erb
<div class="row">
  <div class="col-4">
    <%= form_with url: root_path do |f| %>
      <%= f.text_field :query, class: "form-control", "data-controller": :search %>
    <% end %>
    <%= render "welcome/results" %>
  </div>

  <div class="col-8">
    <%= turbo_frame_tag :show_content %>
  </div>
</div>

# views/welcome/_results.html.erb
<%= turbo_frame_tag :results do %>
  <div class="list-group mt-3">
    <% @posts.each do |post| %>
      <%= link_to post.title,
        welcome_path(post),
        class: "list-group-item list-group-item-action",
        "data-controller": "search-active" %>
    <% end %>
  </div>
<% end %>

# views/welcome/_show.html.erb
<%= turbo_frame_tag :show_content do %>
  <h1><%= @post.title %></h1>
  <%= @post.content %>
<% end %>

# config/routes.rb
Rails.application.routes.draw do
  root to: 'welcome#index'
  resources :welcome, only: :show
end