クライアント側でインクリメンタルバリデーション(Opal使用)

# 未完成

# 動機
- クライアント側でインクリメンタルにバリデーションしたい
- Opalで
- バリデーション自体はサーバ側に任せたい
    - でもWEB Socketは使いたくない

# 利用
- クライアント側

```ruby
opal
  require 'opal-jquery'

  Document.ready? do

    # 前提
    # フォームのinputはidが "#{model_name}_#{cur_attr_name}" となっている
    # 送受信のI/F
    #   フォーマットはJSON
    #   リクエスト: 
    #      Parameters: {"user"=>{"name"=>"a", "age"=>"", "password"=>"", "password_confirmation"=>""}}
    #   レスポンス:
    #      respone.jsonで以下の形式が取れる
    #      {"messages"=>{"password"=>[""], "name"=>[""], "age"=>[""], "password_confirmation"=>[""]}}

    ClientValidator.register('/users/validate') do
      model 'user'
      attrs 'name', 'age', 'password', 'password_confirmation'

      # 実行前。共通
      before do |elem|
      end

      # 実行後、valid時。アトリビュート共通
      all_success do |elem, response|
        # 全てのアトリビュートがvalidならsubmitボタン有効
        Element[':submit'].prop('disabled', false)
      end

      # 実行後、invalid時。アトリビュート共通
      any_error do |elem, response|
        # 何れかのアトリビュートがinvalidならsubmitボタン無効
        Element[':submit'].prop('disabled', true)
      end

      # 実行後、valid時。アトリビュート毎
      attr_success do |elem, attr_name, response|
        # アトリビュート毎のvalidationメッセージを消す
        Element["##{elem.id}_field"].remove_class('has-error')
        Element["#user_#{attr_name}_error"].text = "\u00a0"
      end

      # 実行後、invalid時。アトリビュート毎
      attr_error do |elem, attr_name, response|
        # アトリビュート毎のvalidationメッセージを表示
        Element["##{elem.id}_field"].add_class('has-error')
        Element["#user_#{attr_name}_error"].text = response.json[:messages][attr_name]
      end 

      # 実行後。アトリビュート共通
      after do |elem, response|
      end
    end
```

- サーバ側

```
  def validate
    user = User.new(user_params)
    if user.valid?
      render json: { notice: '' }
    else
      render json: { notice: user.errors.messages }, status: 400
    end
  end
```

# クラス
```
  class ClientValidator
    def self.register(url, &block)
      n = new
      n.instance_variable_set(:@actions, {})
      n.instance_variable_set(:@url, "#{url}.json")
      n.instance_eval(&block)
      n.done
    end

    private

      def model(model_name)
        @model_name = model_name
      end
  
      def attrs(*attr_names)
        @attr_names = attr_names
      end
  
      def before(&block);       @actions[:before]        = block; end
      def after(&block);        @actions[:after]         = block; end
      def all_success(&block);  @actions[:all_success]   = block; end
      def any_error(&block);    @actions[:any_error]     = block; end
      def attr_success(&block); @actions[:attr_success]  = block; end
      def attr_error(&block);   @actions[:attr_error]    = block; end
      def after(&block);        @actions[:after]         = block; end
  
      def done
        validate(@url, @model_name, @attr_names, @actions)
      end
  
  
      def validate(url, model_name, attr_names, actions)
        attr_names.each do |attr_name|
          cur_attr_name, *other_attr_names = *attr_names.partition{|a| a == attr_name}.flatten
          register_event(url, model_name, cur_attr_name, other_attr_names, actions)
        end
      end
    
      def form_ajax_data(model_name, attrs)
        attr_hash = attrs.map{|attr|
                      [attr, Element["##{model_name}_#{attr}"].value]
                    }.to_h
        {model_name.to_sym => attr_hash}
      end
    
      def register_event(url, model_name, cur_attr_name, other_attr_names, actions)
        Element["##{model_name}_#{cur_attr_name}"].on :keyup do |evt|
          data = form_ajax_data(model_name, [cur_attr_name] + other_attr_names)
          send_ajax(url, model_name, cur_attr_name, data, actions)
        end
      end
    
      def send_ajax(url, model_name, cur_attr_name, data, actions)
        elem = Element["##{model_name}_#{cur_attr_name}"]
        actions[:before] && actions[:before].call(elem)
    
        HTTP.post(url, payload: data) do |res|
          if res.ok?
            actions[:all_success] && actions[:all_success].call(elem, cur_attr_name, res)
          else
            actions[:any_error] && actions[:any_error].call(elem, cur_attr_name, res)
          end
          unless res.json[:messages][cur_attr_name.to_sym]
            actions[:attr_success] && actions[:attr_success].call(elem, cur_attr_name, res)
          else
            actions[:attr_error] && actions[:attr_error].call(elem, cur_attr_name, res)
          end
          actions[:after] && actions[:after].call(elem, res)
        end
      end
  end
```

# ダメでしょ
- 車輪の再発明
- I/Fの前提が多すぎるオレオレ仕様。ほぼRails専用
- keyupイベントで検知しているためタッチデバイス非対応
- 以下、対応が必要
    - リクエスト・レスポンスのフォーマット
    - フォームのinputの自由化