TR kizaki Tech Memo

Airbnb-clone by ruby and rails

tutorial

Building a header nav

Setting up a Modal


  • databaseを作る
bundle exec rails db:create
  • サーバー起動
bundle exec rails s
  • rootをsettingする
// config/routes.rb

Rails.application.routes.draw do
  root "home#index"
end
  • rspecをtestとしてsetting https://github.com/rspec/rspec-rails testing framework。コマンド1つで何度でもテストを実行できるので、プログラムのバグを減らしたり、コードの品質を上げる
gem install rspec-rails
//Gemfile 

gem "rspec-rails", "~> 6.0.0"
  • Airbnbのinspectを利用してfaviconをdownload setting
// app/views/layouts/application.html.erb

<%= favicon_link_tag asset_path('gatsby-icon.png') %>
  • generate controller spec/requests/home_spec.rbを作成、ほかのhelperはremove
─(00:10:02 on feature ✹ ✭)──> bundle exec rails g controller home --force
       force  app/controllers/home_controller.rb
      invoke  tailwindcss
       exist    app/views/home
      invoke  rspec
      create    spec/requests/home_spec.rb
      invoke  helper
      create    app/helpers/home_helper.rb
      invoke    rspec
      create      spec/helpers/home_helper_spec.rb
┌─(~/dev/monchifc)──────────────┐
└─(00:10:14 on feature ✹ ✭)──> rm app/helpers/home_helper.rb spec/helpers/home_helper_spec.rb
// app/controllers/home_controller.rb
# frozen_string_literal: true 
class HomeController < ApplicationController
  def index
  end
end
  • specをsettingする…specの設定でrspecでhelper fileを作成する

helper…Viewをよりシンプルに書くためのモジュール。Viewでの共通する処理をメソッドとして定義し、簡単に使いまわせるようにした機能。Helperを利用することによって、繰り返し記述するような処理を簡略化することができ、開発効率を高めてくれる

// spec/request/home_spec.erb

require 'rails_helper'

RSpec.describe "Homes", type: :request do
  describe "GET /index" do
    it "succeeds" do
      get root_path
      expect(response).to be_successful 
    end
  end
end
  • rspecでhelper fileを作成
└─(00:13:17 on feature ✹ ✭)──> bundle exec rails generate rspec:install
      create  .rspec
       exist  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb
└─(13:19:59 on feature ✹ ✭)──> bundle exec rspec            1 ↵ ──(Wed,Jul19)─┘
.

Finished in 0.48665 seconds (files took 4.93 seconds to load)
1 example, 0 failures

create a user model

gem "devise"
bundle
└─(14:17:45 on feature ✹)──> bundle exec rails g devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Depending on your application's configuration some manual setup may be required:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

     * Required for all applications. *

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"
     
     * Not required for API-only Applications *

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

     * Not required for API-only Applications *

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views
       
     * Not required *
  • deviseを使ってuserのmodelを作る

devise…railsで作ったwebアプリケーションに簡単に認証機能を実装できるgem

migrate fileとmodel fileが作成される

└─(14:30:16 on feature ✹ ✭)──> bundle exec rails g devise user
      invoke  active_record
      create    db/migrate/20230719103644_devise_create_users.rb
      create    app/models/user.rb
      invoke    rspec
      create      spec/models/user_spec.rb
      insert    app/models/user.rb
       route  devise_for :users

db:migrate→migrationファイルを読み込むためのコマンド

└─(14:36:44 on feature ✹ ✭)──> bundle exec rails db:migrate db:test:prepare
== 20230719103644 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.1579s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0042s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0026s
== 20230719103644 DeviseCreateUsers: migrated (0.1648s) =======================
// app/models/user.rb

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end
// config/routes.rb

Rails.application.routes.draw do
  devise_for :users
 
  root "home#index"
end
  • 認証画面用のviewsを大量生産
└─(14:41:26 on feature ✹ ✭)──> bundle exec rails generate devise:views
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_error_messages.html.erb
      create    app/views/devise/shared/_links.html.erb
      invoke  form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      invoke  erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/email_changed.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb
  • ログイン画面はこちらのファイル, サーバー確認 http://127.0.0.1:3000/users/sign_inで確認
