LoginSignup
6
7

More than 5 years have passed since last update.

Parsleyを使ったバリデーションフォームテクニック

Last updated at Posted at 2018-10-15

はじめに

  • 今回はParsleyという、フロントエンド側のフォームバリデーションライブラリを快適に使う方法をつらつらと書いていきます。
  • ベースはRailsでやっていますが、知識を応用すればどのWAF内でも使えるような気がします。

利用するライブラリ

準備

インストール

  • Railsのvendor/assets/parsleyディレクトリに、ダウンロードしてきたParsleyライブラリを配置します。
vendor/assets/parsley/
├── parsley-ja.js(ja.jsから改名する)
├── parsley.css
├── parsley.min.js
└── parsley.min.js.map
// app/assets/stylesheets/application.scss
@import "parsley";
@import "custom/*";
// app/assets/javascripts/application.js
//= require parsley.min
//= require parsley-ja
//= require_tree ./custom
  • Gemの方もインストールします。
gem 'active_hash'
gem 'cocoon'

Parsley大元の設定

  • app/assets/javascripts/custom配下に設定する。
  • 今後記述しないが、parsleyクラスのついたformタグに対してparsley処理が走るように設定するため、parsleyを使う場合は必ずクラス付与を忘れないこと。
# app/assets/javascripts/custom/parsley.coffee

$ ->
  $('form.parsley').parsley({
    # レンダーされるエラーのul,liタグの指定
    errorsWrapper: '<ul class="parsley-error-list"></ul>',
    errorTemplate: '<li class="parsley-error-item"></li>',
    # parsleyを適用するフォームの種類の指定(デフォルトだとhiddenが使えないので指定する)
    excluded: 'input[type=button],' +
              'input[type=submit],' +
              'input[type=reset],' +
              '[disabled]',
    inputs: 'input, textarea, select, :hidden',
    # 入力に変更があった場合にバリデーションを走らせる
    trigger: 'change'
  })

ActiveHashによる区分値の設定

  • 今回はセレクトボックスをActiveHashというライブラリを使って作成する。
  • 準備の仕方は簡単で、Gemをインストールした状態で以下の二つのファイルを配置すれば、あとは簡単に区分値を取得できる。
# app/models/item.rb
class Item < ActiveYaml::Base
  include ActiveHash::Enum

  set_root_path 'config/master'
  set_filename name.underscore
end
# config/master/item.yml
- id: 1
  name: アイテム1
- id: 2
  name: アイテム2
- id: 3
  name: アイテム3
