defi.js
Data binding without framework
<input class="in" type="text">
<output class="out"></output>

<script src="defi.min.js"></script>
<script>
const obj = {};
defi.bindNode(obj, 'x', '.in, .out');
obj.x = 'Data binding without framework';
</script>

Download   Github

New! Check out React hooks powered by defi!

Introduction

Before we start, in case if you found an inaccuracy or a typo on this page, feel free to open an issue here.

defi.js bunch of utilities that enable accessor-based reactivity for JavaScript objects.

It can be installed via NPM:

npm i defi
const { bindNode, calc } = require('defi');

bindNode(obj, 'key', node)

Or downloaded to be used as a global variable

// use defi as a global variable
defi.bindNode(obj, 'key', node)

How would I use it?

As a simple task let's say you want to define a simple form with first name and last name input, where while you type a greeting appears.

<input class="first">
<input class="last">
<output class="greeting"></output>
// default data
const obj = {
  first: 'John',
  last: 'Doe'
};

// let's listen for first and last name changes
defi.on(obj, 'change:first', () => console.log('First name is changed'));
defi.on(obj, 'change:last', () => console.log('Last name is changed'));

// we would like to re-calculate 'greeting' property every time
// when the first or last are changed
defi.calc(obj, 'greeting', ['first', 'last'], (first, last) => `Hello, ${first} ${last}`);

// and we want to set up a two-way data binding between the props
// and corresponding DOM nodes
defi.bindNode(obj, {
  first: '.first',
  last: '.last',
  greeting: '.greeting'
});

If first or last is changed then event handlers print info about that to console, greeting property is updated, .greeting element is populated by calculated data (by default "Hello, John Doe"). And it happens every time when these properties are changed and it doesn't matter which way. You can do obj.first = 'Jane' or you can type text into its field, and everything will happen immediately.

That's the real accessor-based reactiveness! Check the example above here and try to type obj.first = 'Jane' at the "Console" tab.

Note that if you want to use a custom HTML element (at the example above we use <output> tag) to update its innerHTML you will need to pass so-called "binder" as a rule of how the bound element should behave. By default defi.bindNode doesn't know how to interact with non-form elements.

const htmlBinder = {
  setValue: (value, binding) => binding.node.innerHTML = value,
};
// this will update innerHTML for any element when obj.greeting is changed
defi.bindNode(obj, 'greeting', '.greeting', htmlBinder)

Also you can use html from common-binders (a collection of binders of general purpose).

const { html } = require('common-binders');
defi.bindNode(obj, 'greeting', '.greeting', html())

Also check out defi-router - a routing library for defi.js.

defi.bindNode(obj, key, node, binder, options) object

CommonJS module: 'defi/bindnode'

Binds a property of an object to HTML node, implementing two-way data binding

Skip this section if you're using defi-react because React handles DOM rendering by its own.

It creates a bridge between value of a property and a state of HTML node on the page: from a simple input to a complicated widget (the complexity of elements is unlimited). After using this function, it isn't necessary to monitor the synchronizations between model and view.

Note that a bunch of common binders can be found at common-binders package. Also the function, by default, supports all form elements without need to pass binder argument.

The function acepts three arguments: a property name, HTML node and a binding rule (a binder). In its turn, a binder is an ordinary object and it can have the following properties: on, getValue, setValue, initialize, destroy (Read more here: binder). All the five properties are optional. It also allows to declare one-way data bindings (any direction).

The bindNode function supports the many-to-many bindings. Several elements can be bound to one property and several properties can be bound to one element, including ones from different objects.

defi.bindNode(object, 'myKey', '.my-element', {
    on: 'click',
    getValue() { ... },
    setValue() { ... }
});

For example, you want to bind a property of an object to a input[type="checkbox"] node:

defi.bindNode(obj, 'myKey', '.my-checkbox', {
    // when is element state changed?
    // - after 'click' event
    on: 'click',
    // how to extract element state?
    // - return 'checked' value
    getValue: ({ node }) => node.checked,
    // how to set element state?
    // - set 'checked' value
    setValue: (v, { node }) => node.checked = !!v,
});

After binding is declared, you can set value of an object property in your most habitual way and HTML node (in this case, a checkbox) will change its state immediately. After clicking on the checkbox, the property value will be changed to the corresponding one as well.

// sets checked = true
obj.myKey = true;