// app/views/devise/sessions/new.html.erb

<h2>Log in</h2>

<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
  <div class="field">
    <%= f.label :email %><br /> 
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password, autocomplete: "current-password" %>
  </div>

  <% if devise_mapping.rememberable? %>
    <div class="field">
      <%= f.check_box :remember_me %>
      <%= f.label :remember_me %>
    </div>
  <% end %>

  <div class="actions">
    <%= f.submit "Log in" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>
// app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>Vacation Homes & Condo Rentals - Monchibnb</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>

    <%= favicon_link_tag asset_path('gatsby-icon.png') %>
  </head>

  <body>
    <nav class="bg-gray-800">
  <div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
    <div class="relative flex h-16 items-center justify-between">
      <div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
        <!-- Mobile menu button-->
        <button type="button" class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
          <span class="sr-only">Open main menu</span>
          <!--
            Icon when menu is closed.

            Menu open: "hidden", Menu closed: "block"
          -->
          <svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
          </svg>
          <!--
            Icon when menu is open.

            Menu open: "block", Menu closed: "hidden"
          -->
          <svg class="hidden h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
          </svg>
        </button>
      </div>
      <div class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
        <div class="flex flex-shrink-0 items-center">
          <img class="h-8 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500" alt="Your Company">
        </div>
        <div class="hidden sm:ml-6 sm:block">
          <div class="flex space-x-4">
            <!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
            <a href="#" class="bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium" aria-current="page">Dashboard</a>
            <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Team</a>
            <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Projects</a>
            <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Calendar</a>
          </div>
        </div>
      </div>
      <div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
        <button type="button" class="rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
          <span class="sr-only">View notifications</span>
          <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
          </svg>
        </button>

        <!-- Profile dropdown -->
        <div class="relative ml-3">
          <div>
            <button type="button" class="flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
              <span class="sr-only">Open user menu</span>
              <img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="">
            </button>
          </div>

          <!--
            Dropdown menu, show/hide based on menu state.

            Entering: "transition ease-out duration-100"
              From: "transform opacity-0 scale-95"
              To: "transform opacity-100 scale-100"
            Leaving: "transition ease-in duration-75"
              From: "transform opacity-100 scale-100"
              To: "transform opacity-0 scale-95"
          -->
          <div class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
            <!-- Active: "bg-gray-100", Not Active: "" -->
            <a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-0">Your Profile</a>
            <a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-1">Settings</a>
            <a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-2">Sign out</a>
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- Mobile menu, show/hide based on menu state. -->
  <div class="sm:hidden" id="mobile-menu">
    <div class="space-y-1 px-2 pb-3 pt-2">
      <!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
      <a href="#" class="bg-gray-900 text-white block rounded-md px-3 py-2 text-base font-medium" aria-current="page">Dashboard</a>
      <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Team</a>
      <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Projects</a>
      <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Calendar</a>
    </div>
  </div>
</nav>
    <%= yield %>
  </body>
