@gogocat/data-bind

1.12.0 • Public • Published

GitHub release coverage Codacy Badge GitHub

What is dataBind?

dataBind is a light weight javaScript MV* framework aim for update DOM easier and in better managed way.

  • Declarative: dataBind simpliy bind view data to the HTML, wire events, and provides two way or one way data binding

  • High performance: dataBind is very fast. Please do try the famous dbmonster example, locate in examples/dbmonsterForOf.html and fiber /examples/fiber-demo.html compare with other frameworks

  • DOM is the source of truth: There is no vitrual DOM or complex reactive observables to worry about

  • Isolated scope: Each component only works with its own viewModel scope. No complex props pass up and down

  • zero setup: There is no need to run any build tool for development or production

  • framework agnostic : dataBind can work with any other framework. There is no need to rebuild everything in order to use it. It is design to leverage and modernise what is already working

How to use it?

For web load via script tag

<script src="dist/js/dataBind.min.js"></script>

Or node_module

npm install @gogocat/data-bind

then

 import dataBind from '@gogocat/data-bind';

Usage

The following is a very simple example shows text binding.

Most of the component logic will be in the viewModel(plain old JavaScript object).

dataBind.init will return an instance of Binder(this is the bound dataBind object).

Then just call render to start render to the page.

HTML

<section data-bind-comp="simpleComponent">
    <div>
        <h5 data-bind-text="heading"></h5>
        <p data-bind-text="description"></p>
    </div>
</section>

Js

const simpleComponentViewModel = {
    heading: 'Test heading',
    description: 'This is my test description',
};

// init data bind with view
const simpleComponent = dataBind.init(
    document.querySelector('[data-bind-comp="simpleComponent"]'),
    simpleComponentViewModel
);


// trigger render and log after render
simpleComponent
    .render()
    .then(function() {
        // for debug
        console.log(simpleComponent);
    });

To make change, just update the data in viewModel and then call render().

simpleComponentViewModel.heading='new heading';

simpleComponent.render();

render function is an asynchronous, debounced operation. So it will consolidate changes and render only once.

💡 All declarative bindings accept value or function that returns value from the viewModel.

Example: heading in the viewModel can be a value or function that returns value.

The binding can also pass-in parameters.

<h5 data-bind-text="heading($data)"></h5>

The following parameters are helpers reference $index or $data or $root. More details below

For more advance example. Please check examples/bootstrap.html.

bootstrap example shows how to use multiple, nested components and services together. Please run this example from a local server.


The init and render functions

...

// DOM ready bind viewModel with target DOM element
const simpleComponent = dataBind.init(
    document.querySelector('[data-bind-comp="simpleComponent"]'),
    simpleComponentViewModel
);

  

// trigger render, then console log for debug
simpleComponent
    .render()
    .then(function(ctx) {
        // for debug
         console.log(simpleComponent === ctx);
    });

In this simple example. First we call .init to initialise the component with the viewModle:

const  simpleComponent = dataBind.init([targetDOMElement], [viewModel]);

The returned value of dataBind.init is a instance of Binder, which is the bound component. Behind the scene, dataBind will parse the target DOM element and cache elements that has binding attributes and wire up with the viewModel. At this stage it doesn't make any change to the DOM.

The next call of render function is to render value from viewModel to the DOM (if there are difference). It returns a promise object for logic that can be trigger after the component fully rendered.

The resolver callback will receive a context object; because inside the resolver function this is refer to window.

context object is the same object as simpleComponent in this example.

To re-render the component, just call render. As mentioned, this function is an asynchronous and debounced operation. This mean, doesn't matter how many times it get call it will only make change to DOM once. Minimise browser repaint/reflow.

For edge case; pass an optional setting object when calling render to control what binding should be render or not.

simpleComponent.render({
    templateBinding: true,
    textBinding: true,
    cssBinding: true,
    ifBinding: true,
    showBinding: true,
    modelBinding: true,
    attrBinding: true,
    forOfBinding: true,
    switchBinding: true,
    changeBinding: true,
    clickBinding: true,
    dblclickBinding: true,
    blurBinding: true,
    focusBinding: true,
    hoverBinding: true,
    submitBinding: true,
});

Overwrite 'data-bind-x` namespace

// global dataBind settings

dataBind.use({
    bindingAttrs: {
        comp: 'data-xy-comp',
        tmp: 'data-xy-tmp',
        text: 'data-xy-text',
        click: 'data-xy-click',
        dblclick: 'data-xy-dblclick',
        blur: 'data-xy-blur',
        focus: 'data-xy-focus',
        hover: 'data-xy-hover',
        change: 'data-xy-change',
        submit: 'data-xy-submit',
        model: 'data-xy-model',
        show: 'data-xy-show',
        css: 'data-xy-css',
        attr: 'data-xy-attr',
        forOf: 'data-xy-for',
        if: 'data-xy-if',
        switch: 'data-xy-switch',
        case: 'data-xy-case',
        default: 'data-xy-default'
    },
});

  