# railsコンソール
> Item.all
=> [#<Item:0x0000563949ca9878 @attributes={:id=>1, :name=>"アイテム1"}>,
 #<Item:0x0000563949ca94b8 @attributes={:id=>2, :name=>"アイテム2"}>,
 #<Item:0x0000563949ca9170 @attributes={:id=>3, :name=>"アイテム3"}>]

基本形

入力必須

  • required: true属性を指定するだけ。
/ テキストフィールド
= f.text_field :title, class: 'form-control', required: true

/ セレクトボックス
= f.collection_select :item_id, Item.all, :id, :name, { include_blank: '未選択' }, class: 'form-control', required: true

完全一致

  • password確認フォームに同じ文字列が入っているかの確認とかに使う。
= f.password_field :password, id: 'password', class: 'form-control', required: true
/ 上のフォームと完全一致じゃないとエラーになる
= f.password_field :password_confirmation, class: 'form-control', required: true, data: { 'parsley-equalto': '#password' }

パターン指定

  • こちらもパスワードのフォーマットの指定に使う。
  • パスワードのパターンを指示する正規表現はRailsConfigとかを使って、アプリ全体で共有できるようにしておくといい
  • エラーメッセージもオリジナルのを指定できる。
# config/settings.yml
format:
  password: /^(?=.*?[a-zA-Z])(?=.*?\d)[0-9a-zA-Z]{8,128}$/
= f.password_field :password, placeholder: '※英数含む8文字以上128文字以下', class: 'form-control', required: true, pattern: Settings.format.password, data: { 'parsley-error-message': "パスワードは8文字以上の英数にしてください" }
  • ちなみにこちらのサイトによれば、ひらがな・カタカナによるバリデーションもできるらしいです。(ウェブマスターさんありがとうございます。)

発展系

選択するか、チェックボックスを入れるか

  • よく見るかはわからないが、必須入力の選択肢を特定条件下で向こうにするしくみ。
  • チェックボックスのchengeトリガーで、フォームのdisabled属性を変化させ、disabledになった時はバリデーションをリセットするという手順を踏む。

.form-group
  = f.label 'アイテム'
  = f.collection_select :item_id, Item.all, :id, :name, { include_blank: '未選択' }, class: 'form-control main-input', required: true
  .form-check.mt-3
    label.form-check-label
      = f.check_box :item_id_blank, class: 'form-check-input later-checkbox' 
      | 後で決める
= f.submit 'Submit', class: 'btn btn-success w-100'
// 読み込むJSファイル
$(function() {
  var onChangeLaterCheck = function(checkbox) {
    $mainInput = $(checkbox).closest('.form-group').find('.main-input')
    if ($(checkbox).is(':checked')) {
      $mainInput.prop('disabled', true);
      $mainInput.val('');
      // formをdisabledにするので、エラー文が出ていた場合には削除する。
      $mainInput.parsley().reset();
    } else {
      $mainInput.prop('disabled', false);
    }
  };

  $('.later-checkbox').on('change', function() {
    onChangeLaterCheck(this);
  });

  $('.later-checkbox').on('change', function() {
    onChangeLaterCheck(this);
  });
});

個数判定(一つ以上入力してください的な)

  • nested_attributesなどの一対多の子要素を作成するフォームで、最低〇〇個以上入力して欲しい時に使える。

/ app/views/sample/index.html.slim

/ フォームタグは一例なので適宜書き換える
= form_with(model: User.new, url: sample_path, class: 'parsley', local: true, method: :get) do |f|
  .form-group
    / cocoonライブラリの子要素追加ボタン。敢えて表示せずに裏から叩く
    = link_to_add_association '', f, :items, class: 'd-none', partial: 'item_fields', data: { association_insertion_node: "#itemList", association_inser
tion_method: "append" }                                            
    #itemList.nested-element-list
      / 要素数をhiddenフィールドで保持して、min属性でいくつ以上必要か指定する。                        
      = hidden_field_tag :element_count, 0, id: 'itemElementCount', class: 'element-count', min: 1, data: { 'parsley-min-message': 'アイテムを一つ以上選択してください' }
      = f.fields_for :items do |item|
        = render 'item_fields', f: item
    .nested-element-entry                              
      .form-group.row                                                                     
        .col-md-10                                                
          = f.collection_select :item_id, Item.all, :id, :name, { include_blank: '未選択' }, class: 'form-control entry-input'
  = f.submit 'Submit', class: 'btn btn-success w-100'
/ app/views/sample/_item_fields.html.slim

.nested-fields
  = f.hidden_field :id
  .form-group.row
    .col-md-10
      = f.collection_select :item_id, Item.all, :id, :name, { include_blank: '未選択' }, class: 'form-control nested-main-input', required: true
    .col-md-2
      = link_to_remove_association '削除', f, class: 'btn btn-danger'
// 読み込むJSファイル
$(function() {
  // フォーム入力時に要素を一つ追加する(選択式の場合のみ)
  $('.nested-element-entry').on('change', function() {
    if ($(this).find('select').val()) {
      $(this).closest('.form-group').find('a.add_fields').click();
    }
  });

  // 要素数カウント
  elementCountValidation = function($targetList, validate = true) {
    if (!$targetList.find('.element-count').length) { return }

    // visible属性がないと正確な数が取れない
    var count = $targetList.find('.nested-fields:visible').length;
    $targetList.find('.element-count').val(count);
    if (validate) {
      $targetList.find('.element-count').parsley().validate();
    }
  }

  // 要素追加時には、フォームの内容を反映して、フォームを空にする
  $('.nested-element-list').on('cocoon:after-insert', function(e, added) {
    $targetList = $(e.currentTarget);

    // これを指定しないと、新しく追加された子要素のselectでは、parsleyのchangeトリガーが起動しなくなってしまう。
    $('.parsley').parsley().refresh();

    // Entryフォームの値を追加した要素に移動する
    $entryInput = $targetList.siblings('.nested-element-entry').find('.entry-input');
    $(added).find('.nested-main-input').val($entryInput.val());
    $entryInput.val('');

    // 要素数バリデーション
    elementCountValidation($targetList);
  }).on('cocoon:after-remove', function(e) {
    $targetList = $(e.currentTarget);

    // 要素数バリデーション
    elementCountValidation($targetList);
  });

  // 要素数だけ数えてバリデーションしない
  $('.nested-element-list').each(function() {
    elementCountValidation($(this), false);
  });
});

重複判定(同じ要素がすでに存在しています)

  • nested_attributesのフォームとかで、同じタイトルが入力できないようにする。
  • 一個前に出てきた個数カウント入力欄に対してparsley-equaltoを指定することで、「要素数」 = 「ユニークな要素数」が成り立たない場合にエラーを出すようにする。
  • 先ほどの個数判定と並列で使うことも可能。

/ 要素数を保持
= hidden_field_tag :element_count, 0, id: 'itemElementCount', class: 'element-count'
/ こちらのhiddenを追加
= hidden_field_tag :unique_count, 0, class: 'unique-element-count', data: { 'parsley-equalto': '#itemElementCount', 'parsley-equalto-message': '重複している項目があります' }
$(function() {
  // フォーム入力時に要素を一つ追加する(選択式の場合のみ)
  $('.nested-element-entry').on('change', function() {
    if ($(this).find('select').val()) {    
      $(this).closest('.form-group').find('a.add_fields').click();
    }
  });

  // 要素数カウント
  elementCount = function($targetList) {
    if (!$targetList.find('.element-count').length) { return }

    var count = $targetList.find('.nested-fields:visible').length;
    $targetList.find('.element-count').val(count);
  }

  // ユニークバリデーション
  uniqueElementValidation = function($element) {
    if (!$targetList.find('.unique-element-count').length) { return }

    var values = $element.find('.nested-fields:visible').map(function() {
      return $(this).find('.nested-main-input').val();
    }).toArray();
    var unique_values = values.filter((val, idx, arr) => {
      return arr.indexOf(val) === idx;
    });
    $element.find('.unique-element-count').val(unique_values.length);
    $element.find('.unique-element-count').parsley().validate();
  }

  // 要素に変更があった場合にユニークバリデーションを走らせる
  $('.nested-element-list').on('change', '.nested-fields:visible .nested-main-input', function() {
    $targetList = $(this).closest('.nested-element-list');
    uniqueElementValidation($targetList);
  });

  // 要素追加時には、フォームの内容を反映して、フォームを空にする
  $('.nested-element-list').on('cocoon:after-insert', function(e, added) {
    $targetList = $(e.currentTarget);

    // これを指定しないと、新しく追加された子要素のselectでは、parsleyのchangeトリガーが起動しなくなってしまう。
    $('.parsley').parsley().refresh();

    // フォームの内容を最後の要素に移動する
    $entryInput = $targetList.siblings('.nested-element-entry').find('.entry-input');
    $(added).find('.nested-main-input').val($entryInput.val());
    $entryInput.val('');

    // 要素数カウント
    elementCount($targetList);
    // ユニーク要素カウント  
    uniqueElementValidation($targetList);
  }).on('cocoon:after-remove', function(e) {
    $targetList = $(e.currentTarget);

    // 要素数カウント
    elementCount($targetList);
    // ユニーク要素カウント 
    uniqueElementValidation($targetList);
  });

  // 一番最初に要素のカウントとユニークカウントを実行する。
  $('.nested-element-list').each(function() {
    elementCount($(this));
    uniqueElementValidation($(this));
  });
});

ウィジェットフォーム

おわりに

  • 自分でゼロから実装するよりも遥かに簡単にフロントバリデーションができてしまうのはとても嬉しいですね!
6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7