</html>
└─(21:04:15 on feature-a)──> bin/rails tailwindcss:install      ──(Thu,Jul20
Add Tailwindcss include tags and container element in application layout
      insert  app/views/layouts/application.html.erb
Build into app/assets/builds
      create  app/assets/builds
      create  app/assets/builds/.keep
      append  app/assets/config/manifest.js
      append  .gitignore
Add default config/tailwindcss.config.js
      create  config/tailwind.config.js
Add default app/assets/stylesheets/application.tailwind.css
      create  app/assets/stylesheets/application.tailwind.css
Add default Procfile.dev
      create  Procfile.dev
Ensure foreman is installed
         run  gem install foreman from "."
Fetching foreman-0.87.2.gem
Successfully installed foreman-0.87.2
Parsing documentation for foreman-0.87.2
Installing ri documentation for foreman-0.87.2
Done installing documentation for foreman after 0 seconds
1 gem installed
Add bin/dev to start foreman
      create  bin/dev
Compile initial Tailwind build
         run  rails tailwindcss:build from "."

Rebuilding...

Done in 467ms.
  • tailwindcssのinstallでbin/devがgenerateされた。
// bin/dev

#!/usr/bin/env sh

if ! gem list foreman -i --silent; then
  echo "Installing foreman..."
  gem install foreman
fi

exec foreman start -f Procfile.dev "$@"

さっそく./bin/devでサーバーを立ち上げてjsを使えるようにする

└─(22:19:28 on feature-a ✹ ✭)──> ./bin/dev                  1 ↵ ──(Thu,Jul20)─┘
./bin/dev
22:20:18 web.1  | started with pid 16307
22:20:18 css.1  | started with pid 16308
22:20:19 web.1  | => Booting Puma
22:20:19 web.1  | => Rails 7.0.6 application starting in development 
22:20:19 web.1  | => Run `bin/rails server --help` for more startup options
22:20:20 web.1  | Puma starting in single mode...
22:20:20 web.1  | * Puma version: 5.6.6 (ruby 3.2.2-p53) ("Birdie's Version")
22:20:20 web.1  | *  Min threads: 5
22:20:20 web.1  | *  Max threads: 5
22:20:20 web.1  | *  Environment: development
22:20:20 web.1  | *          PID: 16307
22:20:20 web.1  | * Listening on http://127.0.0.1:3000
22:20:20 web.1  | * Listening on http://[::1]:3000
22:20:20 web.1  | Use Ctrl-C to stop
22:20:21 css.1  | 
22:20:21 css.1  | Rebuilding...
22:20:22 css.1  | 
22:20:22 css.1  | Done in 512ms.
app/views/layouts/_header.html.erb

<!--/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{html,js}"],
  theme: {
    extend: {},
  },
  plugins: [],
}
-->

<header class="bg-gray-800">
     <div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
      <div class="relative flex h-16 items-center justify-between">
       <div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
        <!-- Mobile menu button-->
        <button type="button" class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
          <span class="sr-only">Open main menu</span>
          <!--
            Icon when menu is closed.

            Menu open: "hidden", Menu closed: "block"
          -->
          <svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
          </svg>
          <!--
            Icon when menu is open.

            Menu open: "block", Menu closed: "hidden"
          -->
          <svg class="hidden h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
          </svg>
        </button>
      </div>
      <div class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
        <div class="flex flex-shrink-0 items-center">
          <img class="h-8 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500" alt="Your Company">
        </div>
        <div class="hidden sm:ml-6 sm:block">
          <div class="flex space-x-4">
            <!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
            <a href="#" class="bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium" aria-current="page">Dashboard</a>
            <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Team</a>
            <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Projects</a>
            <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Calendar</a>
          </div>
        </div>
      </div>
      <div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
        <button type="button" class="rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
          <span class="sr-only">View notifications</span>
          <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
          </svg>
        </button>

        <!-- Profile dropdown -->
        <div class="relative ml-3">
          <div>
            <button type="button" class="flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
              <span class="sr-only">Open user menu</span>
              <img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="">
            </button>
          </div>

          <!--
            Dropdown menu, show/hide based on menu state.

            Entering: "transition ease-out duration-100"
              From: "transform opacity-0 scale-95"
              To: "transform opacity-100 scale-100"
            Leaving: "transition ease-in duration-75"
              From: "transform opacity-100 scale-100"
              To: "transform opacity-0 scale-95"
          -->
          <div class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
            <!-- Active: "bg-gray-100", Not Active: "" -->
            <a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-0">Your Profile</a>
            <a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-1">Settings</a>
            <a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-2">Sign out</a>
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- Mobile menu, show/hide based on menu state. -->
  <div class="sm:hidden" id="mobile-menu">
    <div class="space-y-1 px-2 pb-3 pt-2">
      <!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
      <a href="#" class="bg-gray-900 text-white block rounded-md px-3 py-2 text-base font-medium" aria-current="page">Dashboard</a>
      <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Team</a>
      <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Projects</a>
      <a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Calendar</a>
    </div>
  </div>
</header>

  • importmapがinstall成功しているかどうか。importmap.rbに各種ツールが定義されているかcheck