More interesting example: binding object property to a jQuery UI widget (of course you can use any other library, jQuery isn't something specially supported).

<div class="my-slider"></div>
defi.bindNode(obj, 'myKey', '.my-slider', {
    // when is element state changed?
    // - after 'slide' event
    // (a function can be used to listen to any non-DOM events)
    on: (callback, { node }) => $(node).on('slide', callback),
    // how to extract element state?
    // - return 'value' of the widget
    getValue: ({ node }) => $(node).slider('option', 'value'),
    // how to set element state?
    // - set 'value'
    setValue: (v, { node }) => $(node).slider('option', 'value', v),
    // how to initialize the widget?
    // you can initialize the slider in any way,
    // but 'initialize' function provides some syntactic sugar
    initialize: ({ node }) => $(node).slider({ min: 0, max: 100 }),
});
// will set the slider value 42
obj.myKey = 42;

It looks easy but you may ask a question: "What should I do to avoid writing these rules every time?". Indeed, there can be a lot of elements of the same type on the page: text fields, drop down menus, fields from the HTML5 specification as well as third party widgets.

As observed in this documentation, the third argument is not obligatory for the ones of the bindNode function. This problem is solved by the defi.defaultBinders array which contains functions checking an HTML node against a set of rules and returns corresponding binder or undefined. You get an opportunity to reduce your code a great deal, putting binding rules into a separate part of your code and to use a syntax for binding without the third argument:

defi.bindNode(obj, 'myKey', '.my-element');

How to do it? You should add a function checking an element against a set of rules to the beginning of the defi.defaultBinders array.

const checkboxBinder = () => {
    return {
        on: 'click',
        getValue: ({ node }) => node.checked,
        setValue: (v, { node }) => node.checked = !!v,
    }
};

// the unshift function adds the function
// to the beginning of the defi.defaultBinders array
defi.defaultBinders.unshift(node => {
    // check if the element is a checkbox
    if(node.tagName == 'INPUT' && node.type == 'checkbox') {
        // if checking is OK, return a new binder
        return checkboxBinder();
    }
});
defi.bindNode(obj, 'myKey', '.my-checkbox');
obj.myKey = true;

What should you do if you need to pass arguments for initializing some plugin or a widget? You can call the function that returns a binder manually.

const uiSlider = (min, max) => {
    return {
        on: 'slide',
        getValue: ({ node }) => $(node).slider('option', 'value'),
        setValue: (v, { node }) => $(node).slider('option', 'value', v),
        initialize: ({ node }) => $(node).slider({ min, max }),
    }
};
defi.bindNode(obj, 'myKey1', '.my-slider1', uiSlider(0, 100));
defi.bindNode(obj, 'myKey2', '.my-slider2', uiSlider(1, 1000));
obj.myKey1 = 42;
obj.myKey2 = 999;

defi.defaultBinders OOB has a support for all form elements without any exception: select (including multiple), textarea, output, input (including all types from the specification of HTML5: text, checkbox, radio, range, number, date, search, time, datetime, datetime-local, color and others). That means it is not necessary to designate a binder for standard elements.

<input type="color" class="my-color-input">
defi.bindNode(obj, 'myColor', '.my-color-input');
obj.myColor = '#66bb6a';

Besides, after the binding, a new non-standard :bound(KEY) CSS selector is available for you.

defi.bindNode(obj, 'myKey', '.my-element');

// will find the element '.my-inner-element' inside '.my-element'
defi.bindNode(obj, 'myAnotherKey', ':bound(myKey) .my-inner-element');

And the syntax of possible event names is extended:

defi.bindNode(obj, 'myKey', '.my-element');

// will handle the click on the '.my-element' element
defi.on(obj, 'click::myKey', () => { ... });

// will handle the click on the '.my-element .my-inner-element'
defi.on('click::myKey(.my-inner-element)', () => { ... });

If a node is not found "Bound element is missing" error will be thrown. See an option optional: true below.

Important features of the function and special flags

The fourth argument of bindNode function is options. This object can include special flags or custom data which will be passed to bind and bind:KEY event handlers.

defi.on(obj, 'bind:x', evt => {
    console.log(evt.foo); // bar
});
defi.bindNode(obj, 'x', node, binder, { foo: 'bar' });

To understand important features of bindNode it is important to read information below but it's not required to remember all these flags.

A flag exactKey=false

If key string includes a dot then such string will be interpreted as a path to a property of nested object. The library will listen all changes of given object tree.

obj.a = { b: { c: 'foo' } };
defi.bindNode(obj, 'a.b.c', node);

obj.a.b.c = 'bar'; // updates node by bar

const oldB = obj.a.b;

obj.a.b = { c: 'baz' }; // updates node by baz

// the node is not updated because
// the connection with the object subtree is destroyed
oldB.c = 'fuu';

In case if you need to use property name as is, use exactKey flag with true value.

obj['a.b.c'] = 'foo';
defi.bindNode(obj, 'a.b.c', node, binder, {
    exactKey: true
});
obj['a.b.c'] = 'bar';

A flag getValueOnBind

When getValue is given then a state of an element will be extracted and assigned to bound property immediately after bindNode call in case if the property has undefined value. To force this behavior even if the property has non-undefined value use getValueOnbind flag with true value. To cancel this behavior use the same flag with false value.

A flag setValueOnBind

When setValue is given then the value of the property will be set as element state immediately after bindNode call in case if the property has non-undefined value. To force this behavior even if the property is undefined use setValueOnBind flag with true value. To cancel this behavior use the same flag but with false value.

Flags debounceGetValue=true and debounceSetValue=true

One of the most important feature of bindNode is that the logic of property change and the logic of element state change uses the debounce pattern. It means that if bound property is changed many times in a short time then bound element state will be updated only once after small delay (thanks to debounceSetValue=true). If element state is changed many times in a short time (eg corresponding DOM event is triggered), the property gets new value only once after minimum delay (thanks to debounceGetValue=true).

const input = document.querySelector('.my-input');
defi.bindNode(obj, 'x', input);
obj.x = 'foo';
console.log(input.value === 'foo'); // false
setTimeout(() => {
    console.log(input.value === 'foo'); // true
});

To cancel this behavior (e. g. initiate synchronous binding) use debounceSetValue and/or debounceGetValue flags with false value.

Flags debounceSetValueOnBind=false and debounceGetValueOnBind=false

As described above bindNode uses debounce pattern on property change and on bound node change. This doesn't apply to a moment when bindNode is called. To remind, when the function is called a property or a node is changed immediately. When debounceGetValueOnBind and/or debounceSetValueOnBind are set to true then debounce is turned on for binding initialization as well.

Flags debounceSetValueDelay=0 and debounceGetValueDelay=0

These flags allow to set debounce delay. debounceSetValueDelay is used when debounceSetValue or debounceSetValueOnBind is true, debounceGetValueDelay is used when debounceGetValue or debounceGetValueOnBind is true.

A flags optional=false

bindNode doesn't throw an error of missing node if optional: true is set.

A flag useExactBinder=false

Even if you pass a binder to bindNode, the framework tries to find default binder at defi.defaultBinders and extend it by properties of the passed object. This feature makes possible to use partially re-defined default binder.

For example, we want to bind input[type="text"] to a property. By default, the standard binder contains "on" property with "input" value for this kind of node. It means that the value of the object property and node state will be synchronized when a user releases a key of the keyboard or pastes text from clipboard. In case if you want synchronization to be performed after the "blur" DOM event, you need to pass an object containing the only "on" property as the third argument. This object will extend the default binder, having retained getValue and setValue values.

defi.bindNode(obj, 'myKey', '.my-input', { on: 'blur' });

To cancel this behavior and use the binder as is, you can use useExactBinder flag with true value.

defi.bindNode(obj, 'x', node, binder, {
    useExactBinder: true
});

Returns object - object

Fires bind bind:KEY

Arguments

NameTypeDetails
objobject

A target object

keystring

A property name

nodestring node $nodes

An HTML element which must be bound to a key

binder optionalbinder

A binder containing the following properties: on , getValue, setValue, initialize, destroy. You can get more detailed information about binders in their documentation: see binder

options optionalobject

Options object which accepts "silent" (don't fire "bind" and "bind:KEY"), flags described above or custom data

Links

defi.bindNode(obj, bindings, binder, options) object

Alternative syntax: passing of an object

To the defi.bindNode function an object can be passed to avoid multiple invocation of the function and reduce code. Keys of the object are property names and values can get the following look:

  • A node;
  • An object with properties node and binder;
  • An array of objects with properties node and binder;

If binder arg is passed as the second argument then it wil be used as the binder for those elements for which a binder wasn't specified.

Returns object - object

Arguments

NameTypeDetails
objobject

A target object

bindingsobject

(see the example)

binder optionalbinder

(see above)

options optionalobject

(see above)

Examples

defi.bindNode(obj, {
	foo: '.custom-checkbox',
	'bar.length': 'textarea'
});
defi.bindNode(obj, {
	foo: {
		node: ':bound(x) .aaa',
		binder: myBinder()
	},
	bar: '.bbb',
	baz: [{
		node: '.ccc'
	}, {
		node: document.querySelector('.ddd'),
		binder: myBinder('baz')
	}]
}, {
	// will be used as a binder for .bbb and .ccc
	setValue(value) {
		foo(value);
	}
});

defi.bound(obj, key, options)

CommonJS module: 'defi/bound'

Returns a bound element

Skip this section if you're using defi-react because React handles DOM rendering by its own.

Arguments

NameTypeDetails
objobject

A target object

keystring

A name of a property, which bound elements you want to get

optionsobject

You can pass all: true if you want to get all elements bound to the key

Examples

defi.bindNode(obj, 'x', '.my-element');
defi.bound(obj, 'x'); // will return document.querySelector('.my-element')
defi.bindNode(obj, 'x', '.my-element');
defi.bound(obj, 'x', { all: true }); // will return an array of elements bound to "x"

defi.calc(obj, targetKey, source, handler=(v)=>v, options)

CommonJS module: 'defi/calc'

Creates a dependency of one property value on values of others

calc creates a dependency of a property (target argument) on values of other properties (source argument). When source property is changed, target is re-calculated automatically.

source arg has a few variations.

A string

A target property is dependent on source property.

obj.b = 1;
defi.calc(obj, 'a', 'b', b => b * 2);
console.log(obj.a); // 2

An array of strings

A target is dependent on properties listed at source array.

obj.b = 1;
obj.c = 2;
obj.d = 3;
defi.calc(obj, 'a', ['b', 'c', 'd'], (b, c, d) => b + c + d);
console.log(obj.a); // 6

An object with properties object and key

At this case target property is dependent on a property from another object.

const someObject = { b: 1 };
defi.calc(obj, 'a', {
    object: someObject,
    key: 'b'
}, b => b * 2);

console.log(obj.a); // 2

key property also accepts an array of property names.

const someObject = {
    b: 1,
    c: 2,
    d: 3
};
defi.calc(obj, 'a', {
    object: someObject,
    key: ['b', 'c', 'd']
}, (b, c, d) => b + c + d);

console.log(obj.a); // 6

An array of object with properties object and key

This variation allows to define dependency from properties of different objects.

const someObjectX = {
    b: 1,
    c: 2
};
const someObjectY = {
    d: 3
};
defi.calc(obj, 'a', [{
    object: someObjectX,
    key: ['b', 'c']
}, {
    object: someObjectY,
    key: 'd'
}], (b, c, d) => b + c + d);

console.log(obj.a); // 6

A combination of strings (own properties) and objects

obj.b = 1;
obj.c = 2;

const someObject = {
    d: 3,
    e: 4
};

defi.calc(obj, 'a', ['b', 'c', {
    object: someObject,
    key: ['d', 'e']
}], (b, c, d, e) => b + c + d + e);

console.log(obj.a); // 10

For reasons of code purity, the combination of strings and objects inside source array is not recommended. Instead, pass an object whose object property refers to source object. An example below makes the same job as shown at the previous example.

obj.b = 1;
obj.c = 2;

const someObject = {
    d: 3,
    e: 4
};

defi.calc(obj, 'a', [{
    object: obj, // the target object
    keys: ['b', 'c']
}, {
    object: someObjectX,
    key: ['d', 'e']
}], (b, c, d, e) => b + c + d + e);

console.log(obj.a); // 10

A path to source property

If source property name includes a dot then the function initiates a dependency on a property from nested object.

obj.b = { c: { d: 1 } };
obj.e = { f: { g: 2 } };

defi.calc(obj, 'a', ['b.c.d', 'e.f.g'], (d, g) => d + g);

console.log(obj.a); // 3

The same thing works for external sources.

obj.b = { c: { d: 1 } };
const someObject = { e: { f: { g: 2 } } };

defi.calc(obj, 'a', [{
    object: obj
    key: 'b.c.d'
}, {
    object: someObject
    key; 'e.f.g'
}], (d, g) => d + g);

console.log(obj.a); // 3

The function is protected from circular references (for example, a depends on b, b depends on c and c depends on a) and if there is a calculation problem, it does not block the page and does not throw an exception about the stack over-flow.

As you may noticed, arguments of handler function always follow the same order as source properties appear.

In case if you want to change a value of one source property and make it so that target property will not be recalculated, then use defi.set function with skipCalc flag.

defi.calc(obj, 'a', 'b', handler);
defi.set(obj, 'b', newValue, {
    skipCalc: true
});

Important features of the function and special flags

The fifth argument of calc function is options (you also can call them just "options"). As usual this object can include special flags or custom data which will be passed to change:TARGET event handler.

defi.on(obj, 'change:a', evt => {
    console.log(evt.foo); // 'bar'
});

defi.calc(obj, 'a', source, handler, { foo: 'bar' });

A flag debounceCalc=true

After calc is called, a target property is calculated with no delays. But when source property is changed the debounce pattern is used. That means that target property will be changed in few milliseconds and only once even if source properties was changed many times in a short time.

obj.b = 1;
obj.c = 2;
obj.d = 3;

defi.calc(obj, 'a', ['b', 'c', 'd'], (b, c, d) => b + c + d);

defi.on(obj, 'change:a', () => {
    // the handler will be called only once
    // despite that source properties was changed thrice
    console.log(`a is changed to ${obj.a}`); // a is changed to 60
});

obj.b = 10;
obj.c = 20;
obj.d = 30;
console.log(obj.a); // 6 instead of 60

To cancel debounce pattern when source properties are changed, in other words to make the calculation synchronously pass debounceCalc with false value to the function.

obj.b = 1;
obj.c = 2;
obj.d = 3;

defi.calc(obj, 'a', ['b', 'c', 'd'], (b, c, d) => b + c + d, {
    debounceCalc: false
});

defi.on(obj, 'change:a', () => {
    // the handler will be called thrice
    // every time when b, c or d are changed

    // a is changed to... 15, 33, 60
    console.log(`a is changed to ${obj.a}`);
});

obj.b = 10;
obj.c = 20;
obj.d = 30;
console.log(obj.a); // 60

A flag debounceCalcOnInit=false

As described above, target property is calculated immediately after the calc is called. To turn on debounce on calc call pass debounceCalcOnInit with true value to the function.

defi.on(obj, 'change:a', () => {
    // the handler will be called only once in a moment
    console.log(`a is changed to ${obj.a}`); // a is changed to 6
});

obj.b = 1;
obj.c = 2;
obj.d = 3;

defi.calc(obj, 'a', ['b', 'c', 'd'], (b, c, d) => b + c + d, {
    debounceCalcOnInit: true
});

console.log(obj.a); // undefined

In real world debounceCalcOnInit flag is unlikely to be useful. Just keep in mind that you can enable "total debounce" if needed.

A flag debounceCalcDelay=0

The flag can be used to set debounce delay when debounceCalc or debounceCalcOnInit is set as true.

A flag setOnInit=true

It is known that target property gets new value after calc is called. To cancel this behavior and don't calculate a property immediately use setOnInit with false value.

defi.calc(obj, 'a', 'b', b => b * 2, {
    setOnInit: false
});

console.log(obj.a); // undefined

// but if obj.b is changed the target property will be calculated
obj.b = 1;

A flag exactKey=false

As described above, it's possible to use a path to source property using a string that contains dots. In case if you need to use exact name of source property use exactKey with true value.

obj['foo.bar.baz'] = 1;
defi.calc(obj, 'a', 'foo.bar.baz', fooBarBaz => fooBarBaz * 2, {
    exactKey: true
});
console.log(obj.a); // 2

A flag promiseCalc=false

This flag allows to return Promise instance from the calculating function. Target property gets its value from resolved promise.

Warning! Promise cannot be canceled. Use the promiseCalc feature carefully and don't allow multiple calls of heavy functions.

defi.calc(obj, 'a', ['b', 'c'], (b, c) => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(a + b)
        }, 1000);
    });
}, {
    promiseCalc: true
});

