JavaScriptのUIオブジェクト設計
JavaScriptでUIクラスを設計するとき、ページ内の要素に対し、それを操作するオブジェクトを割り当てるようにする。
<form> <input id="spinner" type="text" value="0" spinner:min="0" spinner:max="100" spinner:step="2" spinner:initSpeed="150" /> + <input id="spinner2" type="text" value="0" /> <script language="JavaScript"> //<![CDATA[ var spinner1 = new Spinner(); spinner1.bind("spinner"); var spinner2 = new Spinner(); spinner2.bind("spinner2"); function calc() { document.getElementById("result").value = spinner1.getValue() + spinner2.getValue(); } //]]> </script> <input type="button" value="=" onclick="calc()" /> <input id="result" type="text" value="0"/> </form>
オブジェクトをつくり、IDをbindさせると、そのノードと対応するようになるクラスを作る。
Spinnerクラス:
function Spinner() { } Spinner.prototype = { min : -Number.MAX_VALUE, max : Number.MAX_VALUE, step : 1, initSpeed : 150, count : 0, speed : 0, running : false, getValue : function () { return parseInt(this.inputNode.value); }, setValue : function (value) { this.inputNode.value = value; }, inc : function () { this.setValue(Math.min(this.max, this.getValue() + this.step)); }, dec : function () { this.setValue(Math.max(this.min, this.getValue() - this.step)); }, bind : function (id) { var spinner = this; this.id = id; this.inputNode = document.getElementById(this.id); if (this.inputNode.getAttribute("spinner:min")) { this.min = parseInt(this.inputNode.getAttribute("spinner:min")); } if (this.inputNode.getAttribute("spinner:max")) { this.max = parseInt(this.inputNode.getAttribute("spinner:max")); } if (this.inputNode.getAttribute("spinner:step")) { this.step = parseInt(this.inputNode.getAttribute("spinner:step")); } if (this.inputNode.getAttribute("spinner:initSpeed")) { this.initSpeed = parseInt(this.inputNode.getAttribute("spinner:initSpeed")); } this.inputNode.onchange = function () { var text = spinner.inputNode.value; spinner.inputNode.value = text.replace(new RegExp("[^0-9.+-]", "g"), ""); }; var refNode = this.inputNode.nextSibling; this.plusButton = document.createElement("input"); this.plusButton.type = "button"; this.plusButton.value = "+"; var plusFunc = function () { if (spinner.running) { spinner.count += 1; if (spinner.count % 10 === 0) { spinner.speed = Math.max(10, spinner.speed / 2); } spinner.inc(); setTimeout(plusFunc, spinner.speed); } }; this.plusButton.onmousedown = function () { spinner.running = true; spinner.count = 0; spinner.speed = spinner.initSpeed; spinner.inc(); setTimeout(plusFunc, spinner.speed); }; this.plusButton.onmouseup = function () { spinner.running = false; }; this.inputNode.parentNode.insertBefore(this.plusButton, refNode); this.minusButton = document.createElement("input"); this.minusButton.type = "button"; this.minusButton.value = "-"; var minusFunc = function () { if (spinner.running) { spinner.count += 1; if (spinner.count % 10 === 0) { spinner.speed = Math.max(10, spinner.speed / 2); } spinner.dec(); setTimeout(minusFunc, spinner.speed); } }; this.minusButton.onmousedown = function () { spinner.running = true; spinner.count = 0; spinner.speed = spinner.initSpeed; spinner.dec(); setTimeout(minusFunc, spinner.speed); }; this.minusButton.onmouseup = function () { spinner.running = false; }; this.inputNode.parentNode.insertBefore(this.minusButton, refNode); }, };
あまりに長いのでリファクタリングする。
リファクタリング
このSpinnerクラスでは、スピナー用ボタンなどを挿入するbind部分のコードがかなり長くなる。前半は要素からのパラメータの設定であり、後半はUIの構築コードになる。パラメータ設定はパターン的で単純にユーティリティ化することが可能。UI構築のほうはより細かなUIオブジェクトを作ることで単純化できる。
パラメータ読み込み関数(使用法は以下のRepeatButtonのbindメソッド参照):
function lookupAttribute(node, object, prefix, paramTypes) { for (var attrName in paramTypes) { var nodeAttrName = prefix + attrName; if (node.getAttribute(nodeAttrName)) { if (paramTypes[attrName] == "string") { object[attrName] = node.getAttribute(nodeAttrName); } else if (paramTypes[attrName] == "boolean") { object[attrName] = Boolean(node.getAttribute(nodeAttrName)); } else if (paramTypes[attrName] == "int") { object[attrName] = parseInt(node.getAttribute(nodeAttrName)); } else if (paramTypes[attrName] == "float") { object[attrName] = parseFloat(node.getAttribute(nodeAttrName)); } else if (paramTypes[attrName] == "function") { var code = node.getAttribute(nodeAttrName); object[attrName] = function () { return eval(code); }; } } } }
RepeatButtonクラス:
function RepeatButton() { } RepeatButton.prototype = { func : function () {}, running : false, count : 0, speed : 0, initSpeed : 0, bind : function (id) { this.buttonNode = document.getElementById(id); lookupAttribute(this.buttonNode, this, "repeatbutton:", { initSpeed : "int", func : "function", }); this.init(); }, create : function (value, initSpeed, func) { this.buttonNode = document.createElement("input"); this.buttonNode.setAttribute("type", "button"); this.buttonNode.value = value; this.initSpeed = initSpeed; this.func = func; this.init(); return this.buttonNode; }, init : function () { var repeat = this; var repeatFunc = function () { if (repeat.running) { repeat.count += 1; if (repeat.count % 10 === 0) { repeat.speed = Math.max(10, repeat.speed / 2); } repeat.func(); setTimeout(repeatFunc, repeat.speed); } }; this.buttonNode.onmousedown = function () { repeat.running = true; repeat.count = 0; repeat.speed = repeat.initSpeed; repeat.func(); setTimeout(repeatFunc, repeat.speed); }; this.buttonNode.onmouseup = function () { repeat.running = false; }; this.buttonNode.onmouseout = function () { repeat.running = false; }; }, };
そしてこれらを使うと、SpinnerのUI構築コードは以下のようになる:
function Spinner() { } Spinner.prototype = { min : -Number.MAX_VALUE, max : Number.MAX_VALUE, step : 1, initSpeed : 150, getValue : function () { return parseInt(this.inputNode.value); }, setValue : function (value) { this.inputNode.value = value; }, inc : function () { this.setValue(Math.min(this.max, this.getValue() + this.step)); }, dec : function () { this.setValue(Math.max(this.min, this.getValue() - this.step)); }, bind : function (id) { this.id = id; this.inputNode = document.getElementById(this.id); lookupAttribute(this.inputNode, this, "spinner:", { min : "int", max : "int", step : "int", initSpeed : "int", }); this.init(); }, init : function () { var spinner = this; var refNode = this.inputNode.nextSibling; this.repeatPlus = new RepeatButton(); this.plusButton = this.repeatPlus.create("+", this.initSpeed, function () {spinner.inc();}); this.inputNode.parentNode.insertBefore(this.plusButton, refNode); this.repeatMinus = new RepeatButton(); this.minusButton = this.repeatMinus.create("-", this.initSpeed, function () {spinner.dec();}); this.inputNode.parentNode.insertBefore(this.minusButton, refNode); }, };
bind以下の部分がかなり短くなる。