// app/javascript/application.js

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"
  • importmap-rails では config/importmap.rb にDSLを使ってマッピングの設定を記載 します。

Rails 7.0 で標準になった importmap-rails とは何なのか?

config/importmap.rb

# Pin npm packages by running ./bin/importmap

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
  • tailwindのconfig fileを確認
config/tailwindcss

const defaultTheme = require('tailwindcss/defaultTheme')

module.exports = {
  content: [
    './public/*.html',
    './app/helpers/**/*.rb',
    './app/javascript/**/*.js',
    './app/views/**/*.{erb,haml,html,slim}'
  ],
  theme: {
    extend: {
      fontFamily: {
        sans: ['Inter var', ...defaultTheme.fontFamily.sans],
      },
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/aspect-ratio'),
    require('@tailwindcss/typography'),
    require('@tailwindcss/container-queries'),
  ]
}
  • scss fileを有効にするためにgem追加→ displayに反映
/ gemfile

gem "sassc-rails"
  • airbnb logoのsvg codeの上にroot_pathをsetting
// app/views/layouts/_header.html.erb

<%= link_to root_path, class: "logo-link" do %>
// app/assets/stylesheets/_header.scss

.nav-container {
    min-height: 80px;
}

.logo-link {
    svg{
    display: block;
    color: $airbnb-logo;
    }
}
// app/assets/stylesheets/_colors.scss

$airbnb-logo: #eb4c60;
  • _header.html.erbが反映されるようにlayouts設定
app/views/layouts/application.html.erb

<body>
    <%= render "layouts/header"%>
    <%= yield %>
  </body>
  • app/assets/stylesheets/application.scss: cssからscssに拡張子を変更するとerrorになる。 scss fileを反映させるためにManifest.jsをedit, application.cssを追加。 ref: manifestjs
  • The manifest.js file is meant to specify which files to use as a top-level target using sprockets methods linklink_directory, and link_tree.
app/assets/config/manifest.js

//= link_tree ../images
//= link application.css 
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js
//= link_tree ../builds
  • Stimulus’s purpose is to automatically connect DOM elements to JavaScript objects

stimulus

  • stimulusでjs fileを動かす。header controllerを作成
//app/javascript/controllers/header_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.textContent = "Hello Monchi!"
  }
}
  • data-controller header method追加
// app/views/layouts/_header.html.erb

<nav class="nav-header" data-controller="header">

  • git pushでerror発生。Mac再起動でなおった
└─(00:18:01 on feature-a)──> git push origin HEAD                          ──(Sun,Jul23)─┘
fatal: unable to access 'https://github.com/TRkizaki/monchifc.git/': LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443

Understanding SSL_ERROR_SYSCALL Error

This error typically occurs when the TCP three-way handshake between client and server completes but then a TCP reset packet (often written as “RST”) is received by the client, terminating the connection during the SSL phase.

This error is not produced when a client receives a RST packet during the three-way handshake, or after completion of the SSL/TLS negotiation (SSL phase).


  • eltransitionの実装
└─(13:39:21 on main)──> ./bin/importmap pin el-transition       ──(Thu,Jul27)─┘
Pinning "el-transition" to https://ga.jspm.io/npm:el-transition@0.0.7/index.js
//app/javascript/controllers/header_controller.js

import { Controller } from "@hotwired/stimulus"
import {enter, leave, toggle} from 'el-transition'

export default class extends Controller {
  static targets = ['openUserMenu'];

  connect() {
  }
}
//app/views/layouts/_header.html.erb

data-transition-enter="transition ease-out duration-100"
               data-transition-enter-start="transform opacity-0 scale-95"
               data-transition-enter-end="transform opacity-100 scale-100"
               data-transition-leave="transition ease-in duration-75"
               data-transition-leave-start="transform opacity-100 scale-100"
               data-transition-leave-end="transform opacity-0 scale-95"
└─(19:09:04 on main)──> mkdir app/views/shared                  ──(Thu,Jul27)─┘
┌─(~/dev/monchifc)──────────────┐
└─(19:28:15 on main)──> touch app/views/shared/_modal.html.erb
//app/views/shared/_modal.html.erb

