1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Rails 6】片方の選択によってもう片方のオプションの値が変化する動的なセレクトボックスを作る

Posted at

概要

タイトル通りのものを作ります。文字だけだとわかりにくかったら、次の完成イメージを見てください。

完成イメージ

タイトルなし.gif

仕様

  • Ruby 3
  • Rails 6
  • MySQL 5.7
  • Bootstrap 5
  • Docker

※ Bootstrap はバージョン5から JQuery が不要になり導入方法が少し変わったのでご注意ください。

下準備

まず最初に下準備から始めていきます。

各種ディレクトリ & ファイルを作成

$ mkdir rails-dynamic-selectbox && cd rails-dynamic-selectbox
$ touch Dockerfile docker-compose.yml entrypoint.sh Gemfile Gemfile.lock
./Dockerfile
FROM ruby:3.0

RUN curl https://deb.nodesource.com/setup_14.x | bash

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs yarn

ENV APP_PATH /myapp

RUN mkdir $APP_PATH
WORKDIR $APP_PATH

COPY Gemfile $APP_PATH/Gemfile
COPY Gemfile.lock $APP_PATH/Gemfile.lock
RUN bundle install

COPY . $APP_PATH

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]
./docker-compose.yml
version: "3"
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: password
    volumes:
      - mysql-data:/var/lib/mysql
      - /tmp/dockerdir:/etc/mysql/conf.d/
    ports:
      - 4306:3306
  web:
    build:
      context: .
      dockerfile: Dockerfile
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
      - ./vendor/bundle:/myapp/vendor/bundle
    environment:
      TZ: Asia/Tokyo
      RAILS_ENV: development
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  mysql-data:
./entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
./Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rails", "~> 6"
./Gemfile.lock
# 空欄でOK

rails new

おなじみのコマンドでアプリの雛型を作成。

$ docker-compose run web rails new . --force --no-deps -d mysql --skip-test

database.ymlを編集

デフォルトの状態だとデータベースとの接続ができないので「database.yml」の一部を書き換えます。

./config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  host: db

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

production:
  <<: *default
  database: <%= ENV["DATABASE_NAME"] %>
  username: <%= ENV["DATABASE_USERNAME"] %>
  password: <%= ENV["DATABASE_PASSWORD"] %>
  host: <%= ENV["DATABASE_HOST"] %>

コンテナを起動 & データベースを作成

$ docker-compose build
$ docker-compose up -d
$ docker-compose run web bundle exec rails db:create

動作確認(localhost:3000 にアクセス)

スクリーンショット 2022-01-03 21.41.26.png

localhost:3000 にアクセスしてウェルカムページが表示されればOKです。

slim を導入

個人的にビューのテンプレートエンジンは erb よりも slim の方が好みなので変更します。

./Gemfile
gem 'slim-rails'
gem 'html2slim'
$ docker-compose build

既存のビューファイルを slim に書き換え。

$ docker-compose run web bundle exec erb2slim app/views app/views
$ docker-compose run web bundle exec erb2slim app/views app/views -d

Bootstrap を導入

見た目を整えるために Bootstrap を使用します。

※ 下記の手順はバージョン5を想定したものになるので、別のバージョンを使いたい場合は他の記事を参考に導入してください。

yarn add

必要なライブラリをインストール。

$ docker-compose run web yarn add bootstrap @popperjs/core

app/views/layouts/application.html

./app/views/layouts/application.html
- = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
+ = stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'

「app/views/layouts/application.html」内の9行目あたりに記述されている「stylesheet_link_tag」を「stylesheet_pack_tag」に書き換えます。

app/javascript/stylesheets/application.scss

$ mkdir app/javascript/stylesheets
$ touch app/javascript/stylesheets/application.scss

「app/javascript/stylesheets/」以下に「application.scss」を作成し、次の1行を追記してください。

./app/javascript/stylesheets/application.scss
@import "~bootstrap/scss/bootstrap";

app/javascript/packs/application.js

「app/javascript/packs/application.js」内に次の2行を追記。

./app/javascript/packs/application.js
import "bootstrap";
import "../stylesheets/application";

これで Bootstrap の設定は完了です。

Font Awesome を導入

各種アイコンを使うために Font Awesome を導入します。

$ docker-compose run web yarn add @fortawesome/fontawesome-free
app/javascript/packs/application.js
// 追記
import "@fortawesome/fontawesome-free/js/all";

