/**
* @author Greenwald
* @license PDL
* @module Binder
*/
import Route from './Route.js';
export default class Binder {
/**
* @property ATTRIBUTE
* @memberof module:Binder.Binder
* @description The attribute for defining the data scope for descendant elements.
* @type {string}
*/
static get ATTRIBUTE() { return 'bind'; }
/**
* @property PREFIX
* @memberof module:Binder.Binder
* @description The prefix for identifying binding attributes.
* @type {string}
*/
static get PREFIX() { return 'bind-'; }
#active = 0;
/**
* @property active
* @memberof module:Binder.Binder
* @description Indicates whether the binder is currently processing a view.
* @type {boolean}
*/
get active() { return this.#active > 0; }
#extensions = [];
/**
* @class Binder
* @memberof module:Binder
* @description The core of webtini functionality. Renders a view using a data model.
* @param {...Binder.Extension} extensions Loads the Binder instance with Binder.Extensions.
* @example
* ```html
* <html>
* <body>
* <h1 bind-textcontent="title"></h1>
* <p bind-textcontent="content"></p>
* </body>
* </html>
* ```
*
* ```javascript
* var datamodel = {
* title: 'The Page Title',
* content: 'The page content',
* };
* var binder = new Binder();
* binder.bind(document.body, data)
* ```
* @example
* ```javascript
* import {TemplateBinder} from './TemplateBinder.js';
* import {EventBinder} from './EventBinder.js';
* import {StyleBinder} from './StyleBinder.js';
* import {ClassBinder} from './ClassBinder.js';
* var binder = new Binder(
* new TemplateBinder(),
* new EventBinder(),
* new StyleBinder(),
* new ClassBinder()
* );
* ```
*/
constructor(...extensions) { this.#extensions = extensions ?? []; }
/**
* @function bind
* @memberof module:Binder.Binder
* @description Renders the view using the data.
* @param {Element} view The root element to bind.
* @param {*} data The Route or data model to bind from.
* @returns {Element}
*/
bind(view, data) {
this.#active++;
data = data instanceof Route ? data : new Route(data);
const handled = this.#extensions.find(e => e.handleElement(this, view, data));
if (!handled) {
if (view instanceof Element) {
for (var attr of [...view.attributes]) {
var name = attr.name.toLowerCase();
if (!attr.name.startsWith(Binder.PREFIX)) { continue; }
name = name.substring(Binder.PREFIX.length);
if (this.#extensions.find(e => e.handleAttribute(this, view, data, name, attr.value))) { continue; }
var selection = data.select(attr.value?.split('.')).value;
var assignment = Route.select(view, name.split('-'));
assignment.assign(selection);
}
}
if (view instanceof Node) {
const scope = view instanceof Element && view.hasAttribute(Binder.ATTRIBUTE) ? data.select(view.getAttribute(Binder.ATTRIBUTE)?.split('.')) : data;
for (var child of [...view.childNodes]) { this.bind(child, scope); }
}
}
this.#active--;
return view;
}
#defer = setTimeout(() => {});
/**
* @function defer
* @memberof module:Binder.Binder
* @description Queues binding to occur after the current call stack is clear. Only the latest call to this function will execute.
* @param {Element} view The root element to bind.
* @param {*} data The Route or data model to bind from.
*/
defer(view, data) {
clearTimeout(this.#defer);
this.#defer = setTimeout(() => this.bind(view, data));
}
static Extension =
/**
* @class Extension
* @memberof module:Binder.Binder
* @description An interface for extending the functionality of the {@link Binder} class.
* @example
* ```javascript
* import {Binder} from './Binder.js';
* export default class MyExtension extends Binder.Extension {
* handleElement(binder, element, route) {
* if (!(element instanceof HTMLDivElement)) { return false; } // Only handle div elements
* element.style.borderColor = 'red'; // Outline them in red
* binder.bind(element, route); // Apply normal binding (optional)
* return true; // Indicate that the element was handled and prevent further processing
* }
* }
*/
class Extension {
/**
* @function handleElement
* @memberof module:Binder.Binder.Extension
* @description A method for handling descendant elements.
* @param {Binder} binder The Binder instance that is currently processing the view.
* @param {Element} element The view element being processed.
* @param {Route} route The Route or data model being bound to the element.
* @returns {boolean} If true, prevents further processing of the element by the Binder.
*/
handleElement(binder, element, route) { return false; }
/**
* @function handleAttribute
* @memberof module:Binder.Binder.Extension
* @description A method for handling an attribute on an element
* @param {Binder} binder The Binder instance that is currently processing the view.
* @param {Element} element The view element being processed.
* @param {Route} route The Route or data model being bound to the element.
* @param {string} name The name of the attribute being processed, minus the Binding.PREFIX.
* @param {string} value The value of the attribute being processed.
* @returns {boolean} If true, prevents further processing of the attribute by the Binder.
*/
handleAttribute(binder, element, route, name, value) { return false; }
}
}