obj.b = 1;
obj.c = 2;

// "a" will be changed in a second
defi.calc(obj, 'response', 'data', async (data) => {
    const resp = await fetch(url, {
        method: 'post',
        body: data
    });

    return resp.json();
}, {
    promiseCalc: true
});

Arguments

NameDefaultTypeDetails
objobject

A target object

targetKeystring

A property which needs to be calculated

sourcestring array

Which properties the target property is depended on

handler optional(v)=>vfunction

A function which returns a new value

options optionalobject

An object which can contain some special flags or data for change:KEY handler

Examples

defi.calc(obj, 'greeting', 'name', name => `Hello, ${name}!`);

obj.name = 'World';

// ... in a moment
alert(obj.greeting); // 'Hello, World!'

The calculation of the rectangle perimeter with two sides known (and the calculation of the sides with the perimeter known)

obj.a = 3;
obj.b = 4;

obj.chain(obj)
    .calc('p', ['a', 'b'], (a, b) => (a + b) * 2)
    .calc('a', ['p', 'b'], (p, b) => p/2 - b)
    .calc('b', ['p', 'a'], (p, a) => p/2 - a);

alert(obj.p); // 14

defi.on(obj, 'change:p', () => {
    // "The perimeter has been changed and equals 18"
    console.log(`The perimeter has been changed and equals ${obj.p}`);
});

