DTPab

DTPにまつわるあれこれ

ScriptUIでグループをまたいだラジオボタンの処理

元ネタ

id:haraguai_is_bad さんがブログで面白いスクリプトを公開されています。

haraguai-is-bad.hatenablog.com

ここで9つのラジオボタンを使った処理をScriptUIで実装されています。
紹介されているスクリプトではラジオボタン1つにつき2つの関数を実行されていて、確かに正攻法ではそうなるなーと思ったところです。

なので記事のコメントにも書かせてもらったのですが、ラジオボタンが何個増えても大丈夫なような、そんな修正に強い方法を書いてみました。
はてなブログはコメント欄が小さすぎて長文を書くには適していないので、自分のブログに記事を書いてしまうという不躾なことをしてます。

コード

まずは完成品のサンプルコード。

var w = new Window("dialog");
var g = w.add("group");
g.orientation = "column";
var g1 = g.add("group");
var g2 = g.add("group");
var g3 = g.add("group");
var rb = [
    g1.add("radioButton"),
    g1.add("radioButton"),
    g1.add("radioButton"),
    g2.add("radioButton"),
    g2.add("radioButton"),
    g2.add("radioButton"),
    g3.add("radioButton"),
    g3.add("radioButton"),
    g3.add("radioButton")
];
for (var i=0; i<rb.length; i++) {
    rb[i].onClick = function() {
        var rbIndex = i;
        return function() {
            for (var k=0; k<rb.length; k++) {
                if (rbIndex === k) {
                    continue;
                }
                rb[k].value = false;
            }
        };
    }();
}
w.add("button", undefined, "OK").onClick = function(){
    w.close(2);
};
w.show();

このようにしておくと、仮にラジオボタンが増えることになっても配列rbの中にラジオボタンオブジェクトを追加するだけで済みます。

開発モノローグ

ここからは着想から完成までをモノローグにてお送りします。

スキル発動

まずボタンが増えてもonClickイベントをわざわざ追加しないよう、全部for文で処理してぇな……と、スキル「面倒臭がる」を発動。
なのでまずは面倒臭がります。

for文で処理しよ

for文で回せば全部同じonClickイベントを設定できるんじゃない?
じゃぁラジオボタンを全部配列に入れたろ。 ということで配列rbを用意して、そこにラジオボタンを定義しました。

var rb = [
    g1.add("radioButton"),
    g1.add("radioButton"),
    g1.add("radioButton"),
    g2.add("radioButton"),
    g2.add("radioButton"),
    g2.add("radioButton"),
    g3.add("radioButton"),
    g3.add("radioButton"),
    g3.add("radioButton")
];

for文でonClickメソッドを定義

よーしここからだ! とりあえず実直にonClickメソッドを登録していくかなー。

チャレンジ その1

for (var i=0; i<rb.length; i++) {
    rb[i].onClick = function(i) {
        for (var k=0; k<rb.length; k++) {
            if (i === k) {
                continue;
            }
            rb[k].value = false;
        };
    };
}

f:id:uske_S:20190809142811g:plain

だめやんけ!!!

原因究明

alert(rb[0].onClick);で定義されている関数を確認しよう。 f:id:uske_S:20190809143238p:plain

ですよねー。引数iの値が適切に渡されてないからiがずっとundefiendじゃん。だめじゃん。

チャレンジ その2

てことはiを定数にできたらいいってことじゃないか??

for (var i=0; i<rb.length; i++) {
    var rbIndex = i;
    rb[i].onClick = function(rbIndex) {
        for (var k=0; k<rb.length; k++) {
            if (rbIndex === k) {
                continue;
            }
            rb[k].value = false;
        };
    };
}

f:id:uske_S:20190809142811g:plain

だめやんけ!!!(3分ぶり2回目)

原因究明

f:id:uske_S:20190809143554p:plain

何も変わってないじゃん。なんなの?バカなの?orz

チャレンジ その3

関数の中に値を保持したいわけだよな。ということは関数の中で定義しないとだめだな。 ひとまずこうして関数のなかでrbIndexを定義。

for (var i=0; i<rb.length; i++) {
    rb[i].onClick = function() {
        var rbIndex = i;  //ここに移動
        for (var k=0; k<rb.length; k++) {
            if (rbIndex === k) {
                continue;
            }
            rb[k].value = false;
        };
    };
}

でもこれではonClickメソッドの中の変数iが参照されない。そしたらもう一段階下のスコープに置こう。

ん、ということはクロージャーにすればいいのかな?

for (var i=0; i<rb.length; i++) {
    rb[i].onClick = function() {
        var rbIndex = i;
        return function(){
            for (var k=0; k<rb.length; k++) {
                if (rbIndex === k) {
                    continue;
                }
                rb[k].value = false;
            };
        };
    }();
}

これでどうだ???

f:id:uske_S:20190809144443g:plain

できたや〜〜〜〜ん(歓喜

真面目に解説

一番外側のforについてはいいですね。省略。
続いて配列rbの各ラジオボタンにonClickメソッドを登録していくわけですが、よく見てもらうとRadioButton.onClick = function(){…}();となっています。この時点で一度中の関数を実行しています。実行した結果何がonClickメソッドに定義されるのかを見ていきます。

まず最初に変数rbIndexを定義しました。これは後で定義する関数から参照するためのひとつ上のスコープの変数になります。
この変数の定義の後、return function(){};となっていますね。つまりreturn以下の関数(インナーファンクション)が戻り値として返されることになります。その関数がonClickメソッドを実行した際に実際に呼ばれる関数になるわけです。ではどんな関数でしょうか。実際に1つめのラジオボタンに登録された関数を覗いてみます。

f:id:uske_S:20190809151044p:plain

変数rbIndexが定義されている場所は見えませんが実際にはその一つ上のスコープに確かに存在します。
ではその部分だけ取り出しましょう。

var rbIndex = i;
return function(){
    for (var k=0; k<rb.length; k++) {
        if (rbIndex === k) {
            continue;
        }
        rb[k].value = false;
    };
};

外側のfor文で、変数rabIndexにはそのときの変数iの値が格納されます。ラジオボタンそれぞれに定義されたonClickメソッドは、このreturn以下の関数を実行することになるのですが、そのとき1つ上のスコープにある変数rbIndexを参照するので、クリックしたラジオボタン以外をうまくオフに(RadioButton.value = false)にできているというわけです。

……ね?(伝われ!

以上、グループをまたいだラジオボタンの処理をクロージャーを使って解決してみた例でした。めでたしめでたし。