2013 07 09

何かが違う

夕陽

夕陽。 オレンジ味のアイスクリームみたいで、何だか美味しそう。

+

新人、といっても2年目ぐらいなのだが、もう平成生まれなんだね。 何かの拍子に

「そろそろ平成生まれが入ってくるんだね」

「あ、僕は平成ですよ」

なんて、ちょっと驚いた。 そうか、もう平成だったのか。 そりゃ俺も歳を取るはずだよな。

その新人が画面の制御で悩んでいるのを横目に見ていて、先日の打ち合わせのときに後ろから聞こえてきた会話を思い出した。

「入力欄がちょっとあるだけでこんなに面倒だとは思いませんでしたよ」

「こっちのなんて入力欄が50個ぐらいあるんだけど」

「あー、それはちょっと、私には無理です。 そんな一杯ある奴のどれかが変わったとか、どうやってチェックするんですか?」

「これ、変更前の値をhiddenで持ってるんだよ」

「あー、やっぱりそうですか。 やっぱ名前に before とか after とか付いてるんですか?」

「そう。 期日前価格とかあるから、before_before とか after_before とか、訳が判らなくなってるよ」

「うわ、カオスですね」

若手と先輩が、待ち時間に他所のシステムのことを話していたようだった。

同じようなこと、つまり画面表示後にどれかの値が変更されたかどうかを判定する場合、俺だったらどうするだろうか。 jQuery を使って form に change をバインドするのが手っ取り早そうだけど、多分そうじゃないんだよな。 hidden でそれぞれの値を持っておいて一つ一つ変更が無いか確認するのは、変更して変更して結局元に戻った場合は変更しなかったとして扱いたいのだろうから。

しかし、だからと言って一つ一つの入力要素の全てに hidden を対応させるのはどうなんだろう。 taglib で自動生成してくれるならともかく、自分で一々 hidden を書くのは面倒だし、いかにも力技で芸が無い感じだし。

ということで、JavaScript が使えることを前提に、自分だったらどうするかを考えてみた。

最初に思いついたのは、defaultX シリーズを使うこと。 入力要素は、値を取得するためのプロパティ x に対して、defaultX として初期値を持っている。

要素 入力値取得 初期値取得
input text="text" value defaultValue
input text="checkbox" checked defaultChecked
input text="radio" checked defaultChecked
textarea value defaultValue
option selected defaultSelected

なので、全ての入力要素でこれらを比較し、どれかに違いがあれば変更があったとするのだな。 実装はこんな感じ。

function isChanged( form ) { return Array.prototype.some.call( form.getElementsByTagName( "input" ), function ( obj ) { switch ( obj.type ) { case "text" : return obj.value !== obj.defaultValue; case "checkbox" : case "radio" : return obj.checked !== obj.defaultChecked; default : return false; } } ) || Array.prototype.some.call( form.getElementsByTagName( "option" ), function ( obj ) { return obj.selected !== obj.defaultSelected; } ) || Array.prototype.some.call( form.getElementsByTagName( "textarea" ), function ( obj ) { return obj.value !== obj.defaultValue; } ); }

割と簡単だな。 html5だと、input で指定できる type がいくつか追加されているのだが、どれも text と同じ扱いでいけるだろう。 たぶん。 で、変更の有無を調べたい form の id を "form1" として、上記はこんな感じで使う。

if ( isChanged( document.getElementById( "form1 " ) ) ) { alert( "何かを変えている" ); }

画面の構成には全く依存していないのがポイント。 入力欄の数や種類がどれだけあっても、何かを変えたかどうかのチェックならこれだけ。 ちょっと拡張すれば、具体的に何がどう変わったかも取得できる。

ほとんどはこれでいけると思うが、対応できないのは DOM をクライアント側で弄る場合。 例えば、追加ボタンをクリックして入力欄を次々と追加するとか、上位の選択に応じて下位の選択肢を構築するとか。

入力欄を追加しても何も入力していなければ変更無しとするならいい。 でも、追加操作そのものを変更としたいなら、各要素で入力値と初期値を比べるのだけではなく、その要素そのものが最初にあったのかどうかも調べる必要がある。 この場合は、やっぱり初期状態をどこかで保持しておいて、それと今とを比較して構成も値も一致するかどうかを調べるしかなさそうだな。

ということで、初期状態を保持するパターン。

function FormChecker( form ) { var initials = getValues( form ); this.isChanged = function () { var currents = getValues( form ); return initials.length !== currents.length || initials.some( function ( initialValue, i ) { return initialValue !== currents[ i ]; } ); }; function getValues( form ) { var values = []; Array.prototype.forEach.call( form.getElementsByTagName( "input" ), function ( obj ) { switch ( obj.type ) { case "text" : values.push( obj.value ); break; case "checkbox" : case "radio" : values.push( obj.checked ); break; } } ); Array.prototype.forEach.call( form.getElementsByTagName( "option" ), function ( obj ) { values.push( obj.selected ); } ); Array.prototype.forEach.call( form.getElementsByTagName( "textarea" ), function ( obj ) { values.push( obj.value ); } ); return values; } }

コード量5割増し。 こちらは初期値を予め取得しておく必要があるので、使い方もちょっと手間がかかる。

window.onload = function() { var fc = new FormChecker( document.getElementById( "form1" ) ); document.getElementById( "button1" ).onclick = function() { if ( fc.isChanged() ) { alert( "何かを変えている" ); } } }

こんな感じで、画面のロードが完了したタイミング、厳密には DOM tree の構築が完了したタイミング以降でチェック用のオブジェクトを生成しておく。 このオブジェクトが、自分が生成されたタイミングの値を初期値として取得・保持する。 で、確認したいときに isChanged メソッドを呼ぶと、そのタイミングの値を取得して初期値と比較し、違いの有無を返す。 サンプルでは、button1 ボタンをクリックすると変更の有無をダイアログで表示するようにしている。

きっとこの辺りが現実的なところだろうと、ちょっと妥協して入力値の数と順番を値だけで比較している。 その値が入力欄のどのタイプで得られたかについては無視。 入力欄のタイプとは、例えば input type="text" なのか textarea なのかということ。 いくら DOM を弄るとはいえ、ここまで弄ることを想定しなくてもいいだろう。

しかし、some とか forEach があると記述が楽でいいね。 あとは、引数や DOM の要素列とかの擬似配列が Array としてそのまま扱えるようになれば更に嬉しいのだが、そんな日は来るのだろうか。