// init
const simpleComponent = dataBind.init(
    document.querySelector('[data-bind-comp="simpleComponent"]'),
    simpleComponentViewModel
);

// render
simpleComponent.render();

dataBind use method can be use to set global setting of binding attribute namespace. It accept an option object showing in above example.

Visual bindings

The following bindings produce visual changes

Template binding

<section  
  data-bind-comp="simpleComponent"  
  data-bind-tmp="{id: 'exampleTemplate', data: '$root'}"
></section>


<template  id="exampleTemplate">
	<h1  data-bind-text="heading"></h1>
</template>

The attribute data-bind-tmp accept a JSON like object. id is reference to the template element id. data is reference to the data object within the bound viewModel. In this example $root means the root of the viewModel itself.

If there a 3rd option as append: true or prepend: true, the content will then append or preprend to the target container (the section tag in this example). This make building infinity scroll content very easy and efficient.

Text binding

<h1  data-bind-text="heading"></h1>

<h1  data-bind-text="fullName | uppercase"></h1>

The attribute data-bind-text is refernce to the viewModel's property 'heading'. All binding can handle deep object path reference eg. data-bind-text="childObj.myArray[1].heading"

The 2nd example shows usage of filter ' | '. The value from viewModel's property fullName will pass on to the viewModel's uppercase function that returns value to be display. Filters can be chain together one after the other. more detail below.

css binding

<h1  data-bind-css="mycCss"></h1>

The attribute data-bind-css is refernce to the viewModel's property 'mycCss'. This property can be a string of css class name, an object represend mutilple css class toggle eg. {css1: true, css2: false} or a function that returns either string or the object.

if binding

// conditional render the H1 element
<h1  data-bind-if="myCondition">
	<span>Hello</span>
</h1>

// conditditional render the DIV element and its template binding
<div  
  data-bind-if="!myCondition"  
  data-bind-tmp="{id: 'someTemplateId', data: 'someData'}"
></div>

The attribute data-bind-if is refernce to the viewModel's property 'myCondition'. This property can be a boolean or a function that returns boolean.

If myCondition is false. the children elements will be removed from DOM. When later myCondition is set to true. The elements will then render back.

With negate expression(second example above), when the expression !myCondition evaluate to true. The template binding data-bind-tmp will execute and render accordingly.

example

show binding

// conditional display the H1 element
<h1  data-bind-show="isShow">
	<span>Hello</span>
</h1>

The attribute data-bind-show is refernce to the viewModel's property 'isShow'. This property can be a boolean or a function that returns boolean. If isShow is true the element will be display, otherwise it will be hidden. It also can handle negate expression eg !isShow.

model binding

<input 
  id="userName"  
  name="userName"  
  type="text"
  data-bind-model="personalDetails.userName"
  data-bind-change="onInputChange"
  required
>

The attribute data-bind-model is refernce to the viewModel's property 'personalDetails.userName'. This property can be a string or a function that returns string. Model binding is a one-way binding operation that populate the input field value attribute with value come from the viewModel.

data-bind-model

viewModel -> DOM

For two-way data binding; use together with data-bind-change. It will update the viewModel if the value has changed and then trigger the event handler onInputChange. More detail below.

data-bind-change

DOM -> viewModel

example

attribute binding

<img  data-bind-attr="getImgAttr">

  
// js
const viewModel = {
    getImgAttr: function(oldAttrObj, $el) {
        return {
            src: '/someImage.png',
            alt: 'some image',
        };
    }
};

The attribute data-bind-attr is refernce to the viewModel's property 'getImgAttr'. This property can be a object or a function that returns object with key:value. The key is the attribute name and value is the value of that attribute.

attribute binding is useful for more complex usage together with data-bind-for binding.

Please see the <select> elements in this example

forOf binding

<p  
  data-bind-for="result of results"  
  data-bind-text="result.content"
></p>
  

// js
const viewModel = {
    results: [
        {
            content: '1'
        },
        {
            content: '2'
        },
        {
            content: '3'
        }
    ]
};

The attribute data-bind-for is refernce to the viewModel's property 'results'. It will then loop throught the data and repeat the element. The express also accept 'for-in' syntax eg result in results.

The result will looks like this:

