This class allows you to bind data to DOM elements, either one way or bi-directional. This enables you to keep your domain model in sync with what's shown in the browser.
Usage
Bind part of the DOM to a data object:
import Databind from './databind.js';
const dataProxy = new Databind("#app", {
user: {
name: "Alice",
visible: true,
role: "admin",
},
clickCounter: 0,
items: [1, 2, 3, "hello"],
});
Then you can use the following attributes in the DOM to bind to that data:
data-bind: Two-way binding. The value of the attribute is a path in the data object. The value of the DOM element will be kept in sync with the value in the data object, and changes to the DOM element will be written back to the data object.data-read: One-way binding. The value of the attribute is a path in the data object. Changes to the data object at the given path will get synced to the DOM element.data-write: One-way binding. The value of the attribute is a path in the data object. Changes to the DOM element will be written to the data object at that path.data-click: The value of the attribute is an assignment expression. When the element is clicked, the expression will be evaluated and the result will be stored in the data object. For example,data-click="user.role = reviewer"will update theroleproperty ofuserin the data object when the element is clicked.data-active-if: The value of the attribute is a path or an expression (see below). If the expression evaluates to a truthy value, the class specified in theclassoption (default: "active") will be added to the element. Otherwise, it will be removed.
For example:
<div id="app" data-active-if="user.visible">
<input type="text" data-bind="user.name">
<p data-read="user.role"></p>
<button data-click="user.role = admin">Admin</button>
<button data-click="user.role = contrib">Contributor</button>
<p data-read="clickCounter"></p>
<button id="counter">Click me</button>
</div>
Programmatic access
The constructor returns a proxy object which you can use to read from and write to the data object. Changes you make through this proxy will be reflected in the DOM in the annotated places.
dataProxy.data.user.visible = false; // This will hide the entire div with id "app"
console.log(dataProxy.data.user.name); // This will initially log "Alice"
// Listen for clicks on the button, and update the counter shown
document.getElementById("counter").addEventListener("click", () =>
dataProxy.data.clickCounter++);
You can also listen for changes to the data object by adding change event
listeners to the returned object:
dataProxy.addEventListener("change", (path, oldValue, newValue) => {
console.log(`Change at ${path} from ${oldValue} to ${newValue}`);
switch(path) {
case "user.name":
console.log(`User's name changed to ${newValue}`);
break;
}
});
Expressions
The values of the data-active-if and data-click attributes can be more
than just paths in the data object. They allow for simple expressions that
include comparisons and assignments. For example,
data-active-if="user.age >= 18" will add the active class to the element if
the user's age is greater than or equal to 18. data-active-if="user.name == Alice" will add the active class if the user's name is Alice. The supported
comparisons are ==, !=, >, <, >=, and <=. Any whitespace is only
for readability and will be ignored when parsing the expression.
The data-click attribute supports assignment expressions. For example,
data-click="user.name = John" will set the user's name to John when the
element is clicked.
Don't try to be too "clever" with these expressions. They are very limited for a reason; in most people's mental model HTML is not for logic. If you want to do something more complicated, just write write some complementary Javascript code that listens for changes to the data object and updates it accordingly, like in the examples above.
Loops
Loop are basically the "data-read" property for arrays. You can use the
data-loop attribute to repeat a section of the DOM for each element in an
array in the data object.
The value of the data-loop attribute should be an assignment expression
where the assignee is a variable name and the assigned value is a path to an
array in the data object. For example, data-loop="index = items" will
repeat the section of the DOM for each element in the items array in the
data object, and within that section you can use your variable (in this
example index) to refer to the index of the current element.
For example:
<ul data-loop="index = items">
<li data-read="items.index"></li>
</ul>
This will dynamically expand to:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>hello</li>
</ul>
When items are added to or removed from the array, the data-loop attribute
will keep the number of items in sync. When the value of items in the array
or their order changes, the data-read attributes will take care of that.
Items in the array can be objects, in which case you can use paths like
items.index.name to refer to their properties.
There is no data-write equivalent for arrays.
Options
Behaviour settings
The DataBind constructor takes an optional third argument with the following options:
class: The class to toggle fordata-active-ifexpressions (default: "active")stopEvents: Whether to callpreventDefaultandstopPropagationon events after handling them (default: true)immediate: Whether to listen forinputevents instead ofchangeevents for form elements (default: false). If true, changes to form elements will be written to the data object immediately as the user types, instead of only when they blur the element.customConversions: An array of custom conversion objects. Each object should have aselectorproperty, and optionallytoDomandfromDomfunctions. If an element matches the selector, the corresponding conversion functions will be used to convert values between the data object and the DOM, instead of the default conversions.- The
toDomfunction takes three arguments: the path that changed in the data object, the new value at that path, and the DOM element to update. - The
fromDomfunction takes two arguments: the path to write to in the data object, and the DOM element to read from. It should return the value to write to the data object.
- The
For example:
const dataProxy = new Databind("#app", {
date: new Date(),
}, {
class: "enabled",
immediate: true,
customConversions: [
{
selector: "input[type=date]",
toDom: (path, value, element) => element.value = value.toISOString().substring(0, 10),
fromDom: (path, element) => new Date(element.value),
},
],
});
This allows you to build custom interface elements that still respond dynamically to changes in either the DOM or the data, using all the same binding attributes.
Attribute names
You can also use the options object to override the attribute names for the
bindings by passing different values. For example, if you want to use
data-model instead of data-bind and data-is-active instead of
data-active-if for some reason, you can do:
const dataProxy = new Databind("#app", {...}, {
bind: "data-model",
activeIf: "data-is-active"
});
This feature is mainly there to allow you to use this library in contexts where the default attribute names might conflict with other libraries or frameworks, and I would advise against changing them for personal preference. Keeping them predictable keeps your code in line with this documentation and helps people to move from project to project without having to adapt their mental model.
- Source: