Data Binding
Wins
- Bind authoring happens almost entirely in markup via
bind:
attributes. - Bind data references use industry-standard URI and query path syntax.
- You can quickly bind to JSON, XML, or JavaScript objects with equal ease.
- Clear syntax differentiates binds from templating (
[[...]]
vs{{...}}
). - Signal-driven operation makes for easy testing and mocking.
Contents
Concepts
Cookbook
- Bind Input Elements
- Bind Element Content
- Bind Attribute Values
- Bind To JSON
- Bind To XML
- Bind To JS Object
- Query Into JSON
- Query Into XML
- Query Into JS Object
- Scoping A Bind
- Defining In-Page Data
reference
Code
Concepts
In the context of web applications, data binding is the common term used to describe observation of changes to one or more source objects by one or more observers. As such, data binding is a central feature of modern MVC/MVVM architectures and hence most modern platforms.
In TIBET, data binding is syntactic sugar for setting up observations of
Change
signals (or one of their subtypes) coming from a source object. These
binds can be either one-way or bi-directional depending on your requirements.
All binds in TIBET are ultimately implemented via Signals.
TIBET sugars data binding via attributes in the TIBET-specific bind:
namespace. You can also create binds using double-brackets [[ ]]
within
markup, particularly within attribute values.
The markup below uses TIBET's bind:scope
attribute to set a binding context
and TIBET's [[ ]]
syntax to bind the value
attribute's value:
<input type="text" bind:scope="urn:tibet:currentEmployee" value="[[lname]]"/>
Bind scopes can be nested as shown below. In this particular case, the
xctrls:list
is scoped to ~app_dat/codes
and ultimately asks TIBET to invoke
the list's set
method for the data
attribute to whatever data is contained
in the file ~app_dat/codes/states.json
:
<body bind:scope="~app_dat">
<div bind:scope="codes">
<xctrls:list bind:in="{data: states.json}"/>
</div>
</body>
All bind:
sources and sinks are represented by TIBET
URIs, a standard way to refer to resources in the web and in TIBET. All
specific data references use standards-based TIBET
Paths such as JSONPath or XPath depending on the source data format.
Using TIBET's bind:scope
, bind:in
, bind:out
, and bind:io
attributes you
can quickly and easily bind data to your UI regardless of whether it is JSON,
XML, or a standard JavaScript object.
bind:scope
The bind:scope
attribute lets you define a scope on an outer element which
will serve as a root for any relative bind paths at or below that scope. This is
particular useful on elements like forms or tables where a number of individual
properties or rows may benefit from shared scope:
<form bind:scope="urn:tibet:CurrentEmployee">
...
<input bind:io="firstname"/>
...
</form>
Values in bind:scope
attributes found along a UI element's ancestor chain are
combined as needed, supporting fully-nested scoping. Any access path below a
bind:scope
can always be set to an absolute value to directly target a
different data scope.
bind:io
Most TIBET binds are bi-directional and use bind:io
as their directional
attribute to signify the target control both receives input and sends output to
the bind:
<input type="text" bind:io="value"/>
bind:in
When dealing with read-only UI controls you can use bind:in
to signify that
the control receives input from the bind but does not send output to it:
<span bind:in="urn:tibet:hello"/>
bind:out
While not common, it's also possible to bind output separately from input. This can be used to create more interesting data flows via binds:
<unique:widget bind:out="urn:tibet:hello"/>
Bind Values
Simple binds often reference an absolute or relative URI with no specific
query. When using the simple form the bind will default to a query of 'value'
and invoke get('value')
on the resolved URI to acquire the value of the bind.
You can include a query with the URI (or as a standalone bind component) using
XPointer syntax (a fragment #
specifier followed by a recognized query).
Common query syntax options include: xpath1
(XPath 1.0), jpath
(JSON Path),
and tibet
(our own JavaScript path syntax):
<!-- XPath -->
<span bind:scope="urn:tibet:people#xpath1(/people/person[1])">
First name (bind:io): <textarea bind:io="firstname"></textarea>
</span>
<!-- TIBET path (with slicing) -->
<input type="text" bind:io="{value: urn:tibet:people#tibet(people[0].firstname)}"/>
More complex binds can provide a simplified JavaScript object string to define how the bind should operate:
<input type="text" bind:io="{value: 'The index: [[$INDEX]]'}"/>
As shown in the example above, TIBET binds can include references to a small number of internal variables including the current iteration $INDEX.
Binds also support the TIBET Shell (TSH) formatter syntax via .%
. In the
example below we're formatting the current item's 'middlename` slot to be upper
case:
<input type="text" bind:io="{value: 'The middle name: [[$_.middlename .% upperCase]]'}"/>
To target specific aspects of your source or sink TIBET allows you to use an appropriate query syntax for the data in question such as JSONPath, CSS Queries, XPath, or expanded TIBET path syntax (see TIBET Paths for details).
For attribute values such as class=
or to target element content regions you can use a double-bracket syntax [[ ]]
to create binds. We use [[ ]]
for binds to avoid conflicts with double-curly(brace) {{ }}
syntax used by TIBET Templating and other templating systems.
<span bind:scope="urn:tibet:currentEmployee">[[lname]]</span>
The core machinery of data binding, Change
signals triggered from
set()
calls, is universal in TIBET. All TIBET-derived objects automatically
implement set()
and can trigger Change
signals if desired. In TIBET there is
no unique Model
type or associated inheritance requirement. Any object can
serve as a Model, provided you can reference it via TIBET URI.
NOTE: in the RC1 version of TIBET certain source aspects such as `type` and `name` can be problematic. It's best for now if your source JSON uses names that don't mask those. This issue should be resolved in an upcoming release.
As mentioned earlier, all binds in TIBET ultimately refer to a TIBET URI object. TIBET URIs give you an industry-standard syntax to access, cache, and manage application data including being able to snapshot data for undo processing.
A specific URI subtype, TP.core.URN
, provides a way to give any data a public
name and a URI reference, allowing you to bind to any object.
For example, we could create or retrieve employeeData
and assign it to the
TIBET urn urn:tibet:CurrentEmployee
and then refer to that data from anywhere
in TIBET using that URN value as the name:
urn = TP.uc('urn:tibet:CurrentEmployee');
urn.setContent(employeeData);
...
<form bind:scope="urn:tibet:CurrentEmployee">
...
</form>
Data binding in TIBET is fully tooled. When you're using the Lama your current bind information is automatically displayed in the Lama's HUD display for easy reference and manipulation.
Cookbook
Bind Input Elements
Standard HTML <input>
elements, or any other elements whose value
property
should be bound in a bi-directional fashion, can be bound very simply using the
bind:io
attribute:
// assume this URN and data exists in-page or in memory...
TP.uc('urn:tibet:test_person',
'{"person": {"lastname": "Smith", "firstname": "Joe"}}'
);
<input type="text" id="firstNameField"
bind:io="urn:tibet:test_person#jpath($.person.firstname)"/>
As with any bind, the bind:io
attribute value contains a URI with optional
XPointer defining any query portion. In the sample above we point to the URN
urn:tibet:test_person
and query it using JSONPath for the person.firstname
value.
Bind Element Content
You can bind to the content area of an element using TIBET's [[ ]]
syntax.
In this example we use the same data and attribute value as the prior cookbook
entry but since a span is normally read-only we can use bind:in
to simplify
the observations TIBET sets up:
// assume this URN and data exists in-page or in memory...
TP.uc('urn:tibet:test_person',
'{"person": {"lastname": "Smith", "firstname": "Joe"}}'
);
<span>[[urn:tibet:test_person#jpath($.person.firstname)]]</span>
There's no restriction on what portion of an element's content you bind, just be certain the result creates valid XHTML (so properly escape or use entities as needed).
Bind Attribute Values
To bind into a full or partial attribute value use TIBET's [[ ]]
syntax,
placing the brackets inside the attribute text in the location you require.
Below we again bind to an <input>
element's value, this time via attribute
syntax to define a value for the value
attribute rather than using a bind:
attribute:
// assume this URN and data exists in-page or in memory...
TP.uc('urn:tibet:test_person',
'{"person": {"lastname": "Smith", "firstname": "Joe"}}'
);
// For clarity we split the URN into a scope and a query below...
<div bind:scope="urn:tibet:test_people">
<input type="text" value="[[#jpath($.person.firstname)]]"/>
</div>
You can place [[ ]]
syntax anywhere in an attribute, using the bind to
populate only a portion of the attribute value:
<span id="colorSpan"
style="background-color: [[urn:tibet:test_people#jpath($.people[0].color)]];
font-weight: bold">This should be colored based on bound data value.</span>
Yes, we know…don't use inline style… It's a snippet, not a suggestion ;).
Bind To JSON
Binding to JSON strings (as opposed to JavaScript objects) is simple. Point your bind attribute(s) at JSON content and TIBET does the rest.
For example, here's another way to write Hello World! provided your project name is 'hello' ;)
<hello:app id="app">
<tibet:service href="~/tibet.json" id="urn:tibet:tibet_json"
on:attach="UIActivate"/>
<div>[[urn:tibet:tibet_json#jpath($.project.name)]] world!</div>
</hello:app>
In TIBET the easiest way to fetch data from markup is to use the tibet:service
tag. This tag lets us leverage TIBET's virtual path syntax to point to a remote
resource, provide a URN to name the content, and configure a number of other
features such as how we process various HTTP verbs. In this case we added
on:attach="UIActivate"
to tell the tag we want it to fetch its data
essentially 'onload' (for TIBET on:attach) so the data is available immediately.
One of the more interesting/useful features of the tibet:service
tag is that
you can add a watched="true"
attribute to it and any changes to the original
source which are passed to the client will trigger change notification.
If we add watched="true"
to our example and then make a change to the
tibet.json
file on the file system we'll see our div update automatically.
Set the watched attribute:
<hello:app id="app">
<tibet:service href="~/tibet.json" id="urn:tibet:tibet_json"
on:attach="UIActivate" watched="true"/>
<div>[[urn:tibet:tibet_json#jpath($.project.name)]] world!</div>
</hello:app>
And change tibet.json
to have a new project name:
"name": "out of this"
Bind To XML
TIBET lets you bind directly to XML, just point to it with a URI or URN:
<!-- assume this data... -->
<tibet:data id="urn:tibet:test_person">
<person xmlns="">
<lastname>Smith</lastname>
<firstname>Joe</firstname>
</person>
</tibet:data>
<input type="text" bind:io="urn:tibet:test_person#xpath1(/person/firstname)"/>
In the markup above we have an inline XML block we name and bind to. Note that in this example our query is an XPath so we can slice into the data to access the specific value(s) we are after.
Bind To JS Object
You can use TIBET's URN syntax to name, and then bind to, any object in your
application. For example, TIBET automatically registers the current
TP.core.User
instance under the URN urn:tibet:user
:
<div>[[urn:tibet:user]]</div>
TIBET automatically resolves the object reference from any URN and resolves it
based on the aspect the bind references. In the majority of cases that's
equivalent to invoking get('value')
on the resource being referenced.
Query Into JSON
For JSON data TIBET uses JSONPath to query the data.
In our Bind To JSON example we sliced into our tibet.json
file above and
extract the project name instead using the XPointer jpath($.project.name)
. We
can do that regardless of the bind syntax we use (bind:scope, bind:io, etc):
<div bind:scope="urn:tibet:tibet_json">
<input type="text" value="[[#jpath($.project.name)]]"/>
</div>
See https://www.npmjs.com/package/jsonpath-plus for details on JSONPath syntax as currently implemented in TIBET.
Query Into XML
TIBET allows you to query XML using the XPath 1.0 syntax supported by modern browsers. This query capability isn't just limited to binds, you can use it with any XML content.
As with JSON or JS Object bindings you place your query in an industry-standard
XPointer. Below we use the XPath /person/firstname
which TIBET will
auto-collapse into a single value if the target is a single-valued component
(like our input field here):
<!-- assume this data... -->
<tibet:data id="urn:tibet:test_person">
<person xmlns="">
<lastname>Smith</lastname>
<firstname>Joe</firstname>
</person>
</tibet:data>
<input type="text" bind:io="urn:tibet:test_person#xpath1(/person/firstname)"/>
Query Into JS Object
As mentioned in the Bind To JS Object example, TIBET automatically registers the
current TP.core.User
instance under the URN urn:tibet:user
. We can bind to
that object's vcard
property using the following syntax:
<div>[[urn:tibet:user#tibet(vcard)]]</div>
TIBET's path syntax is loosely based on the access path syntax used in Python so
you can use [index]
variations to slice into arrays etc. For more on the query
syntax see TIBET Paths.
Scoping A Bind
TIBET's bind:scope
attribute lets you define binding scopes, nested scopes
within which relative paths are resolved.
For example, the tibet.json
file could be scoped in multiple levels as follows:
<tibet:service href="~/tibet.json" id="urn:tibet:tibet_json"
on:attach="UIActivate"/>
<div bind:scope="urn:tibet:tibet_json">
<div bind:scope="$.project">
<span bind:in="name"/>
</div>
</div>
Note that in the sample above (and in all such cases) the value for bind:scope
should fit the proper query language for the target data.
You should use XPath when scoping XML, JSONPath when scoping JSON, and a valid TIBET path segment when scoping a JavaScript object.
Defining In-Page Data
There are several cases where you might choose to "inline" data for a page directly within that page itself. TIBET's data binding tests make extensive use of this approach for example.
To inline data in a page you can use the tibet:data
element. The name
attribute on that element will be used to automatically take any content in the
element and assign it as the content of the referenced URN.
In the sample below we define urn:tibet:test_person
and let it hold a small
JSON data structure. NOTE we use a CDATA
block to ensure any syntax in the
content itself is ignored and the block is treated opaquely by the browser:
<tibet:data id="urn:tibet:test_person">
<![CDATA[
{"person": {"lastname": "Smith", "firstname": "Joe"}}
]]>
</tibet:data>
As mentioned, this is an excellent approach to use for test pages which may not be able to rely upon a server during test execution.
reference
Binding Variables
'$REQUEST', null,
'TP', TP,
'APP', APP,
'$SOURCE', source,
'$TAG', targetTPElem,
'$TARGET', tpDoc,
'$_', wrappedVal,
'$INPUT', repeatSource,
'$INDEX', index,
'$FIRST', index === 0,
'$MIDDLE', index > 0 && index < last,
'$LAST', index !== last,
'$EVEN', index % 2 === 0,
'$ODD', index % 2 !== 0,
'$#', index);
Code
Binds make use of a broad array of functionality from across TIBET, from
signaling to URIs to set()
methods to tag attach/detach processing in the
DOM.
Dozens of examples of binds can be found in ~lib/test/src/bind
.
Each test will typically ensure a data source, a markup template, and test criteria. The markup template will show you the particular bind syntax being used while the data source will help you analyze how the bind paths are accessing the data.
The "driver" file TP.bind_Tests.js
manages loading the individual .xhtml
files containing the bind syntax examples you're probably looking for. Once
each page loads the driver checks that their values match expectations.
Binding, at least UI binding, in TIBET is managed by code in the bind:
namespace.found in ~lib/src/bind
and the TIBET kernel file TIBETBinding.js
.
Since all binds in TIBET reference URIs of various forms the TIBETURITypes.js
file in the TIBET kernel handles most URI-related functionality.
Finally, since all binds are essentially just sugared change notifications the
TIBETSignaling.js
file and set()
methods which trigger change notifications
are central to how binding operates.