この資料について
話すこと
- MyFormBuilder によるビュー省力化
- Stimulusjs でフォームの動作を共通化
- 最後にもうひとつ
MyFormBuilder によるビュー省力化
FormBuilder とは
<%= form_with(model: @user, local: true, html: { class: 'my-form' }) do |f| %>
<%= f.text_field :name, placeholder: t('.placeholder.name') %>
<%= f.number_field :age, placeholder: t('.placeholder.age') %>
<%= f.submit name: nil %>
<% end %>
ここでいうところの f
がそれ
ところで view って記述量多いですよね?
<%= form_with(model: @user, local: true, html: { class: 'my-form' }) do |f| %>
<%= f.text_field :name, placeholder: t('.placeholder.name') %>
<%= f.number_field :age, min: 0, max: 120, placeholder: t('.placeholder.age') %>
<%= f.submit name: nil %>
<% end %>
- local オプションは大抵 true, form に紋切り型的につけたい class
- 年齢の入力で min と max 付与
- submit タグで大抵
name: nil
つけて name 属性省く - placeholder 記述
これを省力化する仕組みとしての MyFormBuilder
class MyFormBuilder < ActionView::Helpers::FormBuilder
end
Rails 提供の ActionView::Helpers::FormBuilder
を継承し、
いい感じに、そのシステムにあったフォームビルダを作ろうという話
例: I18n キーが存在したらプレースホルダに
class MyFormBuilder < ActionView::Helpers::FormBuilder
def text_field(name, options = {})
# placeholder がない場合、既定のプレースホルダを設定
options[:placeholder] ||= default_placeholder(name)
super(name, options)
end
private
def default_placeholder(name)
# ドット始まりのキーは lazy lookup
@template.t(".placeholder.#{name}", default: '').presence
end
end
例: 年齢の入力に最適化した age_field 作ろう
class MyFormBuilder < ActionView::Helpers::FormBuilder
def age_field(name, options = {})
options[:placeholder] ||= default_placeholder(name)
options[:min] ||= 0
options[:max] ||= 120
number_field(name, options)
end
end
例: submit は未指定の場合 name 属性つけない
class MyFormBuilder < ActionView::Helpers::FormBuilder
# submit name: nil と同じ扱いになるように調整
def submit(name, options = {})
# 第1引数がハッシュならそれを第2引数として扱い, 第1引数は nil 扱い
# わりとこの手は ActionView 関連ではよくある
if name.is_a?(Hash)
options = name
name = nil
end
options[:name] = nil unless options.key?(:name)
super(name, options)
end
end
例えば bootstrap の submit なら class を予め与えても良さそう
class MyFormBuilder < ActionView::Helpers::FormBuilder
def submit(name, options = {})
# 前ページの色々省略…
options[:class] = Array.wrap(options[:class]) << 'btn btn-primary'
super(name, options)
end
end
MyFormBuilder を builder に使うやりかた
<%= form_with(model: @user,
local: true,
# builder オプションにクラスを渡す
builder: MyFormBuilder,
html: { class: 'my-form' }) do |f| %>
...
<%=
# ここでの f は MyFormBuilder のインスタンス
f.text_field :name
%>
<% end %>
都度オプション渡すの? もちろん NO
ヘルパに my_form_with
定義
module ApplicationHelper
def my_form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
options[:builder] = MyFormBuilder
form_with(model: model, scope: scope, url: url, format: format, **options, &block)
end
end
都度オプション渡すの? もちろん NO
ビューでは先に定義した my_form_with
を使う
<%= my_form_with(model: @user, local: true, html: { class: 'my-form' }) do |f| %>
<%= f.text_field :name %>
<% end %>
他オプションも既定値にしてしまう選択
module ApplicationHelper
def my_form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
# クラス指定に my-form を混ぜる
html_options = (options[:html] ||= {})
html_options[:class] = Array.wrap(options[:class] || html_options[:class]) << 'my-form'
# local オプション省略時 true に (ワイルド)
options[:local] = options.fetch(:local, true)
options[:builder] = MyFormBuilder
form_with(model: model, scope: scope, url: url, format: format, **options, &block)
end
end
MyFormBuilder でビューがシンプルに
<% # Before %>
<%= form_with(model: @user, local: true, html: { class: 'my-form' }) do |f| %>
<%= f.text_field :name, placeholder: t('.placeholder.name') %>
<%= f.number_field :age, min: 0, max: 120, placeholder: t('.placeholder.age') %>
<%= f.submit name: nil %>
<% end %>
<% # After %>
<%= my_form_with(model: @user) do |f| %>
<%= f.text_field :name %>
<%= f.age_field :age %>
<%= f.submit %>
<% end %>
MyFormBuilder によるビュー省力化まとめ
- いいかんじ
- MyFormBuilder でビュー記述を省力化できる
-
form_with
にかわりmy_form_with
で MyFormBuilder を指定する - MyFormBuilder を温めておいて何か作るときにサクッと作ろう
MyFormBuilder によるビュー省力化まとめ
- 注意点
- 自分だけのプロジェクトでない場合はやりすぎに注意
- これはいわゆる Easy
- Easy は文脈が共有されてはじめて最大の効果を発揮する
- 今回のケースでは少なくとも local オプション既定値変更は驚きがある
-
追記 Rails 6.1 では
local: true
の状態がデフォルトになりそうです - https://github.com/rails/rails/pull/40708
-
追記 Rails 6.1 では
MyFormBuilder によるビュー省力化まとめ
- My #とは
- 別に好きな名前つければいいと思います
Stimulusjs でフォームの動作を共通化
stimulusjs 使っていますか?
<div data-controller="hello">
<input data-target="hello.name" type="text">
<button data-action="click->hello#greet">Greet</button>
<span data-target="hello.output"></span>
</div>
// hello_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "name", "output" ]
greet() {
this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!`
}
}
stimulusjs わりと便利です
- javascript で振る舞いを持つ controller を記述
- html に
data-controller
data-target
data-action
を付与して controller と紐つける - 例えば React とかは js であるにも関わらずもはや 「動きのあるビュー」 だと言えるが、 stimulusjs は html がビューの主役。
動きをつけるきっかけも html (のdata-controller
属性) が主導。
例: フォームのコントローラを作り、その中にあるテキストエリアの高さを調整
// form_controller.js
import { Controller } from 'stimulus'
export default class extends Controller {
static targets = [ 'textarea' ]
connect() {
// connect 後に DOM が ready になるまで待つハック
const domReady = (f) => { /in/.test(document.readyState) ? setTimeout(domReady, 16, f) : f() }
const autosizeAll = this.autosizeAll.bind(this)
domReady(autosizeAll)
}
// form に存在する全てのテキストエリアの高さを入力内容に応じて調整する
autosizeAll() { this.textareaTargets.forEach(t => this.autosize(null, t)) }
// テキストエリアの入力内容が見える高さに調整する
autosize(e, control = null) {
const target = control ? control : e.currentTarget
target.style.height = 'auto'
target.style.height = target.scrollHeight + 10 + 'px'
}
}
先のフォームコントローラを使う html 例
<form data-controller="form" data-action="resize@window->form#autosizeAll">
<textarea data-target="form.textarea"
data-action="propertychange->form#autosize change->form#autosize input->form#autosize">
foo
bar
baz
qux
</textarea>
</form>
- 初期化の際および window の resize の際に autosizeAll が発火
- テキストエリアの propertychange , change および input の際に autosize が発火
一応動きを
Stimulusjs でフォームの動作を共通化まとめ
- あんまり本腰でなく気軽な Javascript
- だけど、これでいいんだよこれでな感じある
- html が主役
- フォームにちょっとした動きを javascript で付与したいときとか便利
最後にもうひとつ
先の Stimulusjs のフォームコントローラを使う html 例
<form data-controller="form" data-action="resize@window->form#autosizeAll">
<textarea data-target="form.textarea"
data-action="propertychange->form#autosize change->form#autosize input->form#autosize">
foo
bar
baz
qux
</textarea>
</form>
全てのフォームで Stimulusjs のフォームコントローラに接続したかったら、全部のビューで data-controller
とか書いちゃうわけ…?
答え 否
僕らには MyFormBuilder があるじゃないですか
my_form_with
の時点で data 属性を加えましょう
module ApplicationHelper
def my_form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
options[:builder] = MyFormBuilder
(options[:data] ||= {}).merge!(controller: 'form', action: 'resize@window->form#autosizeAll') # NEW!
form_with(model: model, scope: scope, url: url, format: format, **options, &block)
end
end
MyFormBuilder で text_area
を拡張しましょう
class MyFormBuilder < ActionView::Helpers::FormBuilder
def text_area(name, options = {})
options[:placeholder] ||= default_placeholder(name)
data = (options[:data] ||= {})
data[:target] ||= 'form.textarea'
data[:action] ||= 'propertychange->form#autosize change->form#autosize input->form#autosize'
super(name, options)
end
end
これでテキストエリアに勝手に data-target
と data-action
が付与される
さいごのまとめ
- MyFormBuilder 便利
- Stimulusjs も便利
- MyFormBuilder と Stimulusjs の相性が良い。
data-*
属性が勝手に付与されると捗る
おわり