@figliolia/data-binding
v1.0.3
Published
A data binding library driven by JavaScript signals
Downloads
6
Readme
Data Binding
When using native web-components or basic DOM scripting, one of the largest pain-points is often resetting your DOM attributes to reflect new data after some user-behavior has taken place.
Solutions such as UI frameworks have long-since solved this issue, but with web-components becoming more widely supported I decided to put together a small JavaScript library for adding reactivity to your components.
Reactivity is achieved through the use of signals - stateful values, that when changed, update the DOM in accordance to developer needs. By default DOM updates are batched using calls to requestAnimationFrame
with a max number of operations per frame to ensure great performance.
Basic Usage
To create a data-binding for a DOM node or attribute, import the DataBinding
and pass it an initial value and a callback to run whenever the value changes:
import { DataBinding } from "@figliolia/data-binding";
// Select a DOM node(s) with which to bind data
const paragraph = document.getElementById("myParagraph");
// Create your binding with an initial value and an update function
const signal = new DataBinding(
"some text",
(nextValue) => {
paragraph.textContent = nextValue;
}
);
// Update your DOM node's content
signal.update("Some new text!");
// Clean up your binding when you no longer need it
signal.destroy()
From here you can update your signal when your user clicks a new tab, an HTTP request for data resolves, or any behavior your application requires.
In addition to handing your DOM updates, each binding returns a Signal
that you can treat as a stateful
variable. Each Signal
has the following API:
const signal = new Signal(0);
// Get a signal's value
const currentValue = signal.value;
// update a signal's value
signal.update(signal.value + 1);
// subscribe to signal updates
const subscription = signal.subscribe(nextValue => {});
// unsubscribe to signal updates
subscription()
Basic Examples
A Counter Button
import { DataBinding } from "@figliolia/data-binding";
class CounterButton extends HTMLElement {
node = document.createElement("button");
binding = new DataBinding(0, (nextValue) => {
this.node.textContent = `${nextValue}`;
});
constructor() {
super();
this.increment = this.increment.bind(this);
}
connectedCallback() {
this.appendChild(this.node);
this.node.addEventListener("click", this.increment);
}
disconnectedCallback() {
this.node.removeEventListener("click", this.increment);
this.binding.destroy()
}
increment() {
this.binding.update(this.binding.value + 1);
}
}
window.customElements.define("counter-button", CounterButton);
/*
Usage:
<counter-button></counter-button>
*/
A Dynamic Tabs Component
import { DataBinding } from "@figliolia/data-binding";
class DynamicTabs extends HTMLElement {
buttons: HTMLButtonElement[] = [];
contentCache = new Map<string, string>();
contentNode = document.createElement("p");
contentSignal = new DataBinding("Loading...", content => {
this.contentNode.textContent = content;
});
constructor() {
super();
this.onTabClick = this.onTabClick.bind(this);
}
connectedCallback() {
const tabs = this.parseTabs();
const { length } = tabs;
for(let i = 0; i < length; i++) {
const tab = tabs[i];
const buttons = document.createElement("button");
button.textContent = tab;
if(i === 0) {
button.classList.add("active");
void this.fetchContent(tab);
}
button.addEventListener("click", this.onTabClick);
this.buttons.push(button);
this.appendChild(button);
}
this.appendChild(this.contentNode);
}
disconnectedCallback() {
for(const button of this.buttons) {
button.removeEventListener("click", this.onTabClick);
}
this.contentCache.clear();
this.contentSignal.destroy();
}
onTabClick(e) {
for(const tab of this.buttons) {
if(tab !== e.target) {
tab.classList.remove("active");
} else {
e.target.classList.add("active");
void this.fetchContent(e.target.textContent);
}
}
}
fetchContent(tab) {
if(this.contentCache.has(tab)) {
return this.contentSignal.update(this.contentCache.get(tab));
}
this.contentSignal.update("Loading...");
const param = tab.toLowerCase().replaceAll(" ", "-");
fetch(`/api/tab-content?tab=${param}`).then(async (response) => {
const data = await response.json();
this.contentCache.set(tab, data.content);
this.contentSignal.update(data.content);
});
}
parseTabs() {
const tabs: string[] = [];
let increment = 1;
let tab = this.getAttribute(`tab${increment}`)
while(tab) {
tabs.push(tab);
increment++;
tab = this.getAttribute(`tab${increment}`)
}
return tabs;
}
}
window.customElements.define("dynamic-tabs", DynamicTabs);
/*
Usage:
<dynamic-tabs
tab1="Tab 1"
tab2="Tab 2"
tab3="Tab 3">
</dynamic-tabs>
*/