これだけで OK です。

本実装

準備ができたので、本格的な実装に入りましょう。

各種モデルを作成

  • MainCategory
  • SubCategory
  • Post

今回はこの3つを作成していきます。

Post(投稿)は複数のカテゴリ(MainCategory、SubCategory)に属し、メインカテゴリーは子要素としてのサブカテゴリーを持っているイメージです。

MainCategory と SubCategory

MainCategory
id: Integer
name: String
SubCategory
id: Integer
name: String
main_category_id: Integer

MainCategory と SubCategory に関しては Active_hash で作成していきます。

./Gemfile
gem 'active_hash'
$ docker-compose build
./app/models/main_category.rb
class MainCategory < ActiveHash::Base
  include ActiveHash::Associations
  has_many :sub_categories

  self.data = [
    {id: 1, name: "ファッション" }, 
    {id: 2, name: "美容" }, 
    {id: 3, name: "グルメ" }
  ]
end
./app/models/sub_category.rb
class SubCategory < ActiveHash::Base
  include ActiveHash::Associations
  belongs_to :main_category

  self.data = [
    {id: 1, name: "プチプラ", main_category_id: 1 }, 
    {id: 2, name: "ハイブランド", main_category_id: 1 }, 
    {id: 3, name: "コスメ", main_category_id: 2 },
    {id: 4, name: "ヘアアレンジ", main_category_id: 2 },
    {id: 5, name: "ネイル", main_category_id: 2 },
    {id: 6, name: "手料理", main_category_id: 3 },
    {id: 7, name: "カフェ巡り", main_category_id: 3 }
  ]
end

Post

Post
id: Integer
title: String
body: Text
main_category_id: Integer
sub_category_id: Integer
$ docker-compose run web rails g model Post title:string body:text main_category_id:integer sub_category_id:integer
./app/models/post.rb
class Post < ApplicationRecord
  extend ActiveHash::Associations::ActiveRecordExtensions
  
  belongs_to_active_hash :main_category
  belongs_to_active_hash :sub_category

  validates :title, presence: true
  validates :body, presence: true
  validates :main_category_id, presence: true
  validates :sub_category_id, presence: true
end

コントローラーを作成

$ docker-compose run web rails g controller posts
./app/controllers/posts_controller.rb
class PostsController < ApplicationController
  # Bootstrap に対応したフラッシュメッセージを表示するための設定
  add_flash_types :success, :info, :warning, :danger
  before_action :set_post, only: %i[edit update]

  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def create
    post = Post.new(post_params)

    if post.save!
      redirect_to posts_path, success: "投稿の作成に成功しました"
    else
      render :new, danger: "投稿の作成に失敗しました"
    end
  end

  def edit; end

  def update
    if @post.update!(post_params)
      redirect_to posts_path, success: "投稿の更新に成功しました"
    else
      render :new, danger: "投稿の更新に失敗しました"
    end
  end

  private
  
    def post_params
      params.require(:post).permit(
        :title, :body, :main_category_id, :sub_category_id
      )
    end

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

ビューを作成

$ touch app/views/posts/index.html.slim app/views/posts/new.html.slim app/views/posts/edit.html.slim app/views/posts/_form.html.slim
./app/views/posts/index.html.slim
- flash.each do |type, message|
  div class="alert alert-#{type} alert-dismissible rounded-0 fade show" role="alert" 
    button.btn-close aria-label="Close" data-bs-dismiss="alert" type="button" 
    h5.m-0
      = message

.container.p-5
  .d-flex.mb-2
    .ms-auto
      = link_to new_post_path, class: "btn btn-sm btn-outline-primary me-2" do
        i.fas.fa-circle-plus.me-1
        | 新規作成

  .table-responsive
    table.table
      thead
        tr.table-dark style="white-space: nowrap;"
          th
          th scope="col" ID
          th scope="col" タイトル
          th scope="col" 本文
          th scope="col" メインカテゴリー
          th scope="col" サブカテゴリー
      tbody
        - @posts&.each do |post|
          tr style="white-space: nowrap;"
            td
              = link_to edit_post_path(post), class: "btn btn-sm btn-outline-success me-2" do
                i.fas.fa-edit.me-1
                | 編集
            td = post.id
            td = post.title
            td = post.body
            td = post.main_category.name
            td = post.sub_category.name