<div data-controller="modal" 
     id="modal-wrapper"
     class="hidden relative z-10" 
     aria-labelledby="modal-title" 
     role="dialog" 
     aria-modal="true"
     >
  <!--
    Background backdrop, show/hide based on modal state.

    Entering: "ease-out duration-300"
      From: "opacity-0"
      To: "opacity-100"
    Leaving: "ease-in duration-200"
      From: "opacity-100"
      To: "opacity-0"
  -->
  <div id="modal-backdrop"
       class="hidden fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
               data-transition-enter="ease-out duration-300"
               data-transition-enter-start="opacity-0"
               data-transition-enter-end="opacity-100"
               data-transition-leave="ease-in duration-200"
               data-transition-leave-start="opacity-100"
               data-transition-leave-end="opacity-0"
  ></div>

  <div class="fixed inset-0 z-10 overflow-y-auto">
    <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
      <!--
        Modal panel, show/hide based on modal state.

        Entering: "ease-out duration-300"
          From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
          To: "opacity-100 translate-y-0 sm:scale-100"
        Leaving: "ease-in duration-200"
          From: "opacity-100 translate-y-0 sm:scale-100"
          To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
      -->
      <div id="modal-panel" 
           class="hidden relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
           data-transition-enter="ease-out duration-300"
           data-transition-enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
           data-transition-enter-end="opacity-100 translate-y-0 sm:scale-100"
           data-transition-leave="ease-in duration-200"
           data-transition-leave-start="opacity-100 translate-y-0 sm:scale-100"
           data-transition-leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
           >
        <div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
          <div class="sm:flex sm:items-start">
            <div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
              <svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
                <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
              </svg>
            </div>
            <div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
              <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">Deactivate account</h3>
              <div class="mt-2">
                <p class="text-sm text-gray-500">Are you sure you want to deactivate your account? All of your data will be permanently removed. This action cannot be undone.</p>
              </div>
            </div>
          </div>
        </div>
        <div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
          <button type="button" class="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto">Deactivate</button>
          <button type="button" class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto">Cancel</button>
        </div>
      </div>
    </div>
  </div>
</div>
//app/views/layouts/_header.html.erb

<%= render "shared/modal" %>
//app/javascript/controllers/modal_controller.js

import { Controller } from "@hotwired/stimulus"
import { enter, leave, toggle } from 'el-transition'

export default class extends Controller {
    connect() {
     enter(document.getElementById('modal-wrapper'));
     enter(document.getElementById('modal-backdrop'));
     enter(document.getElementById('modal-panel'));
   }
 }
  • setUp DropDownMenu(右端のやつ) openUserMenu → this.openUserMenuTarget
app/javascript/controllers/header_controller.js

import { Controller } from "@hotwired/stimulus"
import { enter, leave, toggle } from 'el-transition'

export default class extends Controller {
  static targets = ['openUserMenu'];

  connect() {
    this.openUserMenuTarget.addEventListener('click', this.toggleDropdownMenu)
  }

  toggleDropdownMenu(){
   toggle(document.getElementById('menu-dropdown-items'));
  }
}
  • userAuthLinkつくる log inとかsign upできるようにしたい
//app/views/layouts/_header.html.erb

<!-- Active: "bg-gray-100", Not Active: "" -->
            <div class="py-1" role="none">
            <a href="#" data-header-target="userAuthLink" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem" tabindex="-1" id="user-menu-item-0">Sign Up</a>
            <a href="#" data-header-target="userAuthLink" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem" tabindex="-1" id="user-menu-item-1">Log In</a>
            </div>
            <div class="py-1" role="none">
            <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem" tabindex="-1" id="user-menu-item-0">Host Your Home</a>
            <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem" tabindex="-1" id="user-menu-item-1">Host an experience</a>
            <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem" tabindex="-1" id="user-menu-item-2">Help</a>
            </div>
          </div>
  • clickしたらどうなるかaddEventListenerするの基本よね
  • stimulus 今回のcaseはopenUserMenu, userAuthLink

issen_controller.js側にstatic targets = [“name”, “output”]という記述をする必要もありますので忘れずに書きましょう

