bb-better-binding

8.1.0 • Public • Published

bb-better-binding

1 way binding from js controllers to html templates

Setup

run: npm i -save bb-beter-binding

Hello World

controller.js

index.html

live demo

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

  1. attribute binding
  2. elem binding
  3. for binding
  4. use binding
  5. as binding
  6. if binding
  7. component binding
  8. block binding
  9. 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: []
    }
};

Readme

Keywords

none

Package Sidebar

Install

npm i bb-better-binding

Weekly Downloads

1

Version

8.1.0

License

ISC

Unpacked Size

45.3 kB

Total Files

12

Last publish

Collaborators

  • mahhov