<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 = 'The magic of accessors';
</script>
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 use 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
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 project. 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 optionoptional: 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
Name | Type | Details |
---|---|---|
obj | object | A target object |
key | string | A property name |
node | string node $nodes | An HTML element which must be bound to a |
binder optional | binder | A binder containing the following properties: |
options optional | object | Options object which accepts |
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
andbinder
; - An array of objects with properties
node
andbinder
;
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
Name | Type | Details |
---|---|---|
obj | object | A target object |
bindings | object | (see the example) |
binder optional | binder | (see above) |
options optional | object | (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
Arguments
Name | Type | Details |
---|---|---|
obj | object | A target object |
key | string | A name of a property, which bound elements you want to get |
options | object | You can pass |
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 onb
,b
depends onc
andc
depends ona
) 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 thepromiseCalc
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
Name | Default | Type | Details |
---|---|---|---|
obj |
| object | A target object |
targetKey |
| string | A property which needs to be calculated |
source |
| string array | Which properties the target property is depended on |
handler optional | (v)=>v
| function | A function which returns a new value |
options optional |
| object | An object which can contain some special flags or data for |
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
Name | Type | Details |
---|---|---|
obj | object | A target object |
batch | array | An object which includes all information about calculated properties |
options optional | object | 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
Name | Type | Details |
---|---|---|
obj | object | 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'
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
Name | Type |
---|---|
node | node |
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
Name | Type | Details |
---|---|---|
obj | object | A target object |
key | string array | A key or an array of keys |
mediator | function | 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(keyMediatorPairs)
Alternative syntax of the defi.mediate function which accepts "key-mediator" object as an argument
Arguments
Name | Type | Details |
---|---|---|
keyMediatorPairs | object | 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
Name | Type | Details |
---|---|---|
obj | object | A target object |
names optional | eventNames | A list of event names which are separated by spaces (for example, |
callback optional | eventHandler | A function-handler |
Links
Examples
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
Name | Type | Details |
---|---|---|
obj | object | A target object |
names | eventNames | An event name or some names which are separated by a space (for example, |
callback | eventHandler | A function which is caused by the event |
options | object | Options object where |
Links
Examples
defi.on(obj, 'foo', () => {
alert('A custom Event is fired');
});
defi.trigger(obj, 'foo');
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-element pairs can be passed to avoid multiple invocation of the function and reduce your code.
Arguments
Name | Type | Details |
---|---|---|
obj | object | A target object |
evtnameHandlerObject | object | An object where keys are event names and values are event handlers |
options | object | See above. |
obj | object | A target object |
Examples
defi.on(obj, {
'custom': evt => ...,
'click::x': evt => ...,
'change:y': 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
Name | Type | Details |
---|---|---|
obj | object | A target object |
key | string | A property name or an array of names to remove |
eventOptions optional | eventOptions | 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 thechange
andchange:KEY
eventssilentHTML
- do not change states of bound HTML nodesforce
- call thechange
andchange:KEY
events even though the property value has not been changedforceHTML
- 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, someoption
tags have been added toselect
tag)skipMediator
- prevents the property transformation by defi.mediateskipCalc
- prevents the work of dependencies created with defi.calc
Fires change change:KEY beforechange beforechange:KEY
Arguments
Name | Type | Details |
---|---|---|
obj | object | A target object |
key | string | A key |
value | * | A value |
eventOptions optional | eventOptions | 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
Name | Type | Details |
---|---|---|
obj | object | A target object |
keyValuePairs | object | An object containing key-value pairs |
eventOptions optional | eventOptions | 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
Name | Type | Details |
---|---|---|
obj | object | A target object |
names optional | eventNames | An event name or some names which are separated by a space |
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, undefined, node, eventOptions)
→
object
CommonJS module: 'defi/unbindnode'
Destroys a binding between given property and HTML node
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
Name | Type | Details |
---|---|---|
obj | object | A target object |
string null | A key or an array of keys. If you pass |
|
node optional | string node $nodes | HTML node |
eventOptions optional | eventOptions | Event object ( |
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
Name | Type | Details |
---|---|---|
obj | object | A target object |
bindings | object | (see the example) |
eventOptions optional | eventOptions | (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
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
Name | Type | Details |
---|---|---|
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
Event name or space-delimited list of event names.
Custom events.
defi.on(obj, 'myevent', () => {...});
defi.trigger(obj, 'myevent');
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
Name | Type | Details |
---|---|---|
on optional | string 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 optional | function | A function which tells how to retrieve a value (state) from HTML node when DOM event is fired |
setValue optional | function | A function which tells how to change DOM node when the property value is changed |
initialize optional | function | A function which is called before binding is launched. For example it can initialize jQuery plugin or something else |
destroy optional | function | A function which is called when a binding is removed using |
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. For example, jQuery instance or NodeList.
Examples
let $nodes = $('.foo');
$nodes = document.querySelectorAll('.bar');
string
A string
Examples
const foo = '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 separated by spaces 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);