Skip to content
/ defi Public

A bunch of utilities that enable accessor-based reactivity for JavaScript objects

License

Notifications You must be signed in to change notification settings

finom/defi

Repository files navigation

Data binding without framework: a bunch of utilities that enable accessor-based reactivity for JavaScript objects.

npm version Coverage Status

If you're looking for defi-react get its README here.

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?

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

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())

Note that there is also a routing library for defi.js - defi-router.

Quick API ref

Full reference with all variations, flags and tutorials live at defi.js.org.

  • bindNode - Binds a property of an object to HTML node, implementing two-way data binding.
// basic use (for standard HTML5 elements)
defi.bindNode(obj, 'myKey', '.my-element');

// custom use (for any custom element)
defi.bindNode(obj, 'myKey', '.my-element', {
    // when is element state changed?
    // (that's a DOM event; a function can be used to listen to any non-DOM events)
    on: 'click',
    // how to extract element state?
    getValue: ({ node }) => someLibraryGetValue(node),
    // how to set element state?
    setValue: (v, { node }) => someLibrarySetValue(node, v),
    // how to initialize the widget?
    // it can be initialized in any way,
    // but 'initialize' function provides some syntactic sugar
    initialize: ({ node }) => someLibraryInit(node),
});

obj.myKey = 'some value'; // updates the element
  • calc - Creates a dependency of one property value on values of other properties (including other objects).
defi.calc(obj, 'a', ['b', 'c'], (b, c) => b + c);
obj.b = 1;
obj.c = 2;
console.log(obj.a); // 3
  • mediate - Transforms property value on its changing.
defi.mediate(obj, 'x', value => String(value));

obj.x = 1;

console.log(obj.x); // "1"
console.log(typeof obj.x); // "string"
  • on - Adds an event handler. Detailed information about all possible events you can get at the "Events" section of the website.
defi.on(obj, 'change:x', () => {
	alert(`obj.x now equals ${obj.x}`);
});

obj.x = 1;
  • off - Deletes an event handler.
defi.off(obj, ['change:x', 'bind']);
defi.on(obj, ['foo', 'bar'], (a, b, c) => {
	alert(a + b + c);
});
defi.trigger(obj, 'foo', 1, 2, 3); // alerts 6
defi.bindNode(obj, 'myKey', '.my-element');

defi.unbindNode(obj, 'myKey', '.my-element');
  • bound - Returns a bound element.
defi.bindNode(obj, 'myKey', '.my-element');
const node = defi.bound(obj, 'myKey'); // will return document.querySelector('.my-element')
  • chain - Allows chained calls of defi.js functions.
defi.chain(obj)
    .calc('a', 'b', b => b * 2)
    .set('b', 3)
    .bindNode('c', '.node');
  • defaultBinders - An array of functions which return a corresponding binder or a falsy value. This makes bindNode to detect how to bind a node if the binder argument isn't given.
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');
const element = document.createElement('input');
element.type = 'text';

console.log(defi.lookForBinder(element));
  • remove - Deletes a property and removes attached change handlers.
defi.remove(obj, 'myKey');
  • set - Sets a property value allowing to pass an event options object.
defi.set(obj, 'myKey', 3, { silent: true });