View Temporary Variables in KO

by sukobuto

HTML

<script src="http://sukobuto.com/cdn/knockout-es5.min.js"></script>
<h1>KnockoutJS Variable Binding</h1>
<h2>for temporary variable in View</h2>

<p>
    コンバータ関数で変換した結果等を一時変数に保持します。<br/>
    通常のバインディングでも、仮想エレメント(コメント型)でも使えます。
</p>
<p>
    一時変数を observable にする場合は無名関数でラップして渡して下さい。<br/>
    無名関数でラップした場合、一時変数は ko.computed となります。<br/>
    ただし Knockout-ES5 がロードされている場合は ko.defineProperty が使われるため<br/>
    data-bind で () を付けて関数呼び出しする必要はなくなります。
</p>


<div id="demo" data-bind="click: function(d, e) { someText = vc.click2Text(e) }">
    
    <svg width="200" height="200">
        <polygon data-bind="attr: { points: vc.stats2Points(stats) }"></polygon>
        <circle class="chartbase" cx="100" cy="100" r="80"></circle>
        <circle class="chartbase light" cx="100" cy="100" r="60"></circle>
        <circle class="chartbase" cx="100" cy="100" r="40"></circle>
        <circle class="chartbase light" cx="100" cy="100" r="20"></circle>
        <!--ko foreach: stats-->
        <!--ko variable: {
            lineEndPoint: function() { return vc.value2Point(120, $index(), $parent.stats.length) },
            labelPoint: function() { return vc.value2Point(value, $index(), $parent.stats.length) }
        }--><!--/ko-->
        <line class="chartbase" x1="100" y1="100"
              data-bind="attr: { x2: lineEndPoint.x, y2: lineEndPoint.y, }"></line>
        <text data-bind="attr: { x: labelPoint.x - 4, y: labelPoint.y + 4 }, text: label"></text>
        <!--/ko-->
    </svg>

	<div data-bind="foreach: stats">
        <div>
            <label data-bind="text: label"></label>
            <input type="range" data-bind="value: value" min="0" max="100" />
            <span data-bind="text: value"></span>
            <button data-bind="click: $parent.remove">X</button>
        </div>
	</div>
    <button data-bind="click: reverse">REVERSE</button><br/>
    <input type="text" data-bind="value: statInput"/>
    <button data-bind="click: createStat">Create a Stat</button>
    
    <p...

CSS

body {
	font-family: Helvetica Neue, Arial, sans-serif;
}

polygon {
	fill: rgba(255, 136, 136, 0.5);
    stroke: #f88;
    stroke-width: 2px;
}

line.chartbase {
	stroke: #999;
    opacity: .7;
}

circle.chartbase {
	fill: transparent;
	stroke: #999;
    opacity: .7;
}

circle.chartbase.light {
    opacity: .3;
}

text {
	font-family: Helvetica Neue, Arial, sans-serif;
	font-size: 10px;
	fill: #666;
}

label {
	display: inline-block;
	margin-left: 10px;
	width: 20px;
}

JavaScript

ko.bindingHandlers['variable'] = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        var variables = valueAccessor();
        for ( var v in variables ) {
            if (variables.hasOwnProperty(v)) {
                if (ko.isObservable(variables[v])) {
                    bindingContext[v] = variables[v];
                } else if (typeof variables[v] === 'function') {
                    if (ko.hasOwnProperty('defineProperty'))
                        ko.defineProperty(bindingContext, v, variables[v]);
                    else bindingContext[v] = ko.computed(variables[v]);
                } else {
                    bindingContext[v] = variables[v];
                }
            }
        }
    }
};
ko.virtualElements.allowedBindings['variable'] = true;

// View Converter
window.vc = (function() {
    var root = this;
    
    // stat の値からスターチャートの頂点座標に変換
    root.value2Point = function(value, index, total) {
        console.log(arguments);
        var x     = 0,
            y     = -value * 0.8,
            angle = Math.PI * 2 / total * index,
            cos   = Math.cos(angle),
            sin   = Math.sin(angle),
            tx    = x * cos - y * sin + 100,
            ty    = x * sin + y * cos + 100
        return {
            x: tx,
            y: ty
        }
    }
    
    // stat の配列からスターチャートのパス頂点情報に変換
    root.stats2Points = function(stats) {
        return stats.map(function(stat) {
            var point = root.value2Point(stat.value, stats.indexOf(stat), stats.length);
            return point.x + ',' + point.y;
        }).join(' ');
    };
    
    // クリックイベントからてきとうなテキストに変換
    root.click2Text = function(e) {
        return 'クリック位置 x=' + e.offsetX + ' y=' + e.offsetY;
    };
    
    return root;
})();


function AppViewModel() {
	var self = this;
    
	self.stats = [];
    self.statInput = "";
    self.someText = "";
    ko.track(self);
    
    self.addStat = function(data) {
       ...