js/template/Template.js
import { HandleUnhandledPromise } from '~/helpers/ErrorManager';
/**
* Manages a section of HTML used with a view. See {@link TemplateType} to see
* how you can import one
*/
export default class Template {
/**
* Creates a template from some form of HTML fragment. This can be from the
* DOM or a JS-based HTMLElement
*
* @param {HTMLElement} root - Root view of the template
* @param {TemplateType} [type=none] - Type of the template to reference.
*/
constructor(root, type = TemplateType.none) {
if (root instanceof Template) {
this._root = root._root;
this._type = root._type;
} else if (typeof root === 'string') {
this._root = document.getElementById(root);
this._type = type;
} else {
this._root = root;
this._type = type;
}
this._root.template = this;
this._parent = this._root.parentNode;
this._hasLoaded = false;
}
/**
* Returns the underlying element
* @type {HTMLElement}
*/
get underlyingNode() {
return this._root;
}
/**
* An empty template
*/
static get empty() {
return new Template(
<div></div>,
TemplateType.clone
);
}
/**
* Performs a `move` {@link TemplateType} for a given HTML id to return a
* template based on the id's root.
* @param {string} id HTML ID of a {@link HTMLElement}
* @param {TemplateType} type Type of the template
* @return {Template} New template.
*/
static fromId(id, type = TemplateType.none) {
return new Template(
document.getElementById(id),
type
);
}
/**
* Creates template `<div>` with text.
* @param {string} text - text of new elem
* @param {TemplateType} [type=none] - Type of the generated template.
* @return {Template} new template.
*/
static fromText(text, type) {
let elem = document.createElement('span');
elem.appendChild(document.createTextNode(text));
return new Template(elem, type);
}
/**
* From innerHTML will wrap in div
* @param {HTMLElement} wrapper
* @param {string} innerHTML the innerHTML
* @param {TemplateType} [type=none]
* @return {Template}
*/
static fromInnerHTML(wrapper, innerHTML, type) {
wrapper.innerHTML = innerHTML;
return new Template(wrapper, type);
}
/**
* Returns a unique instance of the template as an HTMLElement.
* @return {HTMLElement} unique instance of the DOM element.
*/
unique() {
switch (this._type) {
case TemplateType.move:
this._root.parentNode.removeChild(this._root);
this._root.classList.remove('template');
this._type = TemplateType.none;
return this._root;
case TemplateType.clone:
return this._root.cloneNode(true);
default:
return this._root;
}
}
/**
* Removes from the context. Errors if not loaded
*/
removeFromContext() {
this.willUnload();
this.underlyingNode.parentNode.removeChild(this.underlyingNode);
this.didUnload();
}
/**
* Gets the original parent or else a default
* @param {?HTMLElement} defaultElement
* @return {HTMLElement} parent element or the `defualt` provided.
*/
getParent(defaultElement) {
return this._parent || defaultElement;
}
/**
* Called when loaded the first time
* @abstract
*/
async didInitialLoad() { void 0; }
/**
* Called when the view has loaded
* @abstract
*/
didLoad() {
if (this._hasLoaded === false) {
this.didInitialLoad()
.catch(HandleUnhandledPromise);
}
this._hasLoaded = true;
}
/**
* Called before loaded the first time
*/
async willInitialLoad() { void 0; }
/**
* Called right before the view will appear on screen
*/
willLoad() {
if (this._hasLoaded === false) {
this.willInitialLoad()
.catch(HandleUnhandledPromise);
}
}
/**
* Called before disappearing
*/
willUnload() { void 0; }
/**
* Called when disappeared
*/
didUnload() { void 0; }
/**
* Loads the template in a context
* @param {HTMLElement} parent - Will be appended to this node.
* @param {boolean} allowDupliacte If should allow to be loaded multiple times
* @return {HTMLElement} rendered element
*/
loadInContext(parent, allowDuplicate = true) {
if (!allowDuplicate && this._hasLoaded) return;
let elem = this.unique();
this.willLoad();
parent.appendChild(elem);
this.didLoad();
return elem;
}
/**
* Prepends the template in a context
* @param {HTMLElement} parent - Will be appended to this node.
* @param {boolean} allowDupliacte If should allow to be loaded multiple times
* @return {HTMLElement} rendered element
*/
prependInContext(parent, allowDuplicate = true) {
if (!allowDuplicate && this._hasLoaded) return;
let elem = this.unique();
this.willLoad();
parent.insertBefore(elem, parent.firstChild);
this.didLoad();
return elem;
}
/**
* Creates a field w/ updating text
* @param {string} name The field name
* @param {string} defaultValue The default value
* @return {Text}
*/
defineLinkedText(name, defaultValue) {
let node = document.createTextNode(defaultValue);
Object.defineProperty(this, name, {
configurable: true,
enumerable: true,
get: () => node.data,
set: (newValue) => { node.data = newValue }
});
return node;
}
/**
* Defines a linked input
* @param {string} name the name
* @param {HTMLElement} [input=underlyingNode]
*/
defineLinkedInput(name, input = this.underlyingNode) {
Object.defineProperty(this, name, {
configurable: true,
enumerable: true,
get: () => input.value,
set: (newValue) => { input.value = newValue }
});
return input;
}
/**
* Defines a linked class
* @param {string} name - field name
* @param {string} className
* @param {HTMLElement} [node=underlyingNode]
*/
defineLinkedClass(name, className, node = this.underlyingNode) {
const isInv = className.indexOf('!') === 0;
const realClassName = isInv ? className.substring(1) : className;
Object.defineProperty(this, name, {
configurable: true,
enumerable: true,
get: () => node.classList.contains(realClassName),
set: (newValue) => {
if (newValue ^ isInv) node.classList.add(realClassName);
else node.classList.remove(realClassName);
}
});
}
/**
* Loads and replaces
* @param {HTMLElement} source - what to replace
* @return {HTMLElement}
*/
loadReplacingContext(source) {
const elem = this.unique();
this.willLoad();
source.parentNode.replaceChild(elem, source);
this.didLoad();
return elem;
}
/**
* Loads before an element
* @param {HTMLElement} elem - Element to load before
* @return {HTMLElement} rendered element
*/
loadBeforeContext(elem) {
let instance = this.unique();
this.willLoad();
elem.parentNode.insertBefore(instance, elem);
this.didLoad();
return instance;
}
}
/**
* @typedef {Object} TemplateType type of template.
*/
export const TemplateType = {
move: 0,
clone: 1,
none: 2
};