来年で平成が終わってしまう。おそらく日本中のシステムエンジニアは改元に伴う仕様変更に追われていることだと思うのだが、自分もそのうちの一人だ。
とはいえ大半のシステムは新元号が追加されても大丈夫なように、あらかじめ外部定義などで汎用性をもたせているだろうと思うのだが、大昔につくられた(まさか平成が終わるまで使われるとは思っていなかった)システムはハードコーディングされていて汎用性のかけらもない作りになっていたりする。
今回は素のJavaScriptでできた大昔のWeb画面の部品を「jQuery/AngularJS/Angularのいずれかに置き換えるならこうする」という部品化のノウハウを備忘録の意味を兼ねて記事としてまとめることにした。
元号を選択するプルダウン部品
今回は元号をプルダウンで選択するとテキストボックスの値が連動するような部品を例題とする。素のJavaScriptで実装するとこんな感じになる部品だ。
<span>
<input type="text" onchange="this.nextElementSibling.value = this.value">
<select onchange="this.previousElementSibling.value = this.value">
<option value="1">明治</option>
<option value="2">大正</option>
<option value="3">昭和</option>
<option value="4">平成</option>
</select>
</span>
だいぶ大雑把に実装してしまったがイメージは伝わるだろうか。
(本来ならばテキストボックスにmaxlengthなどの入力制限を実装すべきだが今回は割愛)
- プルダウンを変更する → テキストボックスが連動する
- テキストボックスを変更する → プルダウンが連動する
jQueryで部品化
先ほどのJavaScriptの実装はプルダウンの項目がハードコーディングのため
- 部品を複製するには部品のコード全部をコピペで貼りつけていくことになる
- 来年からの新元号が加わると複製した部品全部を変更しないといけない
- 元号以外のプルダウンに応用することができない(
<option>
部分を作り直し)
のような問題点がある。このままでは汎用的な部品とは呼べないので、まずはjQueryを使って部品化してみる。
部品を使う側の実装
<span id="gengoChoice"></span>
<script>
$("#gengoChoice").choice({
"1": "明治",
"2": "大正",
"3": "昭和",
"4": "平成"
});
$("#gengoChoice").change(function(){
console.log($(this).data("controller").selectedValue);
});
</script>
任意の<span>
タグを記述し$(elem).choice()
を呼び出すことで部品(以降choiceと命名)を使うことができるようにする。jQueryUIの部品もこの方法で利用することが多い。引数にプルダウンの選択肢となる元号とコードを定義した連想配列を指定することで汎用的に利用できるようになる。
また、choice部品に対する操作を制限するため、専用のコントローラを利用する。choice部品で選択されたデータ(モデル)は<select>
や<input>
のDOMから取得するのではなく、コントローラから取得することを厳守すれば、仕様変更にも対応しやすくなるはずだ。
部品を作る側の実装
JavaScriptだけでも実装できるが、今はTypeScriptというすばらしいAltJSが存在するので迷いなく利用することにする。
namespace Choice {
export interface CodeMap {
[code: string]: string;
}
export class Controller {
private value: string = null;
private $text: JQuery;
private $select: JQuery;
constructor(private $choice: JQuery, private codeList: CodeMap) {
this.$text = $("<input type=text>");
this.$select = $("<select>");
this.$choice.append(this.$text).append(this.$select);
Object.keys(codeList).forEach((code, index, array) => {
if (index == 0) {
this.value = code;
}
this.$select.append(`<option value=${code}>${codeList[code]}</option>`)
});
this.$text.change(() => {
this.selectedValue = <string>this.$text.val();
});
this.$select.change(() => {
this.selectedValue = <string>this.$select.val();
});
}
get selectedValue() {
return this.value;
}
set selectedValue(code: string) {
if (this.codeList[code] === undefined) {
this.value = "";
this.$select.val("");
this.$text.val("");
}
else {
this.value = code;
this.$select.val(code);
this.$text.val(code);
}
}
}
// jQueryのカスタマイズ
(<any>$.fn).choice = function (codeList: CodeMap) {
let $choice = $(this);
//コントローラを生成して紐付ける
$choice.data("controller", new Controller($choice, codeList));
};
}
$.fn
に関数リテラルchoice
を設定することで、jQueryをカスタマイズする。これにより$(elem).choice()
が利用できるようになる。だが、Controller
クラスのコンストラクタでDOMを生成してバインドするようにしているため、この実装だとコントローラにビューの要素が混在する実装になっている。プログラマーとデザイナーが兼任でない現場では、この実装は避けたほうがいいだろう。
この実装はViewの変更に弱い
多くの場合はhtmlのレイアウトを管理するのはデザイナーでありプログラマーではない。choice部品は<span>
直下の<input>
と<select><option>
の組み合わせで構成される単純な部品だが、これらのタグの構成はデザイナーが管理し、プログラマーは知らなくても良い状態となっていることが理想だろう。
だが、jQueryの実装例(choice.ts)では、プログラマーが管理するTypeScriptのコード中に<input>
や<select><option>
の記述が紛れてしまっている。これでは見た目(View)に対する仕様変更が加わった時に、本来修正しなくても良いコードを修正することなりかねない。これらHTMLタグの記述は別のファイルに分離し、デザイナーにより管理されるべきである。
jQueryでこれを実現するには$(elem).html()
を駆使したり、templateプラグインを利用するなどの手段があるのだが、記事が長くなりすぎたので今回は割愛。ここらでAngularJSの話題に切り替えることにする。
AngularJSで部品化
AngularJSはシングルページアプリケーションを実装するためGoogleによって作られたフレームワークだ。ModelとViewを紐付けるバインド機能や、DOMに特殊な振る舞いを定義するディレクティブという機能が備わっている。AngularJSでは、このディレクティブを利用して部品化するのが妥当な方法だろう。
部品を使う側の実装
AngularJSのディレクティブを使うと、<choice>
という新たな機能を持つタグを定義できるようになる。また、タグとしてだけでなく、<span choice>
のように、新たな属性を作ることもできる。
今回は新たなタグ<choice>
として実装し、choiceタグの属性にプルダウンの選択肢を指定することにする。
<body ng-app="myApp">
<div ng-controller="mainContents">
選択された元号コード:{{selected_gengo_code}}
<choice codelist='{"1":"明治","2":"大正","3":"昭和","4":"平成"};' selected-value="selected_gengo_code"></choice>
</div>
</body>
codelist
属性にプルダウンの選択肢となる元号のコードリストを記述しているが、親スコープとなるmainContentsコントローラから渡すこともできる。
また、choiceによって選択された元号のコードはselected-value
属性で指定したスコープに格納される。
そのことがわかるように選択された元号コード:{{selected_gengo_code}}
で表示するようにしている。
部品を作る側の実装
Viewの実装
jQueryの例では割愛したが、AngularJSにはテンプレートという機能がデフォルトで存在するため、容易にViewを別ファイル化できる。
<input type="text" ng-model="selectedValue" />
<select ng-model="selectedValue" ng-options="key as val for (key, val) in codelist()"></select>
一見、<option>
が存在しないように思われるが、ng-options
という属性がoptionタグを生成するディレクティプになっているため、直接<option>
を記述する必要はない。その代わりデザイナーは"key as val for (key, val) in codelist()"
というロジックを理解しなければならない。
余談だが、自分がはじめてAngularJSをさわったとき、この(デザイナーが内部データ構造を意識しなければならなくなる)記述に納得いかなかった。もともとHTMLのタグにはonclick
など処理を記述できる属性は存在しているが、その内容はonclick="clickAction(this);"
のように1関数を呼び出すだけの簡単な記述とするのが良いと思ってきた。だが、AngularJSでは1関数呼び出しだけでなく、ループ処理や今回のようなcodelist()
で返されるデータの構造を意識しなければならなくなっており、この点はデザイナーとプログラマーのすみ分けが曖昧になってしまうポイントではないかと思う。(ちなみにAngularではこのデータ構造は配列だけに制限されている)
ディレクティブの実装
namespace Choice {
/** テキスト・プルダウン連動部品 */
export class ChoiceDirective implements ng.IDirective {
restrict = "E"; //choiceタグとして定義
templateUrl = "choice.html"; //Viewを別ファイルに
scope = {
codelist: "&", //親スコープから連想配列を受け取る
selectedValue: "=?" //親スコープに結果をバインドする
};
}
myApp.directive("choice", () => new ChoiceDirective());
}
ディレクティブ本体の実装はこうなる。ややこしいのはscopeの扱いだが、ひとまずはディレクティブ内部にデータを持ち込みたい(入力)ときは@
または&
、データを返したい(出力)ときは=
と覚えるようにしている。詳細については公式ドキュメントを参照してほしい。
Angularで部品化
Angularは、AngularJSのvar2以降の後継フレームワークで、「コンポーネント指向」をより強く意識した非互換の別フレームワークである。そのため、AngularJSではざっくりと「ディレクティブ」として扱っていた部品も、Angularでは「テンプレート付ディレクティブ(=コンポーネント)」「構造ディレクティブ」「属性ディレクティブ」の3つに詳細化された。
choice部品は、この中の「テンプレート付ディレクティブ」として実装することになる。
部品を使う側の実装
<div>
{{gengoChoice.selectedValue}}
<app-choice #gengoChoice [codeListMap]='{"1":"明治","2":"大正","3":"昭和","4":"平成"}'></app-choice>
</div>
AngularJSとは記述方法が若干異なるが、なんとなく理解できるだろう。特筆すべきなのは、AngularではAngularJSにはなかった「テンプレート参照変数」#gengoChoice
を使うことができる点だ。これを使うことで使う側は容易にchoiceの値を知ることができる。
部品を作る側の実装
Viewの実装
<input type="text" [(ngModel)]="selectedValue">
<select [(ngModel)]="selectedValue">
<option *ngFor="let code of codeList" [value]="code">{{codeListMap[code]}}</option>
</select>
AgnularJSではng-options
を使い<option>
を生成していたが、Angularでは*ngFor
で生成することになる。*ngFor
に指定されているcodeList
は、配列でなければならないため、コンポーネントの実装で連想配列のキーだけをcodeList
に格納している。つまりAngularではcodeList
のデータ構造が配列だけとなったことでデザイナが意識しなければならない内容が軽減されている。
コンポーネントの実装
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
import { templateJitUrl } from '@angular/compiler';
import { Validators } from '@angular/forms';
@Component({
selector: 'app-choice',
templateUrl: './choice.component.html',
styleUrls: ['./choice.component.css']
})
export class ChoiceComponent implements OnInit {
@Input() codeListMap: { [code: string]: string } = null;
selectedValue: string;
codeList = []; //コード一覧
ngOnInit() {
this.codeList = Object.keys(this.codeListMap);
}
}
AngularJSでややこしかったscopeの扱い(データのバインド)が、Angularでは@Input()
と記載することになっており、より直感的になっていることがわかる。また、使う側で「テンプレート参照変数」を使えば、selectedValue
にアクセスすることも可能なため、作る側の実装で意識することはない。
まとめ
今回はプルダウンで元号を選択する部品をjQuery、AngularJS、Angularそれぞれで部品化する例を示してみた。
比べてみても、やはり新しいフレームワークなだけあって、Angularはコードがシンプルでわかりやすいのではないだろうか。まぁ改元を意識していない古いシステムでAngularが使われていることは絶対ないと思うので、使う機会はないだろうけど。。。
実際のところ、素のJavaScriptでハードコーディングされていたら、せいぜいjQueryを使って部品化する程度にとどめるのが現実的な解ではないかと思う。そう考えるとやはりjQueryは気楽に使える優秀なライブラリだ。