2
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

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

by hamajyotan
1 / 32

この資料について


話すこと

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

:one: 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 %>

:arrow_up: ここでいうところの 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 %>

都度オプション渡すの? :arrow_right: もちろん 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

都度オプション渡すの? :arrow_right: もちろん 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 オプション既定値変更は驚きがある

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

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

:two: 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 が発火

一応動きを

tottoruby34-0.gif


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

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

:three: 最後にもうひとつ


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


答え :arrow_right:

僕らには 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-targetdata-action が付与される


さいごのまとめ

  • MyFormBuilder 便利
  • Stimulusjs も便利
  • MyFormBuilder と Stimulusjs の相性が良い。 data-* 属性が勝手に付与されると捗る

おわり

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
2
Help us understand the problem. What are the problem?