bb-better-binding
v8.1.0
Published
1 way data binding from javascript controller to html template
Downloads
88
Readme
bb-better-binding
1 way binding from js controllers to html templates
Setup
run: npm i -save bb-beter-binding
Hello World
Simple Example
your .html
template
<div bind-if="showNumbers"> </div>
<div bind-for="num in numbers">
<div bind="num"> </div>
</div>
</div>
<input onchange="${changeHandler()}"> </input>
your .js
controller
const source = require('bb-better-binding')().boot(document.firstElementChild);
source.showNumbers = true;
source.numbers = [10, 12, 16, 13];
source.changeHandler = () => {
console.log('stop changing things!!')
};
Components Example
your .html
template
<div bind-for="overdueBook in overdueBooks">
<div bind-use="libraryDue with overdueBook.dueDate overdueBook.title overdueBook.titleColor"> </div>
</div>
<div bind-component="libraryDue with date book titleColor">
<div style="color:${titleColor}; font-size:${fontSize}px" bind="book"> </div>
<div>due on $s{date}</div>
</div>
your .js
controller
const source = require('bb-better-binding')().boot(document.firstElementChild);
source.overdueBooks = [{
dueDate: '15-17-32025-02',
title: 'why humans were taller 8 billion years ago',
titleColor: 'red'
}];
source.fontSize = '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 -->
<div bind-block="todoList with 'red', name"> </div>
// parent controller
const bb = require('bb-better-binding')();
bb.declareBlock('todoList', require('./todoListBlock/todoList'));
const source = bb.boot(document.firstElementChild);
source.name = 'The Elephant\'s Todo List';
<!-- todoList.html template -->
<div style="color:${color}">List Name: $s{name}</div>
<div bind-for="item in list" bind="item"> </div>
// todoList.js controller
let template = require('fs').readFileSync(`${__dirname}/todoList.html`, 'utf8');
let controller = source => {
source.list = ['elephant', 'lion', 'rabbit'];
};
let parameters = ['color', 'name'];
module.exports = {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
<button bind-elem="playButton" onclick="${playAudio()}"> </button>
<audio src="howToStealAWalrus.mp3" type="audio/webm" bind-elem="audioBook"> </audio>
source.playButton.innerText = 'click me to begin ur audiobook!';
source.playAudio = () => source.audioBook.play();
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.
utility expressions available by default for bind-if
and bind
!
, not
<div>visibile: $s{not(x)}</div>
<div bind-if="${!(x)}"> visible if x is falsy </div>
=
, eq
, equal
<div>visibile: $s{eq(x, y)}</div>
<div bind-if="${=(x, y)}"> </div>
!=
, nEq
, notEqual
<div>visibile: $s{nEq(x, y)}</div>
<div bind-if="${!=(x, y)}"> visible if x !== y </div>
>
, greater
<div>visibile: $s{greater(x, y)}</div>
<div bind-if="${>(x, y)}"> visible if x > y </div>
<
, less
<div>visibile: $s{less(x, y)}</div>
<div bind-if="${<(x, y)}"> visible if x < y </div>
>=
, greaterEq
<div>visibile: $s{greaterEq(x, y)}</div>
<div bind-if="${>=(x, y)}"> visibile if x >= y </div>
<=
, lessEq
<div>visibile: $s{greaterEq(x, y)}</div>
<div bind-if="${<=(x, y)}"> visible if x <= y </div>
|
, ||
, or
<div>visibile: $s{or(x, y, z, w)}</div>
<div bind-if="${|(x, y, z, w)}"> visible if any argument is truthy </div>
&
, &&
, and
<div>visibile: $s{and(x, y, z, w)}</div>
<div bind-if="${&(x, y, z, w)}"> visible if all arguments are truthy </div>
avoiding infinite triggers (e.g. Maximum call stack size exceeded
)
Imagine you have the following in your template $s{func(obj)}
, and the following controller,
source.obj = {
value: 100,
count: 0
};
source.func = obj => {
obj.count++;
return obj.value;
};
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.
option 1, _bindIgnore_
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:
source.obj = {
value: 100,
count: 0,
_bindIgnore_: ['count']
};
source.func = obj => {
obj.count++;
return obj.value;
};
option 2, _bindAvoidCycles_
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:
source.obj = {
value: 100,
count: 0,
bindAvoidCycles: true
};
source.func = obj => {
obj.count++;
return obj.value;
};
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:
source.obj = {
value: 100,
count: 0
};
source.func = obj => {
obj.count++;
return obj.value;
};
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};
source.obj = obj;
source.obj.value2 = 22;
obj.value3 = 33;
2
Bindings are triggered when any property on a bound object changes.
<div bind-if="show(obj)">
hi there
</div>
source.show = obj => obj.flag;
source.obj.flag = 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.
<div bind-for="person in people">
<input bind-elem="person${index}Input"/>
</div>
<div bind-for="option in options">
<input bind-elem="option${index}" type="radio" name="options">$s{option}
</div>
let addPerson = defaultName => {
source.people.push(new Person(defaultName));
let index = source.people.length - 1;
// bad code
source[`person${index}Input`].value = defaultName;
// good code, alternative 1
source.getElem(`person${index}Input`).value = defaultName;
// good code, alternative 2
source.getElem();
source[`person${index}Input`].value = defaultName;
};
let initOptions = () => {
source.options = ['rainbow', 'unicorn', 'moon candy', 'kitten hamburger', 'fluffy headless teddy'];
// bad code
source.option0.checked = true;
// good code, alternative 1
source.getElem('option0').checked = true;
// goode code, alternative 2
source.getElem();
source.option0.checked = 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.boot(document.firstElementChild, undefined, true);
source.yummyMenu = ['apple', 'blueberry', 'grapes', 'sunlight', 'canoe'];
bb.tick();
let newMenuItemsHandler = (...items) => {
source.yummyMenu.push(...items);
bb.tick();
};
menuItemsRepository.getMenuItemsEverySecond(newMenuItemsHandler);
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 = require('bb-better-binding')();
bb.declareBlock('blockName', require('./blockPath/blockFile'));
// more block declarations ...
bb.boot(document.firstElementChild);
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: []
}
};