はじめに
- 今回はParsleyという、フロントエンド側のフォームバリデーションライブラリを快適に使う方法をつらつらと書いていきます。
- ベースはRailsでやっていますが、知識を応用すればどのWAF内でも使えるような気がします。
利用するライブラリ
- Parsley
- ActiveHash(セレクトボックス作りに重宝)
- Cocoon(nested_attributesのフォーム作りに)
- 今回セレクトボックスを使う時にActiveHashと、
nested_attributes
のフォームを作るのにCocoon
を使用するので、この二つの知識をもっておかないとわかりにくいかもしれません。 - 凄まじく使えるGemなので、この機会にぜひ使って見てください!
- あとBootStrap4のクラスが多々登場しますので、そちらの方もお願いいたします。
準備
インストール
- 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));
});
});
ウィジェットフォーム
おわりに
- 自分でゼロから実装するよりも遥かに簡単にフロントバリデーションができてしまうのはとても嬉しいですね!