<!--data-forOf_result_of_results-->
<p  data-bind-text="result.content">1</p>
<p  data-bind-text="result.content">2</p>
<p  data-bind-text="result.content">3</p>
<!--data-forOf_result_of_results_end-->

example

switch binding

<div  data-bind-switch="selectedStory">
    <div  data-bind-case="s1">
        <h2>Case 1</h2>
    </div>
    <div  data-bind-case="s2">
        <h2>Case 2</h2>
    </div>
    <div  data-bind-case="s3">
        <h2>Case 3</h2>
    </div>
    <div  data-bind-default="">
        <p>No story found...</p>
    </div>
</div>


// js
const viewModel = {
    selectedStory: 's1'
};

Switch binding is a specail binding that the bound element must be parent of data-bind-case or data-bind-default binding elements, and each data-bind-case or data-bind-default must be siblings.

The attribute data-bind-switch is refernce to the viewModel's property 'selectedStory'. This property can be a string or a function that returns a string.

In this example the result will looks like this, since selectedStorymatchdata-bind-case="s1"`.

<div  data-bind-switch="selectedStory">
    <div  data-bind-case="s1">
        <h2>Case 1</h2>
    </div>
</div>

example

Event bindings

The following binding produce interactivities

change binding

<input  
    id="new-todo"  
    type="text"
    data-bind-change="onAddTask"
    placeholder="What needs to be done?"
    autofocus
>


// js
const viewModel = {
    onAddTask: function(e, $el, newValue, oldValue) {
        // do something...
    },
}

data-bind-change binding is use form input elements(input, checkbox, select..etc) on change event. The bound viewModel handler onAddTask will receive the event object, bound DOM element , new value and the old value.

To make things more flexible. data-bind-change is one way binding (Data flows from DOM to viewModel).

For 2 way binding, please use Model binding together. Which does data flow from viewModel to DOM.

<div  data-bind-comp="todoComponent">
    <input  
        id="new-todo"  
        type="text"
        data-bind-change="onAddTask"
        data-bind-model="currentTask"
        placeholder="What needs to be done?"
        autofocus
    >
</div>


// js
const viewModel = {
    currentTask = '',
    onAddTask: function(e, $el, newValue, oldValue) {
        e.preventDefault();
        this.currentTask = newValue;
        // re-render
        this.APP.render();
    }
}


// init data bind with view
const toDoApp = dataBind.init(
    document.querySelector('[data-bind-comp="todoComponent"]'),
    viewModel
);

// trigger render
toDoApp.render();

In this example, we update currentTask data whenever onAddTask get called(on change) then calls this.APP.render().

Once the viewModel bound with dataBind.init call, the viewModel will be extended. APP property is the bound dataBind object.

click binding

<button
    id="clear-completed"
    data-bind-click="onClearAllCompleted"
>
    Clear completed
</button>


// js
const viewModel = {
    onClearAllCompleted: function(e, $el) {
        // do something...
    }
}

data-bind-click binding is an event handler binding for 'click' event. The handler will receive event object and the DOM element.

dblclick binding

<button  
    id="clear-completed"  
    data-bind-dblclick="onDoubleClicked"
>
    Clear completed
</button>

// js
const viewModel = {
    onDoubleClicked: function(e, $el) {
        // do something...
    }
}

data-bind-dblclick binding is an event handler binding for double click event. The handler will receive event object and the DOM element.

blur binding

<input  name="firstName"  type="text"  data-bind-blur="onBlur">

// js
const viewModel = {
    onBlur: function(e, $el) {
        // do something...
    }
}

data-bind-blur binding is an event handler binding for 'blur' event. The handler will receive event object and the DOM element.

focus binding

<input  name="firstName"  type="text"  data-bind-focus="onFocus">

// js
const viewModel = {
    onFocus: function(e, $el) {
        // do something...
    }
}

data-bind-focus binding is an event handler binding for 'focus' event. The handler will receive event object and the DOM element.

hover binding

<div  data-bind-hover="onHover">Hi</div>

  

// js
const viewModel = {
    onHover: {
        in: function(e, $el) {
            // do something when mouse in
        },
        out: function(e, $el) {
            // do something when mouse out
        }
    }
}

data-bind-hover binding is an special event handler binding for 'mouseenter' and 'mouseleave' events. The binding property must be a object with in and out functions. Each function will receive event object and the DOM element.

submit binding

<form  id="my-form"  data-bind-submit="onSubmit">

...

</form>

  

// js
const viewModel = {
    onSubmit: function(e, $el, formData) {
        // do something...
    }
}

data-bind-focus binding is an event handler binding for 'submit' event. The handler will receive event object and the DOM element and a JSON object represent the form data.

Filter

<p>Price: <span  data-bind-text="story.price | toDiscount | addGst"></span></p>

  

// js
const viewModel = {
    gstRate: 1.1,
    discountRate: 10,
    story: {
        price: 100
    },
    toDiscount: function(value) {
        return Number(value) * this.discountRate;
    },
    addGst: function(value) {
        return Number(value) * this.gstRate;
    },
}

Filter is a convenient way to carry a value and run through series of functions. In this example data-bind-text binding refernce to the viewModel property story.price. With the | filter annotation, the value 100 will then pass to toDiscount method, and then addGst methods. The last fitler's value will then use for display.

'Filter' is just simple function that recevie a value and return a value.

$data, $root and $index

<div  data-bind-for="question of questions">
    <label
        data-bind-text="question.title"
        data-bind-attr="getQuestionLabelAttr($data, $index)"
        data-bind-css="$root.labelCss"
        >
    </label>
    <input type="text" data-bind-attr="getQuestionInputAttr($data, $index)">
</div>

  

// js
const viewModel = {
    labelCss: 'form-label',
    questions: [{
        title: 'How are you?',
        fieldName: 'howAreYou',
    }],
    getQuestionLabelAttr: function(data, index, oldAttr, $el) {
        return {
            'for': `${data.fieldName}-${index}`,
        };
    },
    getQuestionInputAttr: function(data, index, oldAttr, $el) {
        return {
            'name': `${data.fieldName}-${index}`,
            'id': `${data.fieldName}-${index}`,
        };
    },
}

When using data-bind-for binding, $data is refer to the current data in the loop. $index is refer to the current loop index

$root is refer to the viewModel root level.

One time binding

<div  data-bind-if="renderIntro | once">
    <h1>Introduction</h1>
</div>

  

// js
const viewModel = {
    renderIntro: false
}

once is a reserved word in Filter logic, which does one time only binding. In this example because renderIntro is false. data-bind-if will not render the bound element, and because it has filter of once. It will not re-render the element anymore even later renderIntro is set to true. dataBind actually unbind the element after first render.

Communicate between components

dataBind use pub/sub pattern to cross comminicate between components. In the bootstrap examples

const compSearchBar = dataBind.init(
    document.querySelector('[data-bind-comp="search-bar"]'),
    viewModel
);

compSearchBar
    .render()
    .then(function(comp) {
        let self = comp;
        compSearchBar.subscribe('SEARCH-COMPLETED', self.viewModel.onSearchCompleted);
    });

  

\\ compSearchResults.js

...

compSearchResults.publish('SEARCH-COMPLETED', data);

..

Search bar component subscribed SEARCH-COMPLETED event with onSearchCompleted as handler after the initial render call.

Late on, compSearchResults component publish SEARCH-COMPLETED event with data. Which will then trigger compSearchBar component's onSearchCompleted handler.

Notice the event publisher and the event subscriber are the individual component. There is no central pub/sub channel. So multiple components can subscribe a same event and can be unsubscribe individually.

Supported events are

  • subscribe - component subscribe an event

  • subscribeOnce - component subscribe an event only once

  • unsubscribe - component unsubscribe an event

  • unsubscribeAll - component unsubscribe all events

  • publish - component publish an event

Server side rendering and rehydration

dataBind respect any server sider rendering technology. Just mark the component with data-server-rendered attribute.

<div  data-bind-comp="search-bar"  data-server-rendered>
    ...
</div>

Rehydration -

When dataBind parse a component that has data-server-rendered attribute. dataBind will not render on the initial call of render, but will parse all the bindings.

Next time calling render method will then update the view according to the viewModel.

The viewModel should has exact same data as the server side rendered version. So when later on calls render the content will update correctly.

Currently rehydration for if, forOf and switch bindings are still work in progress.

What dataBind is good for

dataBind is designed for leaverage existing infrastructure.

It is good fit for web sites that is:

  • has exisitng server side render technology eg. PHP, .Net, JSP etc

  • quickly build something to test the market but maintainable and easy to unit test

what not

  • new project - Angular, Aurelia or React... may be a better choice

  • dataBind is not an full stack soultion

  • micro component base - dataBind's component concept is not aim to be as small as a <p> tag seen in some library

What's next?

  • The next major version, already on the way, will implement dataBind with native web component. This will make micro component concept super easy, truely portable.

LICENSE

MIT.

Dependencies (0)

    Dev Dependencies (30)

    Package Sidebar

    Install

    npm i @gogocat/data-bind

    Weekly Downloads

    1

    Version

    1.12.0

    License

    MIT

    Unpacked Size

    2.6 MB

    Total Files

    148

    Last publish

    Collaborators

    • gogocat