bb-better-binding
1 way binding from js controllers to html templates
Setup
run: npm i -save bb-beter-binding
Hello World
Simple Example
.html
template
your
.js
controller
your const source = ; sourceshowNumbers = true;sourcenumbers = 10 12 16 13; source { console};
Components Example
.html
template
your due on $s{date}
.js
controller
your const source = ; sourceoverdueBooks = dueDate: '15-17-32025-02' title: 'why humans were taller 8 billion years ago' titleColor: 'red'; sourcefontSize = '30';
Syntax
value binding
<span bind="x"> </span>
replaces the innerHtml of the element with source.x
$s{x}
is a shorthand for <span bind="x"> </span>
for binding
<span bind-for="item in list"> # $s{index} : $s{item} </span>
repeats the element for each element in source.list
and makes item
and index
available to all children elements
if binding
<span bind-if="show"> am i visible? </span>
sets the hidden
property of the element
as binding
<span bind-as="response.data.errorMessages[2].text as text, ugly as pretty> $s{text} </span>
makes text
available to all children elements as a shortcut to source.response.data.errorMessages[2].text
component binding
<div bind-component="banner with text header">
<div style="font-size:50px" bind="header"> </div>
<div style="font-size:20px" bind="text"> </div>
</div>
defines a reusable component named banner
and with paramters text
and header
use binding
<div bind-use="banner with bannerData.text bannerData.header"> </div>
uses a component named banner
, passing source.bannerData.text
and source.bannerData.header
as parameters
Note on component load order
Components are loaded from bottom of the document, upwards. This means, if component-parent
uses component-child
, then component-child
should be loaded first (e.g. defined lower in the html). Similary, all usages of component-parent
should occur after (e.g. higher in the html) the component than where it is defined.
block binding
<!-- parent template -->
// parent controllerconst bb = ;bb;const source = bb;sourcename = 'The Elephant\'s Todo List';
<!-- todoList.html template -->List Name: $s{name}
// todoList.js controllerlet template = ;let { sourcelist = 'elephant' 'lion' 'rabbit';};let parameters = 'color' 'name';moduleexports = template controller parameters;
Creates externalized and reusable blocks
Blocks or Components?
Components
allow you to resuse parts of your template but remain in the same template file and share the same source
. Blocks
go a step further, extracting the reusable part to external files to allow use by multiple pages or blocks, have their own isolated source
, and help keep your templates and controllers smaller.
attribute binding
<div name="box-number-${i}" style="color: ${favoriteColor}; font-size=${largeFont}"> </div>
binds source.i
to the element name and source.favoriteColor
and source.largeFont
to the element's style attributes.
function binding
<input onclick="${logHello(userName, '!!!')}" onchange="${logWoah(this, event)}"> </input>
binds source.userName
and source.logHello
to the element's onclick
attribute. If either changes, the onclick
attribute will be reassigned to source.logHello(source.userName, '!!!')
.
for example, source.logHello = (name, punctuation) => { console.log('hi', name, punctuation) }
and source.userName = 'kangaroo'
.
expression binding
<div bind-if="isBetterNumber(value, 3)"> $s{value} </div>
binds source.value
and source.isBetterNumber
to the bind-if
binding. If either changes, the expression will be reevaluated.
for example, if source.isBetterNumber = (a, b) => a > b;
and source.value = 30;
, then the div
will be visible.
$s{x(y)}
is a shorthand for <span bind="x(y)"> </span>
.
element binding
sourceplayButtoninnerText = 'click me to begin ur audiobook!';source sourceaudioBook;
sets a field on source to the html element.
element bindings are read only; e.g source.playButton
and source.audioBook
are not reassignable in the above example.
optionally, you may wrap refferences to source elements in getElem
. source.inputs.name.firstNameInput.value
becomes source.getElem('inputs.name.firstNameInput').value
. See the Triggering Bindings
section on when this could be useful.
bind-if
and bind
utility expressions available by default for !
, not
visibile: $s{not(x)} visible if x is falsy
=
, eq
, equal
visibile: $s{eq(x, y)}
!=
, nEq
, notEqual
visibile: $s{nEq(x, y)} visible if x !== y
>
, greater
visibile: $s{greater(x, y)} visible if x > y
<
, less
visibile: $s{less(x, y)} visible if x < y
>=
, greaterEq
visibile: $s{greaterEq(x, y)} visibile if x >= y
<=
, lessEq
visibile: $s{greaterEq(x, y)} visible if x <= y
|
, ||
, or
visibile: $s{or(x, y, z, w)} visible if any argument is truthy
&
, &&
, and
visibile: $s{and(x, y, z, w)} visible if all arguments are truthy
Maximum call stack size exceeded
)
avoiding infinite triggers (e.g. Imagine you have the following in your template $s{func(obj)}
, and the following controller,
sourceobj = value: 100 count: 0; source { objcount++; return objvalue;};
This results in both source.func
and source.obj
binding to the span's value binding. In other words, whenever either changes, the value binding (source.func(source.obj)
) is invoked. The problem here is that source.func
will modify source.obj
when it increments count
, resulting in an infinite cycle of the binding being invoked because source.obj
is modified, and source.obj
being modified because the binding is invoked.
_bindIgnore_
option 1, One solution is to ignore the fields that don't need to trigger bindings: source.obj._bindIgnore_ = ['count']
. Any field names in the list _bindIgnore_
will not trigger any bindings when modified. So as long as source.obj._bindIgnore_
includes count
, we can modify count
and no bindings will be triggered. _bindIgnore_
can be modified as needed in order to ignore certain fields only under certain conditions.
template:
$s{func(obj)}
controller:
sourceobj = value: 100 count: 0 _bindIgnore_: 'count'; source { objcount++; return objvalue;};
_bindAvoidCycles_
option 2, What if our template relies on count
as well: $s{obj.count}
? Then we no longer want to ignore updates to source.obj.count
, and _bindIgnore_
is not a satisfactory solution in this case. An alternative way to avoid bindings from triggering is setting source.obj._bindAvoidCycles_ = true
. This will ensure each time source.obj
is changed, it will trigger each of it's binding at most once per change. E.g. creating a new field source.obj.newValue = 200
will trigger source.func(source.obj)
once for the assignment of newValue
, and once more for the increment of obj.count
.
template:
$s{func(obj)}$s{obj.count}
controller:
sourceobj = value: 100 count: 0 bindAvoidCycles: true; source { objcount++; return objvalue;};
_
option 3, Yet a third option is to specify paramters with a _
prefix in the template $s{func(_obj, obj.value)}
. This allows individually configuring each bind with which source
fields are binded to it. In the above example, source.func
will only be invoked when obj.value
is modified, but not when source.obj
is modified. This allows you to use $s{obj.count}
elsewhere in you template, because the _
is applied to each paramter in each binding individually.
template:
$s{func(_obj, obj.value)}$s{obj.count}
controller:
sourceobj = value: 100 count: 0; source { objcount++; return objvalue;};
Triggering bindings
1
Bindings are triggered when source is modified, even if indirectly (e.g. value3
in below example).
$s{obj.value1}$s{obj.value2}$s{obj.value3}
let obj = value1: 1 value2: 2 value3: 3;sourceobj = obj;sourceobjvalue2 = 22;objvalue3 = 33;
2
Bindings are triggered when any property on a bound object changes.
hi there
source objflag;sourceobjflag = true;
The example above displays hi there
. Modifying the field flag
on object source.obj
triggers the binding on obj
, even though there are no direct bindings on obj.flag
.
3
By default bindings are triggered asynchroniously.
This is fine because, except for element bindings, all other bindings are 1 way; modifying source
updates the html
, but user modifications to the html
are projected to source either though event listeners or by element bindings. In order to make sure element bindings can be accessed syncrhoniously in your app, on fetching element bindings via getElem
, all bindings queued to be triggered will trigger. This won't always be necessary.
$s{option}
let { sourcepeople; let index = sourcepeoplelength - 1; // bad code source`personInput`value = defaultName; // good code, alternative 1 sourcevalue = defaultName; // good code, alternative 2 source; source`personInput`value = defaultName;}; let { sourceoptions = 'rainbow' 'unicorn' 'moon candy' 'kitten hamburger' 'fluffy headless teddy'; // bad code sourceoption0checked = true; // good code, alternative 1 sourcechecked = true; // goode code, alternative 2 source; sourceoption0checked = true;}
In the above example, the bad code lines won't work. When initOptions
is invoked, source.options
is set. But because bindings are triggered asynchronously, the html input element is not yet created and the source.option0
element reference does not yet exist. Invoking source.getElem
grantees the html is updated before source.option0
is accessed.
4
It is possible to disable automatic binding triggering. This is useful when building an app that already has a "loop."
Usually, you'll use let source = bb.boot(document.firstElementChild);
To disable automatic binding triggering, you should instead use let source = bb.boot(document.firstElementChild, undefined, true);
To then trigger bindings manually, use bb.tick()
. To enable automatic binding triggering at a later time, use bb.loop()
.
let source = bb; sourceyummyMenu = 'apple' 'blueberry' 'grapes' 'sunlight' 'canoe';bb; let { sourceyummyMenu; bb;}; menuItemsRepository;
execution order of bindings
- attribute binding
- elem binding
- for binding
- use binding
- as binding
- if binding
- component binding
- block binding
- value binding
debug mode
Typically, you would initiate the parsing of html, creation of binds, and retrieval of source via:
const source = require('bb-better-binding')().boot(document.firstElementChild);
or for apps using blocks:
const bb = ;bb;// more block declarations ...bb;
A second optional argument may be passed to the boot
method in order to put the source, binds, and handlers onto an easily-viewable-during-runtime location such as window
.
const source = require('bb-better-binding')().boot(document.firstElementChild, window);
Which results in creating the fields window.source
, window.binds
, window.handlers
, and window.components
with the purpose of making debugging easier. Note, this should only be used for debugging, and binds
, handlers
, and components
should not be modified unless you understand the source code.
binds
binds
describes which handlers
should be invoked when which source
values are changed.
binds = 'a.b.c': fors: container outerElem sourceTo sourceFrom sourceLinks ifs: expressionBind1 expressionBind3 values: expressionBind1 expressionBind2 attributes: attributeBind1 attributeBind2 ; attributeBind = elem: elem1 attributeName functionName // can be null params: stringValue | sourceValue: string // for null functionName params: // for not null functionName; expressionBind = elem: elem1 expressionName // can be null params: bindName // can be null;
handlers
handlers
is the functions tree that is navigated and invoked appropriately when bindings
are invoked because source
values changed
handlers = a: _func_: 'func' b: c: _func_: 'func' ;
components
components
contains all defined components
components = a: outerElem: outerElem params: ;