これで要素をJS側でthis.outputTarget、this.nameTargetというような形で使用することができます

//app/javascript/controllers/header_controller.js

import { Controller } from "@hotwired/stimulus"
import { enter, leave, toggle } from 'el-transition'

export default class extends Controller {
  static targets = ['openUserMenu', 'userAuthLink'];

  connect() {
    this.openUserMenuTarget.addEventListener('click', this.toggleDropdownMenu)

    this.userAuthLinkTargets.forEach((link) => {
      link.addEventListener('click', () => {
      console.log('user links clicked');
     });
      //link.addEventListener('click', () => {});
     });
    }

  toggleDropdownMenu(){
   toggle(document.getElementById('menu-dropdown-items'));
  }
}
  • Connecting the Action
  • document.getElementById('modal-wrapper').addEventListener('click', this.closeModal); でmodalの動きまとめちゃうの重要。modalがどこか画面クリックしたらleaveする仕組みにした。
//app/javascript/controllers/modal_controller.js

import { Controller } from "@hotwired/stimulus"
import { enter, leave, toggle } from 'el-transition'

export default class extends Controller {
    connect() {
    //  enter(document.getElementById('modal-wrapper'));
    //  enter(document.getElementById('modal-backdrop'));
    //  enter(document.getElementById('modal-panel'));

     document.getElementById('modal-wrapper').addEventListener('click', this.closeModal);
   }

   closeModal(event) {
    const modalPanelClicked = document.getElementById('modal-panel').contains(event.target);

    if(!modalPanelClicked) {
     leave(document.getElementById('modal-wrapper'));
     leave(document.getElementById('modal-backdrop'));
     leave(document.getElementById('modal-panel'));

    }
   }
 }

https://stimulus.hotwired.dev/handbook/building-something-real#connecting-the-action

  • ボタンが押された時に色を変更
  • ハンバーガーメニューの処理
  • モーダル画面の表示
  • stimulus参考→Railsはより少ない(JavaScriptの)記述で今どきのアプリケーションを作りたいという方向に舵を切り、その結果としてHotwireという技術が出来上がりました Hotwireは大きくは「Turbo + Stimulus」という要素で構成されています。
config/importmap.rb

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin "el-transition", to: "https://ga.jspm.io/npm:el-transition@0.0.7/index.js"
//app/views/shared/_modal.html.erb

data-action="click->modal#showModal"
  • showModal method追加
app/javascript/controllers/modal_controller.js

import { Controller } from "@hotwired/stimulus"
import { enter, leave, toggle } from 'el-transition'

export default class extends Controller {
    connect() {
     document.getElementById('modal-wrapper').addEventListener('click', this.closeModal);
   }

   closeModal(event) {
    const modalPanelClicked = document.getElementById('modal-panel').contains(event.target);

    if(!modalPanelClicked) {
     leave(document.getElementById('modal-wrapper'));
     leave(document.getElementById('modal-backdrop'));
     leave(document.getElementById('modal-panel'));
    }
   }

   showModal() {
    enter(document.getElementById('modal-wrapper'));
    enter(document.getElementById('modal-backdrop'));
    enter(document.getElementById('modal-panel'));
   }
 }
  • document.getElementById('modal-trigger').click(); で 右端のsign-upとLog-in clickするとmodal displayが出現する仕様
app/javascript/controllers/header_controller.js

import { Controller } from "@hotwired/stimulus"
import { enter, leave, toggle } from 'el-transition'

export default class extends Controller {
  static targets = ['openUserMenu', 'userAuthLink'];

  connect() {
    this.openUserMenuTarget.addEventListener('click', this.toggleDropdownMenu)

    this.userAuthLinkTargets.forEach((link) => {
      link.addEventListener('click', (e) => {
        e.preventDefault();
        document.getElementById('modal-trigger').click();
      });
     });
    }

  toggleDropdownMenu(){
   toggle(document.getElementById('menu-dropdown-items'));
  }
}

Error修正:modal-triggerを独立で指定するのが大事。https://stimulus.hotwired.dev/handbook/building-something-real#connecting-the-action showModal methodは modal_controller.js。