A minimalistic two-way binding system. View twine on GitHub.
{ "color": "blue", "name": { "first": "John", "last": "Smith" }, "showBox": true, "object": { "value": "foo" }, "widgets": [ { "value": 5 }, { "value": 10 } ], "things": [ { "value": 50 }, { "value": 75 }, { "value": 100 }, { "value": 125 } ], "objects": [ { "value": "asdf" }, { "value": "xyz" } ], "available": false, "state": "hidden", "yourInterest": "" }
Bindings are defined with strings of JavaScript directly on a DOM node. This means you're already familiar with the binding syntax, and you have access to all the properties inside your binding definition that the underlying object does.
For input elements, the initial value of a binding will be read from the DOM if it's not already defined:
<input type="text" data-bind="color" value="blue"> <strong data-bind="color.length"></strong> characters used.
Every binding is evaluated relative to a context. All this means is that when you write data-bind="key"
, you're accessing the key
property of the current context. Any value can be used as a context, so most of the time you'll use a plain ol' JavaScript object.
The context for this page is stored on window
, so you can access it in the console if you want. Since it's just an object, it's easy to encode:
<pre data-bind="JSON.stringify(window.context)"></pre>
{"color":"blue","name":{"first":"John","last":"Smith"},"showBox":true,"object":{"value":"foo"},"widgets":[{"value":5},{"value":10}],"things":[{"value":50},{"value":75},{"value":100},{"value":125}],"objects":[{"value":"asdf"},{"value":"xyz"}],"available":false,"state":"hidden","yourInterest":""}
In this demonstration, the context was defined and initialized with:
var context = {}; Twine.reset(context).bind().refresh();
The context is shown in the sidebar for reference as you peruse.
It is also possible to create sub-contexts with the define
attribute.
<div data-define="{name: {first: 'John', last: 'Smith'}}"> <label for="firstname">What is your first name?</label> <input id="firstname" type="text" data-bind="name.first"> <label for="lastname">What is your last name?</label> <input id="lastname" type="text" data-bind="name.last"> </div> <span> Hello, <strong data-bind="name.first + ' ' + name.last"></strong> </span>
data-define
can be used for context creation or for introducing variables into the current context.
<div data-define="{showBox: true}"> <button data-bind-event-click="showBox = !showBox">Toggle visibility</button> <div data-bind-show="showBox">I'm visible!</div> </div>
This can be pretty useful for when you want to use data-bind-show
and start in a visible state.
data-context
lets you change the current context for every element nested inside.
<div data-define="{object: {value: 'foo'} }" data-context="object"> <span data-bind="value"></span> <!-- object.value from inside the context of object --> </div> <div> <span data-bind="object.value"> <!-- object.value from outside the context of object --> </span></div>
data-define-array
lets you define an array of objects one at a time in the context and implicitly reference the correct member in the array.
<div data-define-array="{widgets: {value: 5, foo: function(){ return 12; }}}"> <div data-define-array="{things: {value: 50}}"> <span data-bind="widgets.value"></span> <!-- widgets[0].value --> <span data-bind="widgets.foo()"></span> <!-- widgets[0].foo() --> <span data-bind="things.value"></span> <!-- things[0].value --> </div> <div data-define-array="{things: {value: 75}}"> <span data-bind="widgets.value">5</span> <!-- widgets[0].value --> <span data-bind="widgets.foo()"></span> <!-- widgets[0].foo() --> <span data-bind="things.value"></span> <!-- things[1].value --> </div> </div> <div data-define-array="{widgets: {value: 10, foo: function(){ return 99; }}}"> <div data-define-array="{things: {value: 100}}"> <span data-bind="widgets.value"></span> <!-- widgets[1].value --> <span data-bind="widgets.foo()"></span> <!-- widgets[1].foo() --> <span data-bind="things.value"></span> <!-- things[2].value --> </div> <div data-define-array="{things: {value: 125}}"> <span data-bind="widgets.value"></span> <!-- widgets[1].value --> <span data-bind="widgets.foo()"></span> <!-- widgets[1].foo() --> <span data-bind="things.value"></span> <!-- things[3].value --> </div> </div>
data-define-array
can also be used with data-context
<div data-define-array="{objects: {value: 'asdf', foo: function(){ return 'lmnop'; }}}" data-context="objects"> <!-- objects[0] --> <span data-bind="value"></span> <!-- objects[0].value --> <span data-bind="foo()"></span> <!-- objects[0].foo() --> </div> <div data-define-array="{objects: {value: 'xyz', foo: function(){ return 'qrstuv'; }}}" data-context="objects"> <!-- objects[1] --> <span data-bind="value"></span> <!-- objects[1].value --> <span data-bind="foo()"></span> <!-- objects[1].foo() --> </div>
data-bind-show
adds the class hide
to the node. You can then use CSS like .hide { display: none; }
or similar to hide the node and its children from view.
<label>Enter a color:</label> <input type="text" data-bind="color"> <p data-bind-show="color"> That color is <strong data-bind-show="color.length <= 4">not</strong> longer than 4 characters. </p>
That color is not longer than 4 characters.
data-bind-class
toggles a node's classes, given an object with class names for keys and booleans for values.
<label>Enter a color:</label> <input type="text" data-bind="color"> <h3 data-bind="color" data-bind-class="{blue: color == 'blue', green: color == 'green'}"></h3>
The binding value of checkboxes is a boolean indicating the checked state.
<input type="checkbox" data-bind="available"> <span data-bind="available"></span>
The binding value of radio buttons is the value
attribute of the currently selected button. Make sure to use the same key for every button in the radio group.
<p> State: <strong data-bind="state"></strong> </p> <input type="radio" name="state" data-bind="state" value="hidden" checked=""> <input type="radio" name="state" data-bind="state" value="published">
State: hidden
Alternatively, you may use data-bind-checked
, which is a one-way binding of the checked
attribute.
The following DOM attributes can be bound to via:
data-bind-placeholder
data-bind-checked
data-bind-disabled
data-bind-href
data-bind-title
data-bind-readOnly
data-bind-draggable
<label>What are you interested in?</label> <input type="text" data-bind="yourInterest"> <p data-bind-show="yourInterest"> Search Google for <a href="#" data-bind-href="'https://www.google.ca/?q=' + yourInterest" data-bind="yourInterest" target="_blank"></a> </p>
DOM events are available for quick bindings. The default behaviour (think event.preventDefault
) will be prevented, unless either:
event.type === "submit"
<a>
Additionally, the presence of allow-default
attribute will cause the twine system to never prevent default, rending the two exceptions above moot.
The following events are provided:
data-bind-event-click
data-bind-event-dblclick
data-bind-event-mouseenter
data-bind-event-mouseleave
data-bind-event-mouseover
data-bind-event-mouseout
data-bind-event-mousedown
data-bind-event-mouseup
data-bind-event-submit
data-bind-event-dragenter
data-bind-event-dragleave
data-bind-event-dragover
data-bind-event-drop
data-bind-event-drag
data-bind-event-change
data-bind-event-keypress
data-bind-event-keydown
data-bind-event-keyup
data-bind-event-input
data-bind-event-error
data-bind-event-done
data-bind-event-success
data-bind-event-fail
data-bind-event-blur
data-bind-event-focus
data-bind-event-load
Remember how bindings are just JavaScript? The same applies for event handlers.
<a href="#" class="green" data-bind-event-click="color = 'green'">green</a> or <a href="#" class="blue" data-bind-event-click="color = 'blue'">blue</a>
However, inline mutation is pretty ghetto, so let's define some functions on our context. (these won't appear in the sidebar since JSON.stringify
ignores functions)
context.otherColor = function() { return this.color == "green" ? "blue" : "green"; }; context.toggleColor = function() { this.color = this.otherColor(); };
Switch to <a href="#" data-bind="otherColor()" data-bind-event-click="toggleColor()"></a>
Events bound in this way have access to a few named parameters that you can pass in:
this
: the node in questionevent
: the event object that is firedcontext.keyPressed = function(node, event) { this.whichKey = event.which; this.whichInput = node.getAttribute("id"); };
<input id="foo" type="text" data-bind-event-keydown="keyPressed(this, event)" placeholder="foo"> <input id="bar" type="text" data-bind-event-keydown="keyPressed(this, event)" placeholder="bar"> <p> You pressed key: <strong data-bind="whichKey"></strong> </p> <p> inside of input with ID: <strong data-bind="whichInput"></strong> </p>
You pressed key:
inside of input with ID:
Further, as you can see from the example keyPressed
above, this
inside of a function called this way will refer to the current context.