環境
Ruby 2.5.7
Rails 5.2.4
経緯
Railsのテンプレートエンジンとして.erbを使っているのですが、テンプレートエンジンはそれ以外にも.hamlや.slimがあります。
今回.slimに興味を持ち、既存の.erbファイルの一部を.slimに変更して動作まで確認できたので、その過程を書いていきます。
ほぼケーススタディとなるので、参考にしていただければと思います。
基本構文
基本的な変更点としては次のようなものがあります。
/ 開始タグのみ、終了タグは記述しない
<body></body>
=>
body
<h1>タイトル</h1>
=>
h1 タイトル
/ id名class名は続けて書く
<ul class="list"></ul>
=>
ul.list
<span id="btn"></span>
=>
span#btn
/ divはdivすらも省略する
<div id="main-contents" class="flex container" ></div>
=>
# main-contents.flex.container
/ rubyコード<%= %>は省略する(=があるものは=だけ書く)
<%= link_to '次へ' %>
=>
= link_to '次へ'
/ <% %> =が付かないものは、先頭に-をつける
/ <% end %>は全て省略(ループ文など)
<% if 条件文 %>
<% else %>
<% end %>
=>
- if 条件文
- else
構文の詳細はこちらを参照してください。
GitHub - slim
Qiita - 速習テンプレートSlim(HTML作成編)
Qiita - RailsのHTMLテンプレートエンジン、Slimの基本的な記法
Qiita - 【爆速で習得】Railsでslimを使う方法から基本文法まで
実際のコードで.erbと.slimを比較
ここからは、私が実際に変更したファイルをbefore/after形式で下記の4種類ご紹介します。
*application.html.erb/slim
*_form.html.erb/slim
*new.html.erb/slim
*edit.html.erb/slim
これらのViewファイルは私のGitHub上でも公開しております。
GitHub - matchi_ver.slim
application.html
<!DOCTYPE html>
<html lang="ja">
<head>
<%= favicon_link_tag('favicon.ico') %>
<%= favicon_link_tag 'home-icon.png', rel: 'apple-touch-icon', size: '180x180', type: 'image/png' %>
<%= favicon_link_tag 'home-icon.png', rel: 'android-touch-icon', size: '192x192', type: 'image/png' %>
<title>Matchi</title>
<script src="//maps.google.com/maps/api/js?key=<%= ENV['GOOGLE_PLATFORM_API_KEY'] %>"></script>
<%= include_gon %>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application' %>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= ENV['GOOGLE_ANALYTICS_TRACKING_ID'] %>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', "<%= ENV['GOOGLE_ANALYTICS_TRACKING_ID'] %>");
</script>
</head>
<body>
<header>
<div class="header-container">
<%# PC画面ヘッダー %>
<div class="flex pc-header">
<%= link_to root_path do %>
<div class="logo-image"></div>
<% end %>
<div class="header-nav">
<nav>
<ul class="flex header-ul">
<% url = request.fullpath %>
<%# urlに'owner'があれば店舗用ヘッダー %>
<% if url.include?('owner') %>
<% if master_admin_signed_in? %>
<li><%= link_to '管理者トップ', new_master_admin_session_path %></li>
<li><%= link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete %></li>
<% end %>
<% if owner_restaurant_signed_in? %>
<li><%= link_to '店舗トップ', owner_restaurant_path(current_owner_restaurant) %></li>
<li><%= link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete %></li>
<% end %>
<% if public_user_signed_in? %>
<li><%= link_to '一般会員TOP', mypage_path(current_public_user) %></li>
<li><%= link_to '一般会員ログアウト', destroy_public_user_session_path, method: :delete %></li>
<% else %>
<li><%= link_to '一般会員ログイン', new_public_user_session_path %></li>
<% end %>
<%# urlに'master'があれば管理者用ヘッダー %>
<% elsif url.include?('master') %>
<% if master_admin_signed_in? %>
<li><%= link_to '店舗新規登録', new_owner_restaurant_registration_path %></li>
<li><%= link_to '管理者トップ', new_master_admin_session_path %></li>
<li><%= link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete %></li>
<% end %>
<% if public_user_signed_in? %>
<li><%= link_to '一般会員TOP', mypage_path(current_public_user) %></li>
<li><%= link_to '一般会員ログアウト', destroy_public_user_session_path, method: :delete %></li>
<% else %>
<li><%= link_to '一般会員ログイン', new_public_user_session_path %></li>
<% end %>
<% if owner_restaurant_signed_in? %>
<li><%= link_to '店舗トップ', owner_restaurant_path(current_owner_restaurant) %></li>
<li><%= link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete %></li>
<% else %>
<li><%= link_to '店舗ログイン', new_owner_restaurant_session_path %></li>
<% end %>
<%# 上記以外なら一般ユーザー用ヘッダー %>
<% else %>
<% if master_admin_signed_in? && owner_restaurant_signed_in? %>
<li><%= link_to '管理者トップ', new_master_admin_session_path %></li>
<li><%= link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete %></li>
<li><%= link_to '店舗TOP', owner_restaurant_path(current_owner_restaurant) %></li>
<li><%= link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete %></li>
<% elsif owner_restaurant_signed_in? %>
<li><%= link_to '店舗TOP', owner_restaurant_path(current_owner_restaurant) %></li>
<li><%= link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete %></li>
<% elsif master_admin_signed_in? %>
<li><%= link_to '管理者トップ', new_master_admin_session_path %></li>
<li><%= link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete %></li>
<li><%= link_to '店舗ログイン', new_owner_restaurant_session_path %></li>
<% end %>
<% if public_user_signed_in? %>
<li><%= link_to 'MyPage', mypage_path(current_public_user) %></li>
<%# if alert.count >= 1 %>
<li><%#= link_to 'お知らせがあります。' %></li>
<%# end %>
<li><%= link_to 'ログアウト', destroy_public_user_session_path, method: :delete %></li>
<% else %>
<li><%= link_to '新規登録', new_public_user_registration_path %></li>
<li><%= link_to 'ログイン', new_public_user_session_path %></li>
<% end %>
<% end %>
</ul>
</nav>
</div>
</div>
<%# スマホ画面ヘッダー %>
<div class="flex sp-header">
<div class="hamburger">
<span class="bar bar-top"></span>
<span class="bar bar-center"></span>
<span class="bar bar-bottom"></span>
</div>
<%= link_to root_path do %>
<div class="logo-image"></div>
<% end %>
<%# 未読のお知らせの通知 %>
<div class="alert">
<i id="alert-bell" class="fa-2x far fa-bell"><div class="hidden icon"></div></i>
</div>
<%# ハンバーガーメーニュー %>
<div class="hamburger-menu">
<ul>
<% if public_user_signed_in? %>
<li><%= link_to 'MyPage', mypage_path(current_public_user) %></li>
<% else %>
<li><%= link_to '新規登録', new_public_user_registration_path %></li>
<li><%= link_to 'ログイン', new_public_user_session_path %></li>
<% end %>
<li><%= link_to 'サービス紹介', about_path %></li>
<li><%= link_to 'レストラン一覧', public_restaurants_path %></li>
<li><%= link_to 'メニュー一覧', public_menus_path %></li>
<% if public_user_signed_in? %>
<li><%= link_to 'ログアウト', destroy_public_user_session_path, method: :delete %></li>
<% end %>
</ul>
</div>
</div>
</div>
</header>
<main>
<div class="body-container">
<%= yield %>
</div>
</main>
<footer>
<div class="flex footer-container">
<%= link_to root_path do %>
<div class="logo-image"></div>
<% end %>
<div class="footer-menu">
<ul class="footer-links">
<li><%= link_to 'お問い合わせ', contacts_new_path %></li>
<li><%= link_to '利用規約', terms_path %></li>
<li><%= link_to 'プライバシーポリシー', privacy_path %></li>
<li><%= link_to '運営者情報', admin_path %></li>
</ul>
</div>
</div>
<div class="copyright">
<small>©︎ 2020 MasaoSasaki</small>
</div>
<div id="move-head">
<div class="circle move-head"><i class="fa-2x fas fa-arrow-up"></i></div>
</div>
</body>
</footer>
</html>
doctype html
html lang="ja"
head
= favicon_link_tag('favicon.ico')
= favicon_link_tag 'home-icon.png', rel: 'apple-touch-icon', size: '180x180', type: 'image/png'
= favicon_link_tag 'home-icon.png', rel: 'android-touch-icon', size: '192x192', type: 'image/png'
title Matchi
script src="//maps.google.com/maps/api/js?key=#{ENV['GOOGLE_PLATFORM_API_KEY']}"
= include_gon
= csrf_meta_tags
= csp_meta_tag
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application'
meta name="viewport" content="width=device-width,initial-scale=1"
link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet"
/ Global site tag (gtag.js) - Google Analytics
javascript: async src="https://www.googletagmanager.com/gtag/js?id=#{ENV['GOOGLE_ANALYTICS_TRACKING_ID']}"
body
header
.header-container
/ PC画面ヘッダー
.flex.pc-header
= link_to root_path
.logo-image
.header-nav
nav
ul.flex.header-ul
- url = request.fullpath
/ urlに'owner'があれば店舗用ヘッダー
- if url.include?('owner')
- if master_admin_signed_in?
li = link_to '管理者トップ', new_master_admin_session_path
li = link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete
- if owner_restaurant_signed_in?
li = link_to '店舗トップ', owner_restaurant_path(current_owner_restaurant)
li = link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete
- if public_user_signed_in?
li = link_to '一般会員TOP', mypage_path(current_public_user)
li = link_to '一般会員ログアウト', destroy_public_user_session_path, method: :delete
- else
li = link_to '一般会員ログイン', new_public_user_session_path
/ urlに'master'があれば管理者用ヘッダー
- elsif url.include?('master')
- if master_admin_signed_in?
li = link_to '店舗新規登録', new_owner_restaurant_registration_path
li = link_to '管理者トップ', new_master_admin_session_path
li = link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete
- if public_user_signed_in?
li = link_to '一般会員TOP', mypage_path(current_public_user)
li = link_to '一般会員ログアウト', destroy_public_user_session_path, method: :delete
- else
li = link_to '一般会員ログイン', new_public_user_session_path
- if owner_restaurant_signed_in?
li = link_to '店舗トップ', owner_restaurant_path(current_owner_restaurant)
li = link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete
- else
li = link_to '店舗ログイン', new_owner_restaurant_session_path
/ 上記以外なら一般ユーザー用ヘッダー
- else
- if master_admin_signed_in? && owner_restaurant_signed_in?
li = link_to '管理者トップ', new_master_admin_session_path
li = link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete
li = link_to '店舗TOP', owner_restaurant_path(current_owner_restaurant)
li = link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete
- elsif owner_restaurant_signed_in?
li = link_to '店舗TOP', owner_restaurant_path(current_owner_restaurant)
li = link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete
- elsif master_admin_signed_in?
li = link_to '管理者トップ', new_master_admin_session_path
li = link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete
li = link_to '店舗ログイン', new_owner_restaurant_session_path
- if public_user_signed_in?
li = link_to 'MyPage', mypage_path(current_public_user)
/ if alert.count >= 1
li = link_to 'お知らせがあります。'
li = link_to 'ログアウト', destroy_public_user_session_path, method: :delete
- else
li = link_to '新規登録', new_public_user_registration_path
li = link_to 'ログイン', new_public_user_session_path
/スマホ画面ヘッダー
.flex.sp-header
.hamburger
span.bar.bar-top
span.bar.bar-center
span.bar.bar-bottom
= link_to root_path
.logo-image
/ 未読のお知らせの通知
.alert
i#alert-bell.fa-2x.far.fa-bell
.hidden.icon
/ ハンバーガーメーニュー
.hamburger-menu
ul
- if public_user_signed_in?
li = link_to 'MyPage', mypage_path(current_public_user)
- else
li = link_to '新規登録', new_public_user_registration_path
li = link_to 'ログイン', new_public_user_session_path
li = link_to 'サービス紹介', about_path
li = link_to 'レストラン一覧', public_restaurants_path
li = link_to 'メニュー一覧', public_menus_path
- if public_user_signed_in?
li = link_to 'ログアウト', destroy_public_user_session_path, method: :delete
main
.body-container == yield
footer
.flex.footer-container
= link_to root_path
.logo-image
.footer-menu
ul.footer-links
li = link_to 'お問い合わせ', contacts_new_path
li = link_to '利用規約', terms_path
li = link_to 'プライバシーポリシー', privacy_path
li = link_to '運営者情報', admin_path
.copyright
small ©︎ 2020 MasaoSasaki
#move-head
.circle.move-head
i.fa-2x.fas.fa-arrow-up
new.html
<div class="contents menus-new">
<h2>メニュー追加</h2>
<%= render partial: 'form', locals: {
restaurant: @restaurant,
menu: @menu,
tags: @tags,
menu_tags: @menu_tags,
path: owner_restaurant_menus_path,
truth: false, submit: '作成'
} %>
</div>
.contents.menus-new
h2 メニュー追加
== render 'form',
restaurant: @restaurant,
menu: @menu,
tags: @tags,
menu_tags: @menu_tags,
path: owner_restaurant_menus_path,
truth: false, submit: '作成'
edit.html
<div class="contents menus-edit">
<h2 class="menus-edit-h2">メニュー編集</h2>
<%= render partial: 'form', locals: {
restaurant: @restaurant,
menu: @menu,
tags: @tags,
menu_tags: @menu_tags,
path: owner_restaurant_menu_path,
truth: true, submit: '更新'
} %>
</div>
contents.menus-edit
h2.menus-edit-h2 メニュー編集
== render 'form',
restaurant: @restaurant,
menu: @menu,
tags: @tags,
menu_tags: @menu_tags,
path: owner_restaurant_menu_path,
truth: true, submit: '更新'
_form.html
<div class="menu-form">
<%= form_with model: [restaurant, menu], url: path, local: true do |f| %>
<section class="menu-status">
<div class="menu-form1">
<h3>メニュー詳細</h3>
<table>
<tbody>
<tr>
<td><%= f.label :title, value: 'メニュー名' %></td>
<td><%= f.text_field :title %></td>
</tr>
<tr>
<td><%= f.label :regular_price, value: '正規価格(税抜き):' %></td>
<td><%= f.number_field :regular_price %> 円</td>
</tr>
<tr>
<td><%= f.label :discount_price, value: '提供価格(税抜き):' %></td>
<td><%= f.number_field :discount_price %> 円</td>
</tr>
<tr>
<td><%= f.label :reservation_method, value: '予約方法' %></td>
<td><%= f.select :reservation_method, Menu.reservation_methods.keys.map {|method| [method]} %></td>
</tr>
<tr>
<td><%= f.label :is_sale_frag, value: '販売ステータス' %></td>
<td><%= f.select :is_sale_frag, [['販売中', true], ['販売停止中', false]] %></td>
</tr>
</tbody>
</table>
</div>
<div class="menu-form2">
<h3>メニュー画像</h3>
<div class="menu-image">
<%= f.attachment_field :menu_image %>
<div class="image-preview"></div>
<h4>タグの追加(任意)</h4>
<%= text_field_tag :tag_name %>
<%= button_tag '追加', type: 'button', class: "add-tag-btn" %>
<div id="tag-list"></div>
</div>
</div>
</div>
</section>
<section class="menu-tag-form">
<h3>タグ詳細</h3>
<table>
<tbody>
<%# 編集画面でのみ表示 %>
<% if truth %>
<tr>
<td><h4>現在のタグ一覧</h4></td>
<td>
<% menu_tags.each do |menu_tag| %>
<div class="menu-tag">
<%= Tag.find(menu_tag.tag_id).name %>
<%= link_to 'x', {controller: 'menu_tags', action: 'destroy', tag_id: menu_tag, menu_id: params[:id], restaurant_id: params[:restaurant_id]}, method: :delete %>
</div>
<% end %>
</td>
</tr>
<% end %>
<tr>
<td><h4>タグの追加<br>(一つ以上選択推奨)</h4></td>
<td>
<% tag_count = 0 %>
<% tags.each do |tag| %>
<%# 推奨タグ7個を表示 %>
<% if tag_count < 7 %>
<div class="check-box">
<% if menu_tags.exists?(tag_id: tag.id) %>
<%= check_box :tag_id, tag.id, checked: true %>
<%= label_tag :tag_id, "#{tag.name}"%>
<% else %>
<%= check_box :tag_id, tag.id %>
<%= label_tag :tag_id, "#{tag.name}"%>
<% end %>
</div>
<% tag_count += 1 %>
<% else %>
<% break %>
<% end %>
<% end %>
</td>
</tr>
</tbody>
</table>
</section>
<section class="menu-form-area">
<div class="content-form">
<p><%= f.label :content, value: '内容' %></p>
<%= f.text_area :content %>
</div>
<div class="cancel-form">
<p><%= f.label :cancel, value: 'キャンセル規定' %></p>
<%= f.text_area :cancel %>
</div>
</section>
<div class="submit"><%= f.button "#{submit}", onclick: 'submit();', type: 'button', class: 'btn' %></div>
<% end %>
</div>
.menu-form
= form_with model: [restaurant, menu], url: path, local: true do |f|
section.menu-status
.menu-form1
h3 メニュー詳細
table
tbody
tr
td = f.label :title, value: 'メニュー名'
td = f.text_field :title
tr
td = f.label :regular_price, value: '正規価格(税抜き):'
td
= f.number_field :regular_price
| 円
tr
td = f.label :discount_price, value: '提供価格(税抜き):'
td
= f.number_field :discount_price
| 円
tr
td = f.label :reservation_method, value: '予約方法'
td = f.select :reservation_method, Menu.reservation_methods.keys.map {|method| [method]}
tr
td = f.label :is_sale_frag, value: '販売ステータス'
td = f.select :is_sale_frag, [['販売中', true], ['販売停止中', false]]
.menu-form2
h3 メニュー画像
.menu-image
= f.attachment_field :menu_image
.image-preview
h4 タグの追加(任意)
= text_field_tag :tag_name
= button_tag '追加', type: 'button', class: "add-tag-btn"
#tag-list
section.menu-tag-form
h3 タグ詳細
table
tbody
/ 編集画面でのみ表示
- if truth
tr
td: h4 現在のタグ一覧
td
- menu_tags.each do |menu_tag|
.menu-tag
= Tag.find(menu_tag.tag_id).name
= link_to 'x', {controller: 'menu_tags', action: 'destroy', tag_id: menu_tag, menu_id: params[:id], restaurant_id: params[:restaurant_id]}, method: :delete
tr
td: h4 タグの追加<br>(一つ以上選択推奨)
td
- tag_count = 0
- tags.each do |tag|
/ 推奨タグ7個を表示
- if tag_count < 7
.check-box
- if menu_tags.exists?(tag_id: tag.id)
= check_box :tag_id, tag.id, checked: true
= label_tag :tag_id, "#{tag.name}"
- else
= check_box :tag_id, tag.id
= label_tag :tag_id, "#{tag.name}"
- tag_count += 1
- else
- breeak
section.menu-form-area
.content-form
p = f.label :content, value: '内容'
= f.text_area :content
.cancel-form
p = f.label :cancel, value: 'キャンセル規定'
= f.text_area :cancel
.submit= f.button "#{submit}", onclick: 'submit();', type: 'button', class: 'btn'
まとめ・感想
上記のコードは全て動作確認済みです。
.erbと.slimのコード量を比較すると大体2/3ぐらいになります。
インデントがとても大事で、インデントを間違えるだけで普通にsyntax errorになるので、その場合はブロックごとにコメントアウトしながら確認をしました。
application.html.slimなら、bodyは全てコメントアウトして、まずはhead内だけ取り掛かり、head内でもsyntax errorならまたその中でコメントアウトを駆使して問題を切り分けていくような流れです。
これからslimに書き換えようと思っている方の参考になれば幸いです。
コードでわからない箇所や不明点、質問、解釈の違い、記述方法に違和感がありましたら、コメント等でご指摘いただけると幸いです。
最後まで読んでいただきありがとうございました。
参考サイト
GitHub - slim
Qiita - 速習テンプレートSlim(HTML作成編)
Qiita - RailsのHTMLテンプレートエンジン、Slimの基本的な記法
Qiita - 【爆速で習得】Railsでslimを使う方法から基本文法まで
Qiita - Slim コードのリファクタリング
GitHub - matchi_ver.slim