概要
タイトル通りのものを作ります。文字だけだとわかりにくかったら、次の完成イメージを見てください。
完成イメージ
仕様
- 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
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"]
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:
#!/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 "$@"
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "rails", "~> 6"
# 空欄でOK
rails new
おなじみのコマンドでアプリの雛型を作成。
$ docker-compose run web rails new . --force --no-deps -d mysql --skip-test
database.ymlを編集
デフォルトの状態だとデータベースとの接続ができないので「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 にアクセス)
localhost:3000 にアクセスしてウェルカムページが表示されればOKです。
slim を導入
個人的にビューのテンプレートエンジンは erb よりも slim の方が好みなので変更します。
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
- = 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行を追記してください。
@import "~bootstrap/scss/bootstrap";
app/javascript/packs/application.js
「app/javascript/packs/application.js」内に次の2行を追記。
import "bootstrap";
import "../stylesheets/application";
これで Bootstrap の設定は完了です。
Font Awesome を導入
各種アイコンを使うために Font Awesome を導入します。
$ docker-compose run web yarn add @fortawesome/fontawesome-free
// 追記
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 で作成していきます。
gem 'active_hash'
$ docker-compose build
class MainCategory < ActiveHash::Base
include ActiveHash::Associations
has_many :sub_categories
self.data = [
{id: 1, name: "ファッション" },
{id: 2, name: "美容" },
{id: 3, name: "グルメ" }
]
end
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
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
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
- 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
.container.p-5
.row
.d-flex.align-items-center.justify-content-center
.col-6
= render "form", post: @post
.container.p-5
.row
.d-flex.align-items-center.justify-content-center
.col-6
= render "form", post: @post
.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
の性質を利用して予めメインカテゴリーに紐付いたサブカテゴリーのみが選択肢となったセレクトボックスを隠し持っておき、メインカテゴリーの選択をトリガーとしてそれぞれ対応したものを表示させるという流れになっています。
ルーティングを設定
Rails.application.routes.draw do
resources :posts, except: %i[show destroy]
end
日本語化
最後に「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
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: 保存する
動作確認
localhost:3000/posts/new にアクセスしてこんな感じになっていれば完成です。
ちゃんと動作しているか確認してください。
あとがき
以上、片方の選択によってもう片方のオプションの値が変化する動的なセレクトボックスを作ってみました。