MyFormBuilder によるビュー省力化と Stimulusjs でフォームの動作を共通化

# この資料について

- これは [鳥取Ruby会とっとるびー第34回](https://tottoruby.connpass.com/event/145106/) での発表資料です
    - https://tottoruby.connpass.com/event/145106/

----

# 話すこと

1. MyFormBuilder によるビュー省力化
2. Stimulusjs でフォームの動作を共通化
3. 最後にもうひとつ

----

# :one: MyFormBuilder によるビュー省力化

----

# FormBuilder とは

```erb
<%= 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 %>
```

:arrow_up: ここでいうところの **`f`** がそれ

----

# ところで view って記述量多いですよね?

```erb
<%= 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

```ruby
class MyFormBuilder < ActionView::Helpers::FormBuilder
end
```

Rails 提供の `ActionView::Helpers::FormBuilder` を継承し、
いい感じに、そのシステムにあったフォームビルダを作ろうという話

----

# 例: I18n キーが存在したらプレースホルダに

```ruby
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 作ろう

```ruby
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 属性つけない

```ruby
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 を予め与えても良さそう

```ruby
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 に使うやりかた

```erb
<%= form_with(model: @user,
              local: true,
              # builder オプションにクラスを渡す
              builder: MyFormBuilder,
              html: { class: 'my-form' }) do |f| %>
  ...
  <%=
    # ここでの f は MyFormBuilder のインスタンス
    f.text_field :name
  %>
<% end %>
```

----

# 都度オプション渡すの? :arrow_right: もちろん NO

ヘルパに `my_form_with` 定義

```ruby
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
```

----

# 都度オプション渡すの? :arrow_right: もちろん NO

ビューでは先に定義した `my_form_with` を使う

```erb
<%= my_form_with(model: @user, local: true, html: { class: 'my-form' }) do |f| %>
  <%= f.text_field :name %>
<% end %>
```

----

# 他オプションも既定値にしてしまう選択

```ruby
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 でビューがシンプルに

```erb
<% # 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

----

# MyFormBuilder によるビュー省力化まとめ

- My #とは
    - 別に好きな名前つければいいと思います

----

# :two: Stimulusjs でフォームの動作を共通化

----

# stimulusjs 使っていますか?

```html
<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>
```
```javascript
// 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` 属性) が主導。

----

# 例: フォームのコントローラを作り、その中にあるテキストエリアの高さを調整

```javascript
// 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 例

```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 が発火

----

# 一応動きを

![tottoruby34-0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10022/ada6eff6-2795-b651-99cb-38f925aeaeb1.gif)

----

# Stimulusjs でフォームの動作を共通化まとめ

- あんまり本腰でなく気軽な Javascript 
- だけど、これでいいんだよこれでな感じある
- html が主役
- フォームにちょっとした動きを javascript で付与したいときとか便利

----

# :three: 最後にもうひとつ

----

# 先の Stimulusjs のフォームコントローラを使う html 例

```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` とか書いちゃうわけ…?

---

# 答え :arrow_right: 否

僕らには MyFormBuilder があるじゃないですか

----

# `my_form_with` の時点で data 属性を加えましょう

```ruby
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` を拡張しましょう

```ruby
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-*` 属性が勝手に付与されると捗る

おわり