obj.a = 5;

defi.calc(obj, batch, options)

Extra syntax for defi.calc. Allows to define few calculated properties per single call of the function.

The first argument is an object whose keys are property names and values are objects with the following data:

  • source - which properties the target property is depended on;
  • handler - a function which returns a new value of a property (by default it equals to (value) => value);
  • options - calc options.

The third argument contains common options which extend options of every item (but they still have higher priority than common options).

source can take any kind of look as described above (a string, an array of strings etc).

Arguments

NameTypeDetails
objobject

A target object

batcharray

An object which includes all information about calculated properties

options optionalobject

Options which are common for all listed calculated properties

Examples

defi.calc(obj, {
	x: {
    	source: ['a', 'b'],
    	handler: (a, b) => a + b
	},
	y: {
	    source: {
	        object: someObject,
	        key: 'c'
	    },
	    options: {
	        setOnInit: false
	    }
	},
	z: {
	    source: [{
	        object: this,
	        key: 'x'
	    }, {
	        object: someObject,
	        key: 'd'
	    }],
	    handler: (x, d) => x + d
	}
}, {
    debounceCalc: false
});

defi.chain(obj) object

CommonJS module: 'defi/chain'

Allows chained calls of defi.js functions

The function accepts any object and returns an instance of a class which adopts functions allowing them to be called in a chain.