./app/views/posts/new.html.slim
.container.p-5
  .row
    .d-flex.align-items-center.justify-content-center
      .col-6
        = render "form", post: @post
./app/views/posts/edit.html.slim
.container.p-5
  .row
    .d-flex.align-items-center.justify-content-center
      .col-6
        = render "form", post: @post
./app/views/posts/_form.html.slim
.table-responsive
  = form_with model: post, local: true do |f|
    table.table.table-borderless
      tr
        td = f.label :title, class: "control-label"
        td = f.text_field :title, class: "form-control"
      
      tr
        td = f.label :body, class: "control-label"
        td = f.text_area :body, rows: 3, class: "form-control"

      tr
        td = f.label :main_category_id, class: "control-label"
        td = f.collection_select :main_category_id, MainCategory.all, :id, :name, { include_blank: true }, class: "form-select", id: "mainCategorySelect"
      
      tr
        td = f.label :sub_category_id, class: "control-label"
        td
          = f.collection_select :sub_category_id, @post.main_category&.sub_categories || [], :id, :name, { include_blank: true }, class: "form-select", id: "subCategorySelect", disabled: @post.main_category&.sub_categories.blank?
          - MainCategory.all.each do |main_category|
            template.div id="mainCategory#{main_category.id}"
              = f.collection_select :sub_category_id, main_category.sub_categories, :id, :name, { include_blank: true, selected: @post.sub_category_id }, class: "form-select"
    
    .d-flex.align-items-center.justify-content-center
      = f.submit class: "btn btn-success"

javascript:
  var mainCategorySelect = document.getElementById("mainCategorySelect");

  // メインカテゴリーの選択をトリガーに実行
  mainCategorySelect.addEventListener("change", function(){
    // 入力された値(メインカテゴリーID)を取得し、それに対応したtemplate要素を決定 
    var selectedMainCategoryValue = mainCategorySelect.value;
    var template = document.getElementById(`mainCategory${selectedMainCategoryValue}`);
    
    var subCategorySelect = document.getElementById("subCategorySelect");

    // メインカテゴリーが選択された場合、サブカテゴリーのセレクトボックスを有効化して各種template要素にすり替え
    if (selectedMainCategoryValue !== "") {
      subCategorySelect.disabled = false;
      subCategorySelect.innerHTML = template.innerHTML;
    } else {
      subCategorySelect.disabled = true;
      subCategorySelect.value = "";
    }
  });

ポイントは template です。

参照記事: : コンテンツテンプレート要素

<template> は HTML の要素で、ページが読み込まれたときにすぐにレンダリングされるのではなく、実行時に JavaScript を使って後からインスタンス化することができる HTML を保持するためのメカニズムです。

この template の性質を利用して予めメインカテゴリーに紐付いたサブカテゴリーのみが選択肢となったセレクトボックスを隠し持っておき、メインカテゴリーの選択をトリガーとしてそれぞれ対応したものを表示させるという流れになっています。

スクリーンショット 2022-02-16 3.49.19.png

ルーティングを設定

./config/routes.rb
Rails.application.routes.draw do
  resources :posts, except: %i[show destroy]
end

日本語化

最後に「config/application.rb」内に以下を内容を記述し、デフォルトのタイムゾーンや言語を日本に変更してください。

./config/application.rb
module Myapp
  class Application < Rails::Application
    # ...

    # 追記
    config.time_zone = "Tokyo"
    config.active_record.default_timezone = :local
    config.i18n.default_locale = :ja
    config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}").to_s]

    # ...
  end
end

また、「config/locals/」以下にja.ymlを作成します。

$ touch config/locales/ja.yml
./config/locales/ja.yml
ja:
  activerecord:
    models:
      post: 投稿
      main_category: メインカテゴリー
      sub_category: サブカテゴリー
    attributes:
      post:
        id: ID
        title: タイトル
        body: 本文
        main_category_id: メインカテゴリー
        sub_category_id: サブカテゴリー
        created_at: 登録日
        updated_at: 更新日

  helpers:
    submit:
      create: 登録する
      update: 更新する
      submit: 保存する

動作確認

スクリーンショット 2022-02-16 3.59.07.png

localhost:3000/posts/new にアクセスしてこんな感じになっていれば完成です。

ちゃんと動作しているか確認してください。

あとがき

以上、片方の選択によってもう片方のオプションの値が変化する動的なセレクトボックスを作ってみました。

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?