selectExtensionsを使ってみる

この記事はKnockoutJSアドベントカレンダーの2日目です。
1日目は@tan_go238さんのKnockoutJSの紹介でした。

KnockoutJSではoptionバインディングでselectタグ内のoptionを生成することができるのですが、optGroupを含めた物を生成することは出来ません。というわけで、selectExtensionsとカスタムバインディングを使って自作しようというのが今回の記事です。

具体的にはこんな感じで使えるバインディングを作成します。

<select data-bind="optgroup: {options:categoryArray, optGroups:categoryGroupArray, optionsText:'name', optionsValue:'id' }, selectedOptions: selectedCategoryArray" multiple="true" ></select>

optionsにはoptGroupには含めないoptionの配列、optGroupsにはoptGroupのラベルと子のoption配列を持つオブジェクトを指定します。optionsTextとoptionsValueについては標準のoptionsバインディングと同じ役割のものです。


出来上がったHaxeのコードはこちらになります。
なお、コンパイルする場合knockout.hxが必要になるのでhaxelibでインストールしておいてください。

class BindingHandlers {

    public static var OPTGROUP = {
    init: function(element:js.html.Node, valueAccessor:Void -> Dynamic, allBindingsAccessor:Void -> Dynamic, viewModel:Dynamic, bindingContext:BindingContext):Void {
    },
    update: function(element:js.html.Node, valueAccessor:Void -> Dynamic, allBindingsAccessor:Void -> Dynamic, viewModel:Dynamic, bindingContext:BindingContext):Void {
        var value:Dynamic = Utils.unwrapObservable(valueAccessor());

        var options:Array<Dynamic> = Utils.unwrapObservable(value.options);
        var optionsText:String = value.optionsText;
        var optionsValue:String = value.optionsValue;
        var optGroups:Array<Dynamic> = Utils.unwrapObservable(value.optGroups);

        Utils.setHtml(element, "");
        var nodes = createOption(options, optionsText, optionsValue);
        for(o in nodes){
            element.appendChild(o);
        }

        for (g in optGroups) {
            var label = g.label;
            var optgroup = Browser.document.createElement("optgroup");
            optgroup.setAttribute("label", label);
            
            var options = createOption(g.options, optionsText, optionsValue);
            for( o in options){
                optgroup.appendChild(o);
            }
            element.appendChild(optgroup);
        }
    }
    };

    private static function createOption(options:Array<Dynamic>, optionsText:String, optionsValue:String):Array<Element> {
        var array = [];
        for (o in options) {
            var text = if (optionsText == null) {
                o;
            } else {
                Reflect.field(o, optionsText);
            }

            var value = if (optionsValue == null) {
                o;
            } else {
                Reflect.field(o, optionsValue);
            }
            
            var opt = Browser.document.createElement("option");
            setText(opt, text);
            Knockout.selectExtensions.writeValue(opt, value);
            array.push(opt);
        }
        return array;
    }

    private static function setText(element:js.html.Element, text:String) {
        if (element.textContent != null) {
            element.textContent = text;
        } else {
            element.innerText = text;
        }
    }
}
Knockout.bindingHandlers["optgroup"] = BindingHandlers.OPTGROUP;

DynamicとReflectを結構使ってるためHaxeのありがたみがあまりないのはさておき、ここで注目してもらいたいのはcreateOptionメソッド内でoptionタグのvalue属性を設定するのにKnockout.selectExtensionsを使用している点です。
そもそも、optionタグのvalue属性は数値型であっても文字列型として設定されてしまうという問題があります。そのため、selectedOptionsに設定したObservableArrayには常に文字列型の値が渡ってくるので場合によっては、その後の値の比較などで問題が発生することがあります。
そこで、このselectExtensionsのwriteValueを使用すると、単にoptionタグのvalue属性を設定するだけでなく同時にKnockoutJS内部に値を保持してくれるため、selectedOptionsには元の型で値を渡すことができるようになるというわけですね。

ちなみにknockout.jsのソース内でも以下のようにコメントされています。

Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values
are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values
that are arbitrary objects.

今回の本題ではないのでカスタムバインディングHaxe、knockout.hxの説明を省略していますが、selectExtensionsの紹介でした。

明日は前日に続き @tan_go238 さんの「Components を使ってみる」です。