Returns object - An instance of the class which adopts defi functions

Arguments

NameTypeDetails
objobject

An object

Examples

defi.chain(obj)
    .calc('a', 'b', b => b * 2)
    .set('b', 3)
    .bindNode('c', '.node');

// the same as
// defi.calc(obj, 'a', 'b', b => b * 2)
// defi.set(obj, 'b', 3)
// defi.bindNode(obj, 'c', '.node');

defi.lookForBinder(node) binder

CommonJS module: 'defi/lookforbinder'

Skip this section if you're using defi-react because React handles DOM rendering by its own.

Returns a binder corresponding to an element. If it is not found, it returns undefined. The function uses defi.defaultBinders for the search.

Returns binder - binder

Arguments

NameType
nodenode

Links

Examples

const element = document.createElement('input');
element.type = 'text';

console.log(defi.lookForBinder(element));

// will return something similar to the following object
{
	on: 'input',
	getValue: ({ node }) => node.value,
	setValue: (v, { node }) => node.value = v,
}

defi.mediate(obj, key, mediator) object

CommonJS module: 'defi/mediate'

Transforms property value on its changing

This function is used for transforming property value on its changing. For example, you want the property value to be always either of a certain type or an integer value, or to be no less than zero and no more than a hundred etc.

Returns object - obj

Arguments

NameTypeDetails
objobject

A target object

keystring array

A key or an array of keys

mediatorfunction

A function-mediator which returns a new value. It gets the following arguments: new value, previous value, a key, an object itself

Examples

defi.mediate(obj, 'x', value => String(value));

obj.x = 1;

alert(typeof obj.x); // "string"

An array of keys

defi.mediate(obj, ['x', 'y'], value => String(value));

defi.mediate(obj, keyMediatorPairs)

Alternative syntax of the defi.mediate function which accepts "key-mediator" object as an argument

Arguments

NameTypeDetails
objobject

A target object

keyMediatorPairsobject

An object with key-mediator properties

Examples

defi.mediate(obj, {
	x: String,
	y: Number,
	z: Boolean
});
obj.x = 1;
obj.y = 2;
obj.z = 3;
alert(typeof obj.x); // "string"
alert(typeof obj.y); // "number"
alert(typeof obj.z); // "boolean"

defi.off(obj, names, callback) object

CommonJS module: 'defi/off'

Deletes an event handler

It deletes a handler which has been created before. All arguments (except of a target object of course) are optional. You can delete both all the events (without passing event names) and separate ones (having passed only the event name, the event name and the handler).

Returns object - obj

Fires removeevent removeevent:NAME

Arguments

NameTypeDetails
objobject

A target object

names optionaleventNames

An event name or a an array of event names

callback optionaleventHandler

A function-handler

Links

Examples

defi.off(obj, 'change:x');
defi.off(obj, ['change:x', 'bind']);

The deletion of all events

defi.off(obj);

The deletion of an event with definite handler

const handler = function() {
	//...
}
defi.on(obj, 'change:x', handler);
defi.off(obj, 'change:x', handler);

defi.on(obj, names, callback, options) object

CommonJS module: 'defi/on'

Adds an event handler

The function adds an event handler for an object. Refer to the complete list of possible events with the description here: eventNames. Also check out a short article about events.

Returns object - obj

Fires addevent addevent:NAME

Arguments

NameTypeDetails
objobject

A target object

nameseventNames

An event name or an array of names

callbackeventHandler

A function which is caused by the event

optionsobject

Options object where triggerOnInit (boolean) makes the handler called immediately after event initialization, once (boolean) makes the handler called only once, debounce (boolean or # of milliseconds) debounces the handler

Links

Examples

defi.on(obj, 'foo', () => {
	alert('A custom Event is fired');
});

defi.trigger(obj, 'foo');

Using Symbol as an event name

const foo = Symbol('foo');
defi.on(obj, foo, () => {
	alert('A custom Event is fired');
});

defi.trigger(obj, foo);

Using an array of event names

const bar = Symbol('foo');

defi.on(obj, ['foo', bar, 'baz'], () => {
	alert('A custom Event is fired');
});

defi.trigger(obj, 'foo');
defi.trigger(obj, bar);
defi.trigger(obj, 'baz');

Calling a handler immediately after event initialization

// Displays "bar" at once and waits for a firing of "foo" event
defi.on(obj, 'foo', () => {
	alert('bar');
}, { triggerOnInit: true });

Calling a handler only once

defi.on(obj, 'foo', () => {
	alert('bar');
}, { once: true });

defi.trigger(obj, 'foo'); // displays "bar"
defi.trigger(obj, 'foo'); // does nothing
defi.trigger(obj, 'foo'); // does nothing

defi.on(obj, evtnameHandlerObject, options, obj)

Alternative syntax: "eventname-handler" pairs

In the defi.on function the object with the key-event pairs can be passed to avoid multiple invocation of the function.

Arguments

NameTypeDetails
objobject

A target object

evtnameHandlerObjectobject

An object where keys are event names and values are event handlers

optionsobject

See above.

objobject

A target object

Examples

defi.on(obj, {
	'custom': evt => ...,
	'click::x': evt => ...,
	'change:y': evt => ...,
	[Symbol.for('a symbolic event')]: evt => ...,
});

defi.remove(obj, key, eventOptions) object

CommonJS module: 'defi/remove'

Deletes a property and removes dependent handlers

Returns object - obj

Fires delete delete:KEY

Arguments

NameTypeDetails
objobject

A target object

keystring

A property name or an array of names to remove

eventOptions optionaleventOptions

An event options

Examples

defi.remove(obj, 'myKey');
defi.remove(obj, ['myKey1', 'myKey2']);

Using eventOptions

defi.remove(obj, 'myKey', {
	silent: true
});

defi.set(obj, key, value, eventOptions)

CommonJS module: 'defi/set'

Sets a property value allowing to pass an event options object

The list of supported flags:

  • silent - do not call the change and change:KEY events
  • silentHTML - do not change states of bound HTML nodes
  • force - call the change and change:KEY events even though the property value has not been changed
  • forceHTML - change a state of bound element even though the property value has not been changed. This option is usable if the bound element has been rendered after the binding (for example, some option tags have been added to select tag)
  • skipMediator - prevents the property transformation by defi.mediate
  • skipCalc - prevents the work of dependencies created with defi.calc
  • define - makes the property to be "listenable" by "change" event by setting defi-specific accessors

Fires change change:KEY beforechange beforechange:KEY

Arguments

NameTypeDetails
objobject

A target object

keystring

A key

value*

A value

eventOptions optionaleventOptions

Event options

Examples

defi.on(obj, 'change:myKey', evt => {
	alert(evt.value);
});

// the same as obj['myKey'] = 3
// or obj.myKey = 3
// alerts 3
defi.set(obj, 'myKey', 3);

Using eventOptions

defi.on(obj, 'change:myKey', evt => {
	alert(evt.value);
});

// the handler isn't fired
defi.set(obj, 'myKey', 4, {
	silent: true
});

Passing custom data to a handler

defi.on(obj, 'change:myKey', evt => {
	alert(evt.myCustomFlag);
});

// alerts 42
defi.set(obj, 'myKey', 4, {
	myCustomFlag: 42
});

defi.set(obj, keyValuePairs, eventOptions)

Alternative "key-value" syntax of the defi.set function

Arguments

NameTypeDetails
objobject

A target object

keyValuePairsobject

An object containing key-value pairs

eventOptions optionaleventOptions

An event object

Examples

defi.set(obj, {
	myKey1: 1,
	myKey2: 2
});

Passing eventOptions as a second argument

defi.set(obj, {
	myKey: 3
}, {
	myFlag: 'foo'
});

defi.trigger(obj, names, arg) object

CommonJS module: 'defi/trigger'

Fires an event

After adding event handlers using defi.on any event can be fired manually using this function.

Returns object - obj

Arguments

NameTypeDetails
objobject

A target object

names optionaleventNames

An event name or an array of names

arg optional*

Any arguments which will be passed to every event handler

Links

Examples

defi.on(obj, ['foo', 'bar'], (a, b, c) => {
	alert(a + b + c);
});
defi.trigger(obj, 'foo', 1, 2, 3); // alerts 6

defi.unbindNode(obj, key, node, eventOptions) object

CommonJS module: 'defi/unbindnode'

Destroys a binding between given property and HTML node

Skip this section if you're using defi-react because React handles DOM rendering by its own.

Using this function you can delete a binding between a property and HTML node, which has been added recently and no longer needed.

Returns object - obj

Fires unbind unbind:KEY

Arguments

NameTypeDetails
objobject

A target object

keystring null

A key or an array of keys. If you pass null instead of the key, all bindings for the given object will be deleted

node optionalstring node $nodes

HTML node

eventOptions optionaleventOptions

Event object ("silent" key disables firing the events "unbind" and "unbind:KEY")

Examples

defi.bindNode(obj, 'myKey', '.my-element');

// changes the property value and the state of the HTML element
obj.myKey = true;

defi.unbindNode(obj, 'myKey', '.my-element');

// only the property value is being changed now
obj.myKey = false;

defi.unbindNode(obj, bindings, eventOptions) object

Alternative syntax which allows to pass an object with bindings to unbindNode. Look at defi.bindNode(2) for more information

Returns object - obj

Arguments

NameTypeDetails
objobject

A target object

bindingsobject

(see the example)

eventOptions optionaleventOptions

(see above)

Examples

defi.unbindNode(obj, {
	foo: '.aaa'
	bar: {
		node: '.bbb'
	},
	baz: [{
		node: '.ccc'
	}, {
		node: '.ddd'
	}]
});

defi.defaultBinders: array

CommonJS module: 'defi/defaultbinders'

An array of functions which return a corresponding binder or a falsy value

Skip this section if you're using defi-react because React handles DOM rendering by its own.

defaultBinders is the array of functions which check an element in turn against given rules in these functions and return a binder (see binder). This array is used when the third argument has not been passed to the defi.bindNode function. See more detailed information about bindings in defi.bindNode documentation.

Links

Examples

defi.defaultBinders.unshift(element => {
	// check if the element has "foo" class name
	if(element.classList.contains('foo')) {
		// if checking is OK, return a new binder
		return {
			on: ...,
			getValue: ...,
			setValue: ...
		};
	}
});

// ...

defi.bindNode(obj, 'myKey', '.foo.bar');

eventHandler function

An event handler. Takes any arguments passed to defi.trigger

Arguments

NameTypeDetails
options*Any arguments

Examples

const eventHandler = (...args) => {
	console.log(args);
};
defi.on(obj, 'fyeah', eventHandler);
// logs 'foo', 'bar', 'baz'
defi.trigger(obj, 'fyeah', 'foo', 'bar', 'baz');

eventNames string symbol

Event name or an array of event names.

Custom events.
defi.on(obj, ['myevent1', Symbol.for('myevent2')], () => {...});
defi.trigger(obj, Symbol.for('myevent2'));
change:KEY which is triggered every time when a property is changed.
defi.on(obj, 'change:x', evt => {...});
obj.x = 42;
beforechange:KEY which is triggered every time before a property is changed.
defi.on(obj, 'beforechange:x', evt => {...});
obj.x = 42;
addevent:NAME and addevent which are triggered on event add.
// for any event
defi.on(obj, 'addevent', evt => {...});
// for "someevent" event
defi.on(obj, 'addevent:someevent', evt => {...});
// the line below fires "addevent" and "addevent:someevent"
defi.on(obj, 'someevent', evt => {...});
removeevent:NAME and removeevent which are triggered on event remove.
// for any event
defi.on(obj, 'removeevent', evt => {...});
// for "someevent" event
defi.on(obj, 'removeevent:someevent', evt => {...});
// the line below fires "removeevent" and "removeevent:someevent"
defi.off(obj, 'someevent', evt => {...});
DOM_EVENT::KEY, where DOM_EVENT is a name of DOM event, KEY is a key. A handler is called when DOM_EVENT is triggered on a node which is bound to the KEY.
defi.bindNode(obj, 'x', '.my-div');
defi.on(obj, 'click::x', evt => {
    alert('clicked ".my-div"');
});
DOM_EVENT::KEY(SELECTOR), where DOM_EVENT is a name of DOM event, KEY is a key, SELECTOR is a selector. A handler is called when DOM_EVENT is triggered on a node which matches the SELECTOR within a node bound to the KEY.
<div class="my-div">
    <button class="my-button"></button>
</div>
defi.bindNode(obj, 'x', '.my-div');
defi.on(obj, 'click::x(.my-button)', evt => {
    alert('clicked ".my-button"');
});
Delegated events: PATH@EVENT, where PATH is a path to a target object whose events we want to listen, EVENT is an event name.
defi.on(obj, 'a@someevent', () => {...});
defi.on(obj, 'a.b.c@change:d', () => {...});
Any combinations. All events described above can be combined.
defi.on(obj, 'x.y.z@click::u(.my-selector)', () => {...});

binder object

binder contains all information about how to synchronize an object property value with DOM node state. Every member of a binder uses HTML node as its context (this)

Properties

NameTypeDetails
on optionalstring function

DOM event (or space-delimited list of events) which tells when the node state is changed. Besides, it accepts a function as a value if you need to customize a listener definition

getValue optionalfunction

A function which tells how to retrieve a value (state) from HTML node when DOM event is fired

setValue optionalfunction

A function which tells how to change DOM node when the property value is changed

initialize optionalfunction

A function which is called before binding is launched. For example it can initialize a JavaScript plugin or something else

destroy optionalfunction

A function which is called when a binding is removed using unbindNode function

Examples

const binder = {
	on: 'click',
	getValue: (bindingOptions) => bindingOptions.node.value,
	setValue(v, bindingOptions) => bindingOptions.node.value = v,
	initialize: (bindingOptions) => alert('A binding is initialized'),
	destroy: (bindingOptions) => alert('A binding is destroyed'),
};

defi.bindNode(obj, 'a', '.my-checkbox', binder);
const binder = {
	on: (callback, bindingOptions) => bindingOptions.node.onclick = callback,
	// ...
};
// ...

eventOptions object

An object which can contain service flags or custom data which will be passed to an event handler

Examples

const eventOptions = { silent: true };

obj.a = 1;

defi.on(obj, 'change:a', () => {
	alert('a is changed');
});

defi.set(obj, 'a', 2, eventOptions); // no alert
const eventOptions = { f: 'yeah' };

obj.a = 1;

defi.on(obj, 'change:a', eventOptions => {
	alert(eventOptions.f);
});

defi.set(obj, 'a', 2, eventOptions); // alerts "yeah"

node

A DOM node

Examples

const node = document.querySelector('.foo');

$nodes

DOM nodes collection (NodeList, array, etc).

Examples

let $nodes = $('.foo');
$nodes = document.querySelectorAll('.bar');

string

A string

Examples

const foo = 'bar';

symbol

A symbol

Examples

const foo = Symbol('foo');
const bar = Symbol.for('bar');

boolean

A boolean

Examples

const bool = true;

number

A number

Examples

const num = 42;

object

An object

Examples

const obj = {
	foo: 'x',
	['bar']: 'y'
};

array

An array

Examples

const arr = ['foo', undefined, null, () => {}];

function

A function

Examples

function comeOnBarbieLetsGoParty() {
	alert("I'm a Barbie girl, in a Barbie world");
}

null

null, just null

Examples

const x = null;

*

Any type

Examples

let whatever = 'foo';
whatever = 42;

The events

The article explains events in defi.js. They can be called a heart of defi.js because they power all the magic happened at calc, bindNode, and other methods.

Basics

Custom events

Let’s start with the simplest thing. In defi.js events can be added with the help of on method.

const handler = () => {
  alert('"someevent" is fired');
};
defi.on(object, 'someevent', handler);

Where the list of events can be passed to.

defi.on(object, ['someevent1', 'someevent2'], handler);

Events can be fired with trigger method.

defi.trigger(object, 'someevent');

At the same time, you can pass some data to the handler having determined the first and the following arguments.

defi.on(object, 'someevent', (a, b, c) => {
  alert([a, b, c]); // 1,2,3
});
defi.trigger(object, 'someevent', 1, 2, 3);

on method accepts options which can affect on cases when the handler should be called. once: true makes the handler called only once.

defi.on(object, 'someevent', handler, { once: true });

defi.trigger(object, 'someevent'); // the handler is called
defi.trigger(object, 'someevent'); // the handler isn't called any more

debounce: true or debounce: delay "debounces" the handler. When an event fires out, the timer with the specified delay by a programmer starts. If no event with the same name is called upon the expiry of the timer, a handler is called. If an event fires before the delay is over, the timer updates and waits again. This is the implementation of a very popular "debounce" micropattern which you can read about on this page.

defi.on(object, 'someevent', () => {
  alert('yep');
}, { debounce: 500 });
for(let i = 0; i < 1000; i++) {
  defi.trigger(object, 'someevent');
}
// it will show ‘yep’ only once in 500ms

Events of property changing

When a property is changed, defi.js fires an event: "change:KEY".

defi.on(object, 'change:x', () => {
    alert('x is changed');
});
object.x = 42;

In case you want to pass some data to the event handler or change a property value without calling "change:KEY" event, instead of a usual assignment use defi.set method which accepts three arguments: a key, a value and an object with data or special flags.

defi.on(object, 'change:x', evt => {
    alert(evt.someData);
});

defi.set(object, 'x', 42, { someData: 'foo' });

You can change a property without calling an event handler in this way:

// changing doesn’t fire an event
defi.set(object, 'x', 9000, { silent: true });

set method supports some more flags, the description of which would make us go beyond the topic of the article, so I refer you to the documentation of the method.

Events which are being fired before a property changing

"beforechange:KEY" is being fired before a property changing. The event can be useful in cases you define "change:KEY" event and want to call the code which precedes this event.

defi.on(object, 'beforechange:x', () => {
    alert('x will be changed in a few microseconds');
});

As usually, you can pass some data to the handler or cancel an event triggering.

defi.on(object,'beforechange:x', evt => {
    alert(evt.someData);
});

defi.set(object, 'x', 42, { someData: 'foo' });

// changing doesn’t fire an event
defi.set(object, 'x', 9000, { silent: true });

Events of a property removing

On removing properties with defi.remove method, "delete:KEY" and delete events are fired.

defi.on(object, 'delete:x', () => {
    alert('x is deleted');
});

defi.on(object, 'delete', evt => {
  alert(`${evt.key} is deleted`);
});

defi.remove(object, 'x');

Binding events

On the binding declaration two events: "bind" and "bind:KEY" are fired, where KEY is a key of a bound property.

defi.on(object, 'bind:x', () => {
    alert('x is bound');
});

defi.on(object, 'bind', evt => {
    alert(`${evt.key} is bound`);
});

defi.bindNode(object, 'x', '.my-node');

This event can be of use, for example, when you need to execute your code after binding made in a separate module.

The events of event adding/removing

When an event is added, "addevent" and "addevent:NAME" events are fired, and when an event is removed, "removeevent" and "removeevent:NAME" events are fired, where NAME is an event name.

defi.on(object, 'addevent', handler);
defi.on(object, 'addevent:someevent', handler);
defi.on(object, 'removeevent', handler);
defi.on(object, 'removeevent:someevent', handler);

One of the ways of its application can be the use of defi.js as an event engine of a third-party library. Let’s say, you want to place all handlers of all external libraries into one on call, having made the code more readable and compact. With the help of addevent you catch all further event initializations, and in the handler you check an event name against some conditions and initialize an event using API of a third-party library.

In the example below there’s a code from a project which uses Fabric.js. "addevent" handler checks an event name for the presence of "fabric:" prefix and if checking is passed, it adds the corresponding handler to the canvas with the help of Fabric API.

object.canvas = new fabric.Canvas(node);
defi.on(object,{
    'addevent': evt => {
        const { name, callback } = evt;
        const prefix = 'fabric:';
        if(name.indexOf(prefix) == 0) {
            const fabricEventName = name.slice(prefix.length);
            // add an event to the canvas
            object.canvas.on(fabricEventName, callback);
        }
    },
    'fabric:after:render': evt => {
        object.data = object.canvas.toObject();
    },
    'fabric:object:selected': evt => { /* ... */ }
});

Delegated events

Now let’s get down to the most interesting: event delegations. The syntax of delegated events is as follows: "PATH@EVENT_NAME", where PATH is a path (properties are separated by a dot) to an object which EVENT_NAME event needs to be added to. Let’s consider examples below.

Example 1

You want to add an event handler in "a" property which in its turn is an object.

defi.on(object, 'a@someevent', handler);

The handler will be called when "someevent" event has been fired in the "a" object.

defi.trigger(obj.a, 'someevent');

Also, the handler can be declared before "a" property is set. If "a" property is rewritten into another object, inner mechanism of the library will catch this change, remove the handler from the previous property value and add it to a new value (if the new value is an object as well).

obj.a = {};
defi.trigger(obj.a, 'someevent');

The handler will be called again.

Example 2

Let’s go deeper. Suppose we have "a" property that contains an object with "b" property, in which "someevent" event must be fired. In this case properties are separated by a dot:

defi.on(object, 'a.b@someevent', handler);
defi.trigger(object.a.b, 'someevent');

Example 3

Besides custom events, you can use the ones which are built in defi.js as well. Instead of "someevent" you can use "change:KEY" event described above.

// in “a” object there’s “b” object,
// in which we listen to changes of “c” property.
defi.on(object, 'a.b@change:c', handler);

Let me remind you that delegated events are added dynamically. On declaring a handler any branch of the way may be absent. If anything is overridden in the object tree, the binding to the old value is disrupted and a new one is created with a new value:

defi.on(object, 'a.b.c.d@someevent', handler);
object.a.b = { c: { d: {} } };
defi.trigger(object.a.b.c.d, 'someevent');

DOM events

defi.js is known to allow bindings of DOM elements on a page to some object properties implementing one-way or two-way data binding:

defi.bindNode(object, 'x', '.my-node');

More detailed information about bindNode method.

Before or after the declaration of the binding you can create a handler that listens to DOM events of the bound element. The syntax is as follows: "DOM_EVENT::KEY", where DOM_EVENT is DOM event, and KEY is a key of a bound property. DOM_EVENT and KEY are separated by a double colon.

defi.on(object, 'click::x', evt => {
  evt.preventDefault();
});

The object of original DOM event is under "domEvent" key of the event object passed to the handler. Besides, there are several properties and methods available in the object so as not to address "domEvent" every time: "preventDefault", "stopPropagation", "which", "target" and some other properties.

This opportunity is just a syntactic sugar over ordinary DOM events and the code below does the same things as the previous one:

document.querySelector('.my-node').addEventListener('click', evt => {
    evt.preventDefault();
});

Delegated DOM events

Event declaring from the example above requires binding declaring. You must take two steps: call bindNode method and declare the event as such. It isn’t always convenient because there are often some cases when DOM node isn’t used anywhere except only one DOM event. For this case there is another syntax variant of DOM events which looks like "DOM_EVENT::KEY(SELECTOR)". In this case KEY is some key bound to some DOM node. And SELECTOR is a selector of DOM node that is a child of the one bound to KEY.

HTML:

<div class="my-node">
    <span class="my-inner-node"></span>
</div>

JS:

defi.bindNode(object, 'x', '.my-node');
defi.on(object, 'click::x(.my-inner-node)', handler);