@endorphinjs/template-runtime
Advanced tools
Comparing version 0.1.11 to 0.1.12
@@ -147,18 +147,153 @@ 'use strict'; | ||
/** | ||
* Invokes `fn` for each `name` hook in given definition | ||
* @param {ComponentDefinition} definition | ||
* @param {string} name | ||
* @param {function} fn | ||
* Marks given item as explicitly disposable for given host | ||
* @param {Component | Injector} host | ||
* @param {DisposeCallback} callback | ||
* @return {Component | Injector} | ||
*/ | ||
function forEachHook(definition, name, fn) { | ||
const { plugins } = definition; | ||
function addDisposeCallback(host, callback) { | ||
if (/** @type {Component} */ (host).componentModel) { | ||
/** @type {Component} */ (host).componentModel.dispose = callback; | ||
} else { | ||
/** @type {Injector} */ (host).ctx.dispose = callback; | ||
} | ||
if (plugins) { | ||
for (let i = 0; i < plugins.length; i++) { | ||
forEachHook(plugins[i], name, fn); | ||
return host; | ||
} | ||
const blockKey = '&block'; | ||
/** | ||
* Creates injector instance for given target, if required | ||
* @param {Element} target | ||
* @returns {Injector} | ||
*/ | ||
function createInjector(target) { | ||
return { | ||
parentNode: target, | ||
items: [], | ||
ctx: null, | ||
ptr: 0, | ||
// NB create `slots` placeholder to promote object to hidden class. | ||
// Do not use any additional function argument for adding value to `slots` | ||
// to reduce runtime checks and keep functions in monomorphic state | ||
slots: null, | ||
attributes: changeSet(), | ||
events: changeSet() | ||
}; | ||
} | ||
/** | ||
* Creates block for given injector | ||
* @param {Injector} injector | ||
* @returns {Block} | ||
*/ | ||
function block(injector) { | ||
return add(injector, { | ||
[blockKey]: true, | ||
inserted: 0, | ||
deleted: 0, | ||
size: 0, | ||
dispose: null | ||
}); | ||
} | ||
/** | ||
* Runs `fn` template function in context of given `block` | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {Function} fn | ||
* @param {Component} component | ||
* @param {*} data | ||
* @returns {*} Result of `fn` function call | ||
*/ | ||
function run(injector, block, fn, component, data) { | ||
let result; | ||
const ix = injector.items.indexOf(block); | ||
if (typeof fn === 'function') { | ||
const ctx = injector.ctx; | ||
injector.ptr = ix + 1; | ||
injector.ctx = block; | ||
result = fn(component, injector, data); | ||
injector.ctx = ctx; | ||
ctx ? consume(ctx, block) : reset(block); | ||
} | ||
injector.ptr = ix + block.size + 1; | ||
return result; | ||
} | ||
/** | ||
* Inserts given node into current context | ||
* @param {Injector} injector | ||
* @param {Node} node | ||
* @returns {Node} | ||
*/ | ||
function insert(injector, node, slotName = '') { | ||
let target; | ||
const { slots } = injector; | ||
if (slots) { | ||
target = slots[slotName] || (slots[slotName] = document.createDocumentFragment()); | ||
} else { | ||
target = injector.parentNode; | ||
} | ||
domInsert(node, target, getAnchorNode(injector.items, injector.ptr, target)); | ||
return add(injector, node); | ||
} | ||
/** | ||
* Moves contents of given block at `pos` location, effectively updating | ||
* inserted nodes in parent context | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {number} pos | ||
*/ | ||
function move(injector, block, pos) { | ||
const { items } = injector; | ||
if (items[pos] === block) { | ||
return; | ||
} | ||
// Move block contents at given position | ||
const curPos = items.indexOf(block); | ||
const blockItems = items.splice(curPos, block.size + 1); | ||
if (curPos < pos) { | ||
pos -= blockItems.length; | ||
} | ||
for (let i = blockItems.length - 1, item; i >= 0; i--) { | ||
item = /** @type {Element} */ (blockItems[i]); | ||
if (!isBlock(item)) { | ||
domInsert(item, item.parentNode, getAnchorNode(items, pos, item.parentNode)); | ||
} | ||
items.splice(pos, 0, item); | ||
} | ||
} | ||
if (name in definition) { | ||
return fn(definition[name], definition); | ||
/** | ||
* Disposes contents of given block | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {Object} scope | ||
* @param {boolean} self Remove block item as well | ||
*/ | ||
function dispose(injector, block, scope, self) { | ||
disposeBlock(block, scope, self); | ||
const { items, ctx } = injector; | ||
const ix = items.indexOf(block) + (self ? 0 : 1); | ||
const size = block.deleted; | ||
if (size) { | ||
ctx && consume(ctx, block); | ||
const removed = items.splice(ix, size); | ||
for (let i = 0; i < removed.length; i++) { | ||
domRemove(removed[i]); | ||
} | ||
} | ||
@@ -168,166 +303,464 @@ } | ||
/** | ||
* Invokes `name` hook for given component definition | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @param {Array} [args] | ||
* Disposes given block | ||
* @param {Block} block | ||
* @param {Object} scope | ||
* @param {boolean} self Dispose block itself | ||
* @returns {void} Should return nothing since function result will be used | ||
* as shorthand to reset cached value | ||
*/ | ||
function runHook(elem, name, args) { | ||
const hookArgs = args ? [elem].concat(args) : [elem]; | ||
function disposeBlock(block, scope, self) { | ||
if (block.dispose) { | ||
block.dispose(scope); | ||
block.dispose = null; | ||
} | ||
block.deleted += block.size + (self ? 1 : 0); | ||
block.size = 0; | ||
} | ||
return forEachHook(elem.componentModel.definition, name, hook => hook.apply(null, hookArgs)); | ||
/** | ||
* Adds given item into current injector position | ||
* @param {Injector} injector | ||
* @param {InjectorItem} item | ||
*/ | ||
function add(injector, item) { | ||
injector.items.splice(injector.ptr++, 0, item); | ||
injector.ctx && markInsert(injector.ctx); | ||
return item; | ||
} | ||
/** | ||
* Creates element with given tag name | ||
* @param {string} tagName | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
* Get DOM node nearest to given position of items list | ||
* @param {InjectorItem[]} items | ||
* @param {number} ix | ||
* @param {Node} parent Ensure element has given element as parent node | ||
* @returns {Node} | ||
*/ | ||
function elem(tagName, cssScope) { | ||
const el = document.createElement(tagName); | ||
cssScope && el.setAttribute(cssScope, ''); | ||
return el; | ||
function getAnchorNode(items, ix, parent) { | ||
while (ix < items.length) { | ||
const item = /** @type {Node} */ (items[ix++]); | ||
if (item.parentNode === parent) { | ||
return item; | ||
} | ||
} | ||
} | ||
/** | ||
* Creates element with given tag name under `ns` namespace | ||
* @param {string} tagName | ||
* @param {string} ns | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
* @param {Block} block | ||
*/ | ||
function elemNS(tagName, ns, cssScope) { | ||
const el = document.createElementNS(ns, tagName); | ||
cssScope && el.setAttribute(cssScope, ''); | ||
return el; | ||
function markInsert(block) { | ||
block.inserted++; | ||
block.size++; | ||
} | ||
/** | ||
* Creates element with given tag name and text | ||
* @param {string} tagName | ||
* @param {string} text | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
* Consumes data from given `child` block by parent `block` | ||
* @param {Block} block | ||
*/ | ||
function elemWithText(tagName, text, cssScope) { | ||
const el = elem(tagName, cssScope); | ||
el.textContent = textValue(text); | ||
return el; | ||
function consume(block, child) { | ||
block.inserted += child.inserted; | ||
block.deleted += child.deleted; | ||
block.size += child.inserted - child.deleted; | ||
reset(child); | ||
} | ||
/** | ||
* Creates element with given tag name under `ns` namespace and text | ||
* @param {string} tagName | ||
* @param {string} ns | ||
* @param {string} text | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
* Reset session data from given block | ||
* @param {Block} block | ||
*/ | ||
function elemNSWithText(tagName, ns, text, cssScope) { | ||
const el = elemNS(tagName, ns, cssScope); | ||
el.textContent = textValue(text); | ||
return el; | ||
function reset(block) { | ||
block.inserted = block.deleted = 0; | ||
} | ||
/** | ||
* Creates text node with given value | ||
* @param {String} value | ||
* @returns {Text} | ||
* Check if given value is a block | ||
* @param {*} obj | ||
* @returns {boolean} | ||
*/ | ||
function text(value) { | ||
const node = document.createTextNode(textValue(value)); | ||
node['$value'] = value; | ||
return node; | ||
function isBlock(obj$$1) { | ||
return blockKey in obj$$1; | ||
} | ||
/** | ||
* Updates given text node value, if required | ||
* @param {Text} node | ||
* @param {Node} node | ||
* @param {Node} parent | ||
* @param {Node} anchor | ||
* @returns {Node} Inserted item | ||
*/ | ||
function domInsert(node, parent, anchor) { | ||
return anchor | ||
? parent.insertBefore(node, anchor) | ||
: parent.appendChild(node); | ||
} | ||
/** | ||
* Removes given DOM node from its tree | ||
* @param {Node} node | ||
*/ | ||
function domRemove(node) { | ||
const parent = node.parentNode; | ||
parent && parent.removeChild(node); | ||
} | ||
/** | ||
* Enters new variable scope context | ||
* @param {Component} host | ||
* @param {object} incoming | ||
* @return {Object} | ||
*/ | ||
function enterScope(host, incoming) { | ||
return setScope(host, createScope(host, incoming)); | ||
} | ||
/** | ||
* Exit from current variable scope | ||
* @param {Component} host | ||
* @returns {Object} | ||
*/ | ||
function exitScope(host) { | ||
return setScope(host, Object.getPrototypeOf(host.componentModel.vars)); | ||
} | ||
/** | ||
* Creates new scope from given component state | ||
* @param {Component} host | ||
* @param {Object} [incoming] | ||
* @return {Object} | ||
*/ | ||
function createScope(host, incoming) { | ||
return assign(obj(host.componentModel.vars), incoming); | ||
} | ||
/** | ||
* Sets given object as current component scope | ||
* @param {Component} host | ||
* @param {Object} scope | ||
*/ | ||
function setScope(host, scope) { | ||
return host.componentModel.vars = scope; | ||
} | ||
/** | ||
* Returns current variable scope | ||
* @param {Component} elem | ||
* @returns {object} | ||
*/ | ||
function getScope(elem) { | ||
return elem.componentModel.vars; | ||
} | ||
/** | ||
* Returns property with given name from component | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @return {*} | ||
*/ | ||
function getProp(elem, name) { | ||
return elem.props[name]; | ||
} | ||
/** | ||
* Returns state value with given name from component | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @return {*} | ||
*/ | ||
function getState(elem, name) { | ||
return elem.state[name]; | ||
} | ||
/** | ||
* Returns value of given runtime variable from component | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @returns {*} | ||
*/ | ||
function getVar(elem, name) { | ||
return elem.componentModel.vars[name]; | ||
} | ||
/** | ||
* Sets value of given runtime variable for component | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @param {*} value | ||
* @returns {number} Returns `1` if text was updated, `0` otherwise | ||
*/ | ||
function updateText(node, value) { | ||
if (value !== node['$value']) { | ||
node.nodeValue = textValue(value); | ||
node['$value'] = value; | ||
return 1; | ||
function setVar(elem, name, value) { | ||
elem.componentModel.vars[name] = value; | ||
} | ||
/** | ||
* Initial block rendering | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {Function} get | ||
* @returns {BlockContext} | ||
*/ | ||
function mountBlock(host, injector, get) { | ||
/** @type {BlockContext} */ | ||
const ctx = { | ||
host, | ||
injector, | ||
block: block(injector), | ||
scope: getScope(host), | ||
get, | ||
fn: undefined, | ||
update: undefined | ||
}; | ||
updateBlock(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Updated block, described in `ctx` object | ||
* @param {BlockContext} ctx | ||
* @returns {number} Returns `1` if block was updated, `0` otherwise | ||
*/ | ||
function updateBlock(ctx) { | ||
let updated = 0; | ||
const { host, injector, scope, block: block$$1, update } = ctx; | ||
const fn = ctx.get(host, scope, injector); | ||
if (ctx.fn !== fn) { | ||
updated = 1; | ||
// Unmount previously rendered content | ||
ctx.fn && dispose(injector, block$$1, scope, false); | ||
// Mount new block content | ||
ctx.update = fn ? run(injector, block$$1, fn, host, scope) : null; | ||
ctx.fn = fn; | ||
} else if (update) { | ||
// Update rendered result | ||
updated = run(injector, block$$1, update, host, scope) ? 1 : 0; | ||
} | ||
return 0; | ||
return updated; | ||
} | ||
/** | ||
* Returns textual representation of given `value` object | ||
* @param {*} value | ||
* @returns {string} | ||
* @param {BlockContext} ctx | ||
*/ | ||
function textValue(value) { | ||
return value != null ? value : ''; | ||
function unmountBlock(ctx) { | ||
dispose(ctx.injector, ctx.block, ctx.scope, true); | ||
} | ||
/** | ||
* Adds pending event `name` handler | ||
* Mounts iterator block | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {string} name | ||
* @param {function} handler | ||
* @param {Function} get A function that returns collection to iterate | ||
* @param {Function} body A function that renders item of iterated collection | ||
* @returns {IteratorContext} | ||
*/ | ||
function addEvent(injector, name, handler) { | ||
injector.events.cur[name] = handler; | ||
function mountIterator(host, injector, get, body) { | ||
/** @type {IteratorContext} */ | ||
const ctx = { | ||
host, | ||
injector, | ||
get, | ||
body, | ||
block: block(injector), | ||
scope: getScope(host), | ||
index: 0, | ||
rendered: [], | ||
updated: 0 | ||
}; | ||
updateIterator(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Adds given `handler` as event `name` listener | ||
* @param {Element} elem | ||
* @param {string} name | ||
* @param {EventListener} handler | ||
* Updates iterator block defined in `ctx` | ||
* @param {IteratorContext} ctx | ||
* @returns {number} Returns `1` if iterator was updated, `0` otherwise | ||
*/ | ||
function addStaticEvent(elem, name, handler) { | ||
handler && elem.addEventListener(name, handler); | ||
function updateIterator(ctx) { | ||
run(ctx.injector, ctx.block, iteratorHost, ctx.host, ctx) ? 1 : 0; | ||
return ctx.updated; | ||
} | ||
/** | ||
* Finalizes events of given injector | ||
* | ||
* @param {IteratorContext} ctx | ||
*/ | ||
function unmountIterator(ctx) { | ||
const { rendered, injector } = ctx; | ||
let item; | ||
while (item = rendered.pop()) { | ||
dispose(injector, item[0], item[2], true); | ||
} | ||
} | ||
/** | ||
* | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @returns {number} Update status | ||
* @param {IteratorContext} ctx | ||
*/ | ||
function finalizeEvents(injector) { | ||
return finalizeItems(injector.events, changeEvent, injector.parentNode); | ||
function iteratorHost(host, injector, ctx) { | ||
ctx.index = 0; | ||
ctx.updated = 0; | ||
const collection = ctx.get(host, ctx.scope); | ||
if (collection && typeof collection.forEach === 'function') { | ||
collection.forEach(iterator, ctx); | ||
} | ||
// Remove remaining blocks | ||
let item; | ||
while (ctx.rendered.length > ctx.index) { | ||
ctx.updated = 1; | ||
item = ctx.rendered.pop(); | ||
dispose(injector, item[0], item[2], true); | ||
} | ||
} | ||
/** | ||
* Returns function that must be invoked as event handler for given component | ||
* @param {Component} component | ||
* @param {string} name Method name | ||
* @param {HTMLElement} ctx Context element where event listener was added | ||
* @returns {function?} | ||
* @this {IteratorContext} | ||
* @param {*} value | ||
* @param {*} key | ||
*/ | ||
function getEventHandler(component, name, ctx) { | ||
let fn; | ||
function iterator(value, key) { | ||
const { host, injector, rendered, index } = this; | ||
const localScope = { index, key, value }; | ||
if (typeof component[name] === 'function') { | ||
fn = component[name].bind(component); | ||
if (index < rendered.length) { | ||
// Update existing block | ||
const [b, update, scope] = rendered[index]; | ||
setScope(host, assign(scope, localScope)); | ||
if (run(injector, b, update, host, scope)) { | ||
this.updated = 1; | ||
} | ||
exitScope(host); | ||
} else { | ||
const handler = component.componentModel.definition[name]; | ||
if (typeof handler === 'function') { | ||
fn = handler.bind(ctx); | ||
// Create & render new block | ||
const b = block(injector); | ||
const scope = enterScope(host, localScope); | ||
const update = run(injector, b, this.body, host, scope); | ||
exitScope(host); | ||
rendered.push([b, update, scope]); | ||
this.updated = 1; | ||
} | ||
this.index++; | ||
} | ||
/** | ||
* Renders key iterator block | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {Function} get | ||
* @param {Function} keyExpr | ||
* @param {Function} body | ||
* @returns {KeyIteratorContext} | ||
*/ | ||
function mountKeyIterator(host, injector, get, keyExpr, body) { | ||
/** @type {KeyIteratorContext} */ | ||
const ctx = { | ||
host, | ||
injector, | ||
keyExpr, | ||
body, | ||
get, | ||
rendered: obj(), | ||
block: block(injector), | ||
scope: getScope(host), | ||
index: 0, | ||
updated: 1, | ||
used: null | ||
}; | ||
updateKeyIterator(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Updates iterator block defined in `ctx` | ||
* @param {KeyIteratorContext} ctx | ||
* @returns {number} Returns `1` if iterator was updated, `0` otherwise | ||
*/ | ||
function updateKeyIterator(ctx) { | ||
run(ctx.injector, ctx.block, keyIteratorHost, ctx.host, ctx); | ||
return ctx.updated; | ||
} | ||
/** | ||
* @param {KeyIteratorContext} ctx | ||
*/ | ||
function unmountKeyIterator(ctx) { | ||
const { rendered, injector } = ctx; | ||
let items, item; | ||
for (let k in rendered) { | ||
items = rendered[k]; | ||
while (item = items.pop()) { | ||
dispose(injector, item[0], item[2], true); | ||
} | ||
} | ||
} | ||
if (fn) { | ||
fn.displayName = name; | ||
/** | ||
* | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {KeyIteratorContext} ctx | ||
*/ | ||
function keyIteratorHost(host, injector, ctx) { | ||
ctx.used = obj(); | ||
ctx.index = 0; | ||
ctx.updated = 1; | ||
const collection = ctx.get(host, ctx.scope); | ||
if (collection && typeof collection.forEach === 'function') { | ||
collection.forEach(iterator$1, ctx); | ||
} | ||
return fn; | ||
// Remove remaining blocks | ||
for (let k in ctx.rendered) { | ||
for (let i = 0, items = ctx.rendered[k]; i < items.length; i++) { | ||
ctx.updated = 1; | ||
dispose(injector, items[i][0], items[i][2], true); | ||
} | ||
} | ||
ctx.rendered = ctx.used; | ||
} | ||
/** | ||
* Invoked when event handler was changed | ||
* @param {string} name | ||
* @param {EventListener} prevValue | ||
* @param {EventListener} newValue | ||
* @param {Element} elem | ||
* @this {KeyIteratorContext} | ||
* @param {*} value | ||
* @param {*} key | ||
*/ | ||
function changeEvent(name, prevValue, newValue, elem) { | ||
prevValue && elem.removeEventListener(name, prevValue); | ||
addStaticEvent(elem, name, newValue); | ||
function iterator$1(value, key) { | ||
const { host, injector, index, used, rendered, keyExpr, body } = this; | ||
const localScope = { index, key, value }; | ||
const id = keyExpr(value, createScope(host, localScope)); | ||
let entry = id in rendered && rendered[id].shift(); | ||
if (entry) { | ||
// Update existing block | ||
const [b, update, scope] = entry; | ||
setScope(host, assign(scope, localScope)); | ||
move(injector, b, injector.ptr); | ||
if (run(injector, b, update, host, scope)) { | ||
this.updated = 1; | ||
} | ||
exitScope(host); | ||
} else { | ||
// Create & render new block | ||
const b = block(injector); | ||
const scope = enterScope(host, localScope); | ||
const update = run(injector, b, body, host, scope); | ||
this.updated = 1; | ||
exitScope(host); | ||
entry = [b, update, scope]; | ||
} | ||
// Mark block as used. | ||
// We allow multiple items key in case of poorly prepared data. | ||
if (id in used) { | ||
used[id].push(entry); | ||
} else { | ||
used[id] = [entry]; | ||
} | ||
this.index++; | ||
} | ||
@@ -454,86 +887,98 @@ | ||
/** | ||
* Enters new variable scope context | ||
* @param {Component} host | ||
* @param {object} incoming | ||
* @return {Object} | ||
* Adds pending event `name` handler | ||
* @param {Injector} injector | ||
* @param {string} name | ||
* @param {function} handler | ||
*/ | ||
function enterScope(host, incoming) { | ||
return setScope(host, createScope(host, incoming)); | ||
function addEvent(injector, name, handler) { | ||
injector.events.cur[name] = handler; | ||
} | ||
/** | ||
* Exit from current variable scope | ||
* @param {Component} host | ||
* @returns {Object} | ||
* Adds given `handler` as event `name` listener | ||
* @param {Element} elem | ||
* @param {string} name | ||
* @param {EventListener} handler | ||
*/ | ||
function exitScope(host) { | ||
return setScope(host, Object.getPrototypeOf(host.componentModel.vars)); | ||
function addStaticEvent(elem, name, handler) { | ||
handler && elem.addEventListener(name, handler); | ||
} | ||
/** | ||
* Creates new scope from given component state | ||
* @param {Component} host | ||
* @param {Object} [incoming] | ||
* @return {Object} | ||
* Finalizes events of given injector | ||
* @param {Injector} injector | ||
* @returns {number} Update status | ||
*/ | ||
function createScope(host, incoming) { | ||
return assign(obj(host.componentModel.vars), incoming); | ||
function finalizeEvents(injector) { | ||
return finalizeItems(injector.events, changeEvent, injector.parentNode); | ||
} | ||
/** | ||
* Sets given object as current component scope | ||
* @param {Component} host | ||
* @param {Object} scope | ||
* Returns function that must be invoked as event handler for given component | ||
* @param {Component} component | ||
* @param {string} name Method name | ||
* @param {HTMLElement} ctx Context element where event listener was added | ||
* @returns {function?} | ||
*/ | ||
function setScope(host, scope) { | ||
return host.componentModel.vars = scope; | ||
} | ||
function getEventHandler(component, name, ctx) { | ||
let fn; | ||
/** | ||
* Returns current variable scope | ||
* @param {Component} elem | ||
* @returns {object} | ||
*/ | ||
function getScope(elem) { | ||
return elem.componentModel.vars; | ||
if (typeof component[name] === 'function') { | ||
fn = component[name].bind(component); | ||
} else { | ||
const handler = component.componentModel.definition[name]; | ||
if (typeof handler === 'function') { | ||
fn = handler.bind(ctx); | ||
} | ||
} | ||
if (fn) { | ||
fn.displayName = name; | ||
} | ||
return fn; | ||
} | ||
/** | ||
* Returns property with given name from component | ||
* @param {Component} elem | ||
* Invoked when event handler was changed | ||
* @param {string} name | ||
* @return {*} | ||
* @param {EventListener} prevValue | ||
* @param {EventListener} newValue | ||
* @param {Element} elem | ||
*/ | ||
function getProp(elem, name) { | ||
return elem.props[name]; | ||
function changeEvent(name, prevValue, newValue, elem) { | ||
prevValue && elem.removeEventListener(name, prevValue); | ||
addStaticEvent(elem, name, newValue); | ||
} | ||
/** | ||
* Returns state value with given name from component | ||
* @param {Component} elem | ||
* Invokes `fn` for each `name` hook in given definition | ||
* @param {ComponentDefinition} definition | ||
* @param {string} name | ||
* @return {*} | ||
* @param {function} fn | ||
*/ | ||
function getState(elem, name) { | ||
return elem.state[name]; | ||
function forEachHook(definition, name, fn) { | ||
const { plugins } = definition; | ||
if (plugins) { | ||
for (let i = 0; i < plugins.length; i++) { | ||
forEachHook(plugins[i], name, fn); | ||
} | ||
} | ||
if (name in definition) { | ||
return fn(definition[name], definition); | ||
} | ||
} | ||
/** | ||
* Returns value of given runtime variable from component | ||
* Invokes `name` hook for given component definition | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @returns {*} | ||
* @param {Array} [args] | ||
*/ | ||
function getVar(elem, name) { | ||
return elem.componentModel.vars[name]; | ||
} | ||
function runHook(elem, name, args) { | ||
const hookArgs = args ? [elem].concat(args) : [elem]; | ||
/** | ||
* Sets value of given runtime variable for component | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @param {*} value | ||
*/ | ||
function setVar(elem, name, value) { | ||
elem.componentModel.vars[name] = value; | ||
return forEachHook(elem.componentModel.definition, name, hook => hook.apply(null, hookArgs)); | ||
} | ||
@@ -547,15 +992,41 @@ | ||
* @param {Function} [defaultContent] Function for rendering default slot content | ||
* @return {SlotContext} | ||
*/ | ||
function mountSlot(host, name, elem, defaultContent) { | ||
const blockEntry = (host, scope, injector) => { | ||
if (!renderSlot(host, injector)) { | ||
return defaultContent; | ||
} | ||
}; | ||
/** @type {SlotContext} */ | ||
const ctx = { host, name, defaultContent, isDefault: false }; | ||
const { slots } = host.componentModel; | ||
const { slots } = host.componentModel; | ||
/** | ||
* @param {Component} host | ||
* @param {Object} scope | ||
* @param {Injector} injector | ||
*/ | ||
function blockEntry(host, scope, injector) { | ||
ctx.isDefault = !renderSlot(host, injector); | ||
return ctx.isDefault ? ctx.defaultContent : null; | ||
} | ||
slots[name] = mountBlock(host, createInjector(elem), blockEntry); | ||
return ctx; | ||
} | ||
/** | ||
* Unmounts given slot | ||
* @param {SlotContext} ctx | ||
*/ | ||
function unmountSlot(ctx) { | ||
const { host, name } = ctx; | ||
const { slots } = host.componentModel; | ||
if (ctx.isDefault) { | ||
unmountBlock(slots[name]); | ||
} | ||
ctx.defaultContent = null; | ||
delete slots[name]; | ||
} | ||
/** | ||
* Sync slot content if necessary | ||
@@ -628,2 +1099,145 @@ * @param {Component} host | ||
/** | ||
* Sets runtime ref (e.g. ref which will be changed over time) to given host | ||
* @param {Component} host | ||
* @param {string} name | ||
* @param {HTMLElement} elem | ||
* @returns {number} Update status. Refs must be explicitly finalized, thus | ||
* we always return `0` as nothing was changed | ||
*/ | ||
function setRef(host, name, elem) { | ||
host.componentModel.refs.cur[name] = elem; | ||
return 0; | ||
} | ||
/** | ||
* Sets static ref (e.g. ref which won’t be changed over time) to given host | ||
* @param {Component} host | ||
* @param {string} name | ||
* @param {Element} value | ||
*/ | ||
function setStaticRef(host, name, value) { | ||
value && value.setAttribute(getRefAttr(name, host), ''); | ||
host.refs[name] = value; | ||
} | ||
/** | ||
* Finalizes refs on given scope | ||
* @param {Component} host | ||
* @returns {number} Update status | ||
*/ | ||
function finalizeRefs(host) { | ||
return finalizeItems(host.componentModel.refs, changeRef, host); | ||
} | ||
/** | ||
* Invoked when element reference was changed | ||
* @param {string} name | ||
* @param {Element} prevValue | ||
* @param {Element} newValue | ||
* @param {Component} host | ||
*/ | ||
function changeRef(name, prevValue, newValue, host) { | ||
prevValue && prevValue.removeAttribute(getRefAttr(name, host)); | ||
setStaticRef(host, name, newValue); | ||
} | ||
/** | ||
* Returns attribute name to identify element in CSS | ||
* @param {String} name | ||
* @param {Component} host | ||
*/ | ||
function getRefAttr(name, host) { | ||
const cssScope$$1 = host.componentModel.definition.cssScope; | ||
return 'ref-' + name + (cssScope$$1 ? '-' + cssScope$$1 : ''); | ||
} | ||
/** | ||
* Creates element with given tag name | ||
* @param {string} tagName | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
*/ | ||
function elem(tagName, cssScope) { | ||
const el = document.createElement(tagName); | ||
cssScope && el.setAttribute(cssScope, ''); | ||
return el; | ||
} | ||
/** | ||
* Creates element with given tag name under `ns` namespace | ||
* @param {string} tagName | ||
* @param {string} ns | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
*/ | ||
function elemNS(tagName, ns, cssScope) { | ||
const el = document.createElementNS(ns, tagName); | ||
cssScope && el.setAttribute(cssScope, ''); | ||
return el; | ||
} | ||
/** | ||
* Creates element with given tag name and text | ||
* @param {string} tagName | ||
* @param {string} text | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
*/ | ||
function elemWithText(tagName, text, cssScope) { | ||
const el = elem(tagName, cssScope); | ||
el.textContent = textValue(text); | ||
return el; | ||
} | ||
/** | ||
* Creates element with given tag name under `ns` namespace and text | ||
* @param {string} tagName | ||
* @param {string} ns | ||
* @param {string} text | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
*/ | ||
function elemNSWithText(tagName, ns, text, cssScope) { | ||
const el = elemNS(tagName, ns, cssScope); | ||
el.textContent = textValue(text); | ||
return el; | ||
} | ||
/** | ||
* Creates text node with given value | ||
* @param {String} value | ||
* @returns {Text} | ||
*/ | ||
function text(value) { | ||
const node = document.createTextNode(textValue(value)); | ||
node['$value'] = value; | ||
return node; | ||
} | ||
/** | ||
* Updates given text node value, if required | ||
* @param {Text} node | ||
* @param {*} value | ||
* @returns {number} Returns `1` if text was updated, `0` otherwise | ||
*/ | ||
function updateText(node, value) { | ||
if (value !== node['$value']) { | ||
node.nodeValue = textValue(value); | ||
node['$value'] = value; | ||
return 1; | ||
} | ||
return 0; | ||
} | ||
/** | ||
* Returns textual representation of given `value` object | ||
* @param {*} value | ||
* @returns {string} | ||
*/ | ||
function textValue(value) { | ||
return value != null ? value : ''; | ||
} | ||
/** | ||
* Creates internal lightweight Endorphin component with given definition | ||
@@ -715,2 +1329,3 @@ * @param {string} name | ||
events: attachStaticEvents(element, definition), | ||
dispose: null, | ||
defaultProps | ||
@@ -785,5 +1400,12 @@ }; | ||
* @param {Component} elem | ||
* @returns {void} Should return nothing since function result will be used | ||
* as shorthand to reset cached value | ||
*/ | ||
function unmountComponent(elem$$1) { | ||
const { componentModel } = elem$$1; | ||
const { slots, input, dispose: dispose$$1 } = componentModel; | ||
const scope = getScope(elem$$1); | ||
runHook(elem$$1, 'willUnmount'); | ||
componentModel.mounted = false; | ||
@@ -798,3 +1420,3 @@ detachStaticEvents(elem$$1, componentModel.events); | ||
// XXX doesn’t remove static events (via direct call of `addStaticEvent()`) | ||
const ownHandlers = componentModel.input.events.prev; | ||
const ownHandlers = input.events.prev; | ||
for (let p in ownHandlers) { | ||
@@ -804,2 +1426,8 @@ elem$$1.removeEventListener(p, ownHandlers[p]); | ||
dispose$$1 && dispose$$1(scope); | ||
for (const slotName in slots) { | ||
disposeBlock(slots[slotName].block, scope, true); | ||
} | ||
runHook(elem$$1, 'didUnmount'); | ||
@@ -812,7 +1440,7 @@ elem$$1.componentModel = null; | ||
* @param {Component} component | ||
* @param {string[]} keys | ||
* @param {string[]} [keys] | ||
*/ | ||
function subscribeStore(component, keys) { | ||
if (!component.store) { | ||
throw new Error(`Store is not defined in ${component.nodeName} component definition`); | ||
throw new Error(`Store is not defined for ${component.nodeName} component`); | ||
} | ||
@@ -1014,570 +1642,3 @@ | ||
const blockKey = '&block'; | ||
/** | ||
* Creates injector instance for given target, if required | ||
* @param {Element} target | ||
* @returns {Injector} | ||
*/ | ||
function createInjector(target) { | ||
return { | ||
parentNode: target, | ||
items: [], | ||
ctx: null, | ||
ptr: 0, | ||
// NB create `slots` placeholder to promote object to hidden class. | ||
// Do not use any additional function argument for adding value to `slots` | ||
// to reduce runtime checks and keep functions in monomorphic state | ||
slots: null, | ||
attributes: changeSet(), | ||
events: changeSet() | ||
}; | ||
} | ||
/** | ||
* Creates block for given injector | ||
* @param {Injector} injector | ||
* @returns {Block} | ||
*/ | ||
function block(injector) { | ||
return add(injector, { | ||
[blockKey]: true, | ||
inserted: 0, | ||
deleted: 0, | ||
size: 0 | ||
}); | ||
} | ||
/** | ||
* Runs `fn` template function in context of given `block` | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {Function} fn | ||
* @param {Component} component | ||
* @param {*} data | ||
* @returns {*} Result of `fn` function call | ||
*/ | ||
function run(injector, block, fn, component, data) { | ||
let result; | ||
const ix = injector.items.indexOf(block); | ||
if (typeof fn === 'function') { | ||
const ctx = injector.ctx; | ||
injector.ptr = ix + 1; | ||
injector.ctx = block; | ||
result = fn(component, injector, data); | ||
injector.ctx = ctx; | ||
ctx ? consume(ctx, block) : reset(block); | ||
} | ||
injector.ptr = ix + block.size + 1; | ||
return result; | ||
} | ||
/** | ||
* Inserts given node into current context | ||
* @param {Injector} injector | ||
* @param {Node} node | ||
* @returns {Node} | ||
*/ | ||
function insert(injector, node, slotName = '') { | ||
let target; | ||
const { slots } = injector; | ||
if (slots) { | ||
target = slots[slotName] || (slots[slotName] = document.createDocumentFragment()); | ||
} else { | ||
target = injector.parentNode; | ||
} | ||
domInsert(node, target, getAnchorNode(injector.items, injector.ptr, target)); | ||
return add(injector, node); | ||
} | ||
/** | ||
* Moves contents of given block at `pos` location, effectively updating | ||
* inserted nodes in parent context | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {number} pos | ||
*/ | ||
function move(injector, block, pos) { | ||
const { items } = injector; | ||
if (items[pos] === block) { | ||
return; | ||
} | ||
// Move block contents at given position | ||
const curPos = items.indexOf(block); | ||
const blockItems = items.splice(curPos, block.size + 1); | ||
if (curPos < pos) { | ||
pos -= blockItems.length; | ||
} | ||
for (let i = blockItems.length - 1, item; i >= 0; i--) { | ||
item = /** @type {Element} */ (blockItems[i]); | ||
if (!isBlock(item)) { | ||
domInsert(item, item.parentNode, getAnchorNode(items, pos, item.parentNode)); | ||
} | ||
items.splice(pos, 0, item); | ||
} | ||
} | ||
/** | ||
* Disposes contents of given block | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {boolean} self Remove block item as well | ||
*/ | ||
function dispose(injector, block, self) { | ||
markDisposed(block, self); | ||
const { items, ctx } = injector; | ||
const ix = items.indexOf(block) + (self ? 0 : 1); | ||
const size = block.deleted; | ||
if (size) { | ||
ctx && consume(ctx, block); | ||
const removed = items.splice(ix, size); | ||
for (let i = 0, item; i < removed.length; i++) { | ||
item = removed[i]; | ||
if (!isBlock(item)) { | ||
disposeElement(/** @type {Node} */ (item)); | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Adds given item into current injector position | ||
* @param {Injector} injector | ||
* @param {InjectorItem} item | ||
*/ | ||
function add(injector, item) { | ||
injector.items.splice(injector.ptr++, 0, item); | ||
injector.ctx && markInsert(injector.ctx); | ||
return item; | ||
} | ||
/** | ||
* Get DOM node nearest to given position of items list | ||
* @param {InjectorItem[]} items | ||
* @param {number} ix | ||
* @param {Node} parent Ensure element has given element as parent node | ||
* @returns {Node} | ||
*/ | ||
function getAnchorNode(items, ix, parent) { | ||
while (ix < items.length) { | ||
const item = /** @type {Node} */ (items[ix++]); | ||
if (item.parentNode === parent) { | ||
return item; | ||
} | ||
} | ||
} | ||
/** | ||
* @param {Block} block | ||
*/ | ||
function markInsert(block) { | ||
block.inserted++; | ||
block.size++; | ||
} | ||
/** | ||
* Marks current block content as disposed | ||
* @param {Block} block | ||
* @param {boolean} self Marks block itself as removed | ||
*/ | ||
function markDisposed(block, self) { | ||
block.deleted += block.size + (self ? 1 : 0); | ||
block.size = 0; | ||
} | ||
/** | ||
* Consumes data from given `child` block by parent `block` | ||
* @param {Block} block | ||
*/ | ||
function consume(block, child) { | ||
block.inserted += child.inserted; | ||
block.deleted += child.deleted; | ||
block.size += child.inserted - child.deleted; | ||
reset(child); | ||
} | ||
/** | ||
* Reset session data from given block | ||
* @param {Block} block | ||
*/ | ||
function reset(block) { | ||
block.inserted = block.deleted = 0; | ||
} | ||
/** | ||
* Check if given value is a block | ||
* @param {*} obj | ||
* @returns {boolean} | ||
*/ | ||
function isBlock(obj$$1) { | ||
return blockKey in obj$$1; | ||
} | ||
/** | ||
* Disposes given element or component: notifies all descending components | ||
* with proper lifecycle hooks and detaches given element from DOM | ||
* @param {Node | Component} elem | ||
*/ | ||
function disposeElement(elem) { | ||
const components = collectComponents(elem, []); | ||
for (let i = 0; i < components.length; i++) { | ||
runHook(components[i], 'willUnmount'); | ||
} | ||
domRemove(elem); | ||
for (let i = 0; i < components.length; i++) { | ||
unmountComponent(components[i]); | ||
} | ||
} | ||
/** | ||
* Collects all nested components from given node (including node itself) | ||
* @param {Node | Component} node | ||
* @param {Array} to | ||
* @returns {Component[]} | ||
*/ | ||
function collectComponents(node, to) { | ||
if (/** @type {Component} */ (node).componentModel) { | ||
to.push(node); | ||
} | ||
let child = /** @type {Node} */ (node.firstChild); | ||
while (child) { | ||
collectComponents(child, to); | ||
child = child.nextSibling; | ||
} | ||
return to; | ||
} | ||
/** | ||
* @param {Node} node | ||
* @param {Node} parent | ||
* @param {Node} anchor | ||
* @returns {Node} Inserted item | ||
*/ | ||
function domInsert(node, parent, anchor) { | ||
return anchor | ||
? parent.insertBefore(node, anchor) | ||
: parent.appendChild(node); | ||
} | ||
/** | ||
* Removes given DOM node from its tree | ||
* @param {Node} node | ||
*/ | ||
function domRemove(node) { | ||
const parent = node.parentNode; | ||
parent && parent.removeChild(node); | ||
} | ||
/** | ||
* Initial block rendering | ||
* @param {Component} component | ||
* @param {Injector} injector | ||
* @param {Function} get | ||
* @returns {BlockContext} | ||
*/ | ||
function mountBlock(component, injector, get) { | ||
/** @type {BlockContext} */ | ||
const ctx = { | ||
component, | ||
injector, | ||
block: block(injector), | ||
get, | ||
fn: null, | ||
update: null | ||
}; | ||
updateBlock(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Updated block, described in `ctx` object | ||
* @param {BlockContext} ctx | ||
* @returns {number} Returns `1` if block was updated, `0` otherwise | ||
*/ | ||
function updateBlock(ctx) { | ||
let updated = 0; | ||
const { component, injector, block: block$$1, update } = ctx; | ||
const scope = getScope(component); | ||
const fn = ctx.get(component, scope, injector); | ||
if (ctx.fn !== fn) { | ||
updated = 1; | ||
// Unmount previously rendered content | ||
ctx.fn && dispose(injector, block$$1, false); | ||
// Mount new block content | ||
ctx.update = fn ? run(injector, block$$1, fn, component, scope) : null; | ||
ctx.fn = fn; | ||
} else if (update) { | ||
// Update rendered result | ||
updated = run(injector, block$$1, update, component, scope) ? 1 : 0; | ||
} | ||
return updated; | ||
} | ||
/** | ||
* Mounts iterator block | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {Function} get A function that returns collection to iterate | ||
* @param {Function} body A function that renders item of iterated collection | ||
* @returns {IteratorContext} | ||
*/ | ||
function mountIterator(host, injector, get, body) { | ||
/** @type {IteratorContext} */ | ||
const ctx = { | ||
host, | ||
injector, | ||
get, | ||
body, | ||
block: block(injector), | ||
index: 0, | ||
rendered: [], | ||
updated: 0 | ||
}; | ||
updateIterator(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Updates iterator block defined in `ctx` | ||
* @param {IteratorContext} ctx | ||
* @returns {number} Returns `1` if iterator was updated, `0` otherwise | ||
*/ | ||
function updateIterator(ctx) { | ||
run(ctx.injector, ctx.block, iteratorHost, ctx.host, ctx) ? 1 : 0; | ||
return ctx.updated; | ||
} | ||
/** | ||
* | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {IteratorContext} ctx | ||
*/ | ||
function iteratorHost(host, injector, ctx) { | ||
ctx.index = 0; | ||
ctx.updated = 0; | ||
const collection = ctx.get(host, getScope(host)); | ||
if (collection && typeof collection.forEach === 'function') { | ||
collection.forEach(iterator, ctx); | ||
} | ||
// Remove remaining blocks | ||
while (ctx.rendered.length > ctx.index) { | ||
ctx.updated = 1; | ||
dispose(injector, ctx.rendered.pop()[0], true); | ||
} | ||
} | ||
/** | ||
* @this {IteratorContext} | ||
* @param {*} value | ||
* @param {*} key | ||
*/ | ||
function iterator(value, key) { | ||
const { host, injector, rendered, index } = this; | ||
const localScope = { index, key, value }; | ||
if (index < rendered.length) { | ||
// Update existing block | ||
const [b, update, scope] = rendered[index]; | ||
setScope(host, assign(scope, localScope)); | ||
if (run(injector, b, update, host, scope)) { | ||
this.updated = 1; | ||
} | ||
exitScope(host); | ||
} else { | ||
// Create & render new block | ||
const b = block(injector); | ||
const scope = enterScope(host, localScope); | ||
const update = run(injector, b, this.body, host, scope); | ||
exitScope(host); | ||
rendered.push([b, update, scope]); | ||
this.updated = 1; | ||
} | ||
this.index++; | ||
} | ||
/** | ||
* Renders key iterator block | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {Function} get | ||
* @param {Function} keyExpr | ||
* @param {Function} body | ||
* @returns {KeyIteratorContext} | ||
*/ | ||
function mountKeyIterator(host, injector, get, keyExpr, body) { | ||
/** @type {KeyIteratorContext} */ | ||
const ctx = { | ||
host, | ||
injector, | ||
keyExpr, | ||
body, | ||
get, | ||
rendered: obj(), | ||
block: block(injector), | ||
index: 0, | ||
updated: 1, | ||
used: null | ||
}; | ||
updateKeyIterator(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Updates iterator block defined in `ctx` | ||
* @param {KeyIteratorContext} ctx | ||
* @returns {number} Returns `1` if iterator was updated, `0` otherwise | ||
*/ | ||
function updateKeyIterator(ctx) { | ||
run(ctx.injector, ctx.block, keyIteratorHost, ctx.host, ctx); | ||
return ctx.updated; | ||
} | ||
/** | ||
* | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {KeyIteratorContext} ctx | ||
*/ | ||
function keyIteratorHost(host, injector, ctx) { | ||
ctx.used = obj(); | ||
ctx.index = 0; | ||
ctx.updated = 1; | ||
const collection = ctx.get(host, getScope(host)); | ||
if (collection && typeof collection.forEach === 'function') { | ||
collection.forEach(iterator$1, ctx); | ||
} | ||
// Remove remaining blocks | ||
for (let k in ctx.rendered) { | ||
for (let i = 0, items = ctx.rendered[k]; i < items.length; i++) { | ||
ctx.updated = 1; | ||
dispose(injector, items[i][0], true); | ||
} | ||
} | ||
ctx.rendered = ctx.used; | ||
} | ||
/** | ||
* @this {KeyIteratorContext} | ||
* @param {*} value | ||
* @param {*} key | ||
*/ | ||
function iterator$1(value, key) { | ||
const { host, injector, index, used, rendered, keyExpr, body } = this; | ||
const localScope = { index, key, value }; | ||
const id = keyExpr(value, createScope(host, localScope)); | ||
let entry = id in rendered && rendered[id].shift(); | ||
if (entry) { | ||
// Update existing block | ||
const [b, update, scope] = entry; | ||
setScope(host, assign(scope, localScope)); | ||
move(injector, b, injector.ptr); | ||
if (run(injector, b, update, host, scope)) { | ||
this.updated = 1; | ||
} | ||
exitScope(host); | ||
} else { | ||
// Create & render new block | ||
const b = block(injector); | ||
const scope = enterScope(host, localScope); | ||
const update = run(injector, b, body, host, scope); | ||
this.updated = 1; | ||
exitScope(host); | ||
entry = [b, update, scope]; | ||
} | ||
// Mark block as used. | ||
// We allow multiple items key in case of poorly prepared data. | ||
if (id in used) { | ||
used[id].push(entry); | ||
} else { | ||
used[id] = [entry]; | ||
} | ||
this.index++; | ||
} | ||
/** | ||
* Sets runtime ref (e.g. ref which will be changed over time) to given host | ||
* @param {Component} host | ||
* @param {string} name | ||
* @param {HTMLElement} elem | ||
* @returns {number} Update status. Refs must be explicitly finalized, thus | ||
* we always return `0` as nothing was changed | ||
*/ | ||
function setRef(host, name, elem) { | ||
host.componentModel.refs.cur[name] = elem; | ||
return 0; | ||
} | ||
/** | ||
* Sets static ref (e.g. ref which won’t be changed over time) to given host | ||
* @param {Component} host | ||
* @param {string} name | ||
* @param {Element} value | ||
*/ | ||
function setStaticRef(host, name, value) { | ||
value && value.setAttribute(getRefAttr(name, host), ''); | ||
host.refs[name] = value; | ||
} | ||
/** | ||
* Finalizes refs on given scope | ||
* @param {Component} host | ||
* @returns {number} Update status | ||
*/ | ||
function finalizeRefs(host) { | ||
return finalizeItems(host.componentModel.refs, changeRef, host); | ||
} | ||
/** | ||
* Invoked when element reference was changed | ||
* @param {string} name | ||
* @param {Element} prevValue | ||
* @param {Element} newValue | ||
* @param {Component} host | ||
*/ | ||
function changeRef(name, prevValue, newValue, host) { | ||
prevValue && prevValue.removeAttribute(getRefAttr(name, host)); | ||
setStaticRef(host, name, newValue); | ||
} | ||
/** | ||
* Returns attribute name to identify element in CSS | ||
* @param {String} name | ||
* @param {Component} host | ||
*/ | ||
function getRefAttr(name, host) { | ||
const cssScope$$1 = host.componentModel.definition.cssScope; | ||
return 'ref-' + name + (cssScope$$1 ? '-' + cssScope$$1 : ''); | ||
} | ||
/** | ||
* Renders code, returned from `get` function, as HTML | ||
@@ -1596,2 +1657,3 @@ * @param {Component} host | ||
block: block(injector), | ||
scope: getScope(host), | ||
get, | ||
@@ -1611,3 +1673,3 @@ code: null, | ||
function updateInnerHTML(ctx) { | ||
const { host, injector, block: block$$1 } = ctx; | ||
const { host, injector, block: block$$1, scope } = ctx; | ||
const code = ctx.get(host, injector); | ||
@@ -1619,3 +1681,3 @@ let updated = 0; | ||
ctx.code = code; | ||
dispose(injector, block$$1, false); | ||
dispose(injector, block$$1, scope, false); | ||
isDefined(code) && run(injector, block$$1, renderHTML, host, ctx); | ||
@@ -1628,2 +1690,9 @@ } | ||
/** | ||
* @param {InnerHtmlContext} ctx | ||
*/ | ||
function unmountInnerHTML(ctx) { | ||
dispose(ctx.injector, ctx.block, ctx.scope, true); | ||
} | ||
/** | ||
* @param {Component} host | ||
@@ -1668,4 +1737,2 @@ * @param {Injector} injector | ||
function mountPartial(host, injector, partial, args) { | ||
// NB freeze scope context so all partial runtime objects can be reused | ||
// across renders | ||
/** @type {PartialContext} */ | ||
@@ -1676,2 +1743,3 @@ const ctx = { | ||
block: block(injector), | ||
baseScope: getScope(host), | ||
scope: null, | ||
@@ -1693,3 +1761,3 @@ update: null, | ||
function updatePartial(ctx, partial, args) { | ||
const { host, injector, block: block$$1 } = ctx; | ||
const { host, injector, block: block$$1, baseScope } = ctx; | ||
let updated = 0; | ||
@@ -1699,6 +1767,7 @@ | ||
// Unmount previously rendered partial | ||
ctx.partial && dispose(injector, block$$1, false); | ||
ctx.partial && dispose(injector, block$$1, ctx.scope, false); | ||
// Mount new partial | ||
const scope = ctx.scope = enterScope(host, assign(obj(partial.defaults), args)); | ||
const scope = ctx.scope = assign(obj(baseScope), partial.defaults, args); | ||
setScope(host, scope); | ||
ctx.update = partial ? run(injector, block$$1, partial.body, host, scope) : null; | ||
@@ -1720,2 +1789,9 @@ ctx.partial = partial; | ||
/** | ||
* @param {PartialContext} ctx | ||
*/ | ||
function unmountPartial(ctx) { | ||
dispose(ctx.injector, ctx.block, ctx.scope, true); | ||
} | ||
const prefix = '$'; | ||
@@ -1890,8 +1966,12 @@ | ||
exports.filter = filter; | ||
exports.addDisposeCallback = addDisposeCallback; | ||
exports.mountBlock = mountBlock; | ||
exports.updateBlock = updateBlock; | ||
exports.unmountBlock = unmountBlock; | ||
exports.mountIterator = mountIterator; | ||
exports.updateIterator = updateIterator; | ||
exports.unmountIterator = unmountIterator; | ||
exports.mountKeyIterator = mountKeyIterator; | ||
exports.updateKeyIterator = updateKeyIterator; | ||
exports.unmountKeyIterator = unmountKeyIterator; | ||
exports.createInjector = createInjector; | ||
@@ -1903,2 +1983,3 @@ exports.block = block; | ||
exports.dispose = dispose; | ||
exports.disposeBlock = disposeBlock; | ||
exports.enterScope = enterScope; | ||
@@ -1924,2 +2005,3 @@ exports.exitScope = exitScope; | ||
exports.mountSlot = mountSlot; | ||
exports.unmountSlot = unmountSlot; | ||
exports.updateSlots = updateSlots; | ||
@@ -1939,2 +2021,3 @@ exports.markSlotUpdate = markSlotUpdate; | ||
exports.updateInnerHTML = updateInnerHTML; | ||
exports.unmountInnerHTML = unmountInnerHTML; | ||
exports.elem = elem; | ||
@@ -1948,3 +2031,4 @@ exports.elemNS = elemNS; | ||
exports.updatePartial = updatePartial; | ||
exports.unmountPartial = unmountPartial; | ||
exports.Store = Store; | ||
//# sourceMappingURL=runtime.cjs.js.map |
@@ -143,18 +143,153 @@ /** | ||
/** | ||
* Invokes `fn` for each `name` hook in given definition | ||
* @param {ComponentDefinition} definition | ||
* @param {string} name | ||
* @param {function} fn | ||
* Marks given item as explicitly disposable for given host | ||
* @param {Component | Injector} host | ||
* @param {DisposeCallback} callback | ||
* @return {Component | Injector} | ||
*/ | ||
function forEachHook(definition, name, fn) { | ||
const { plugins } = definition; | ||
function addDisposeCallback(host, callback) { | ||
if (/** @type {Component} */ (host).componentModel) { | ||
/** @type {Component} */ (host).componentModel.dispose = callback; | ||
} else { | ||
/** @type {Injector} */ (host).ctx.dispose = callback; | ||
} | ||
if (plugins) { | ||
for (let i = 0; i < plugins.length; i++) { | ||
forEachHook(plugins[i], name, fn); | ||
return host; | ||
} | ||
const blockKey = '&block'; | ||
/** | ||
* Creates injector instance for given target, if required | ||
* @param {Element} target | ||
* @returns {Injector} | ||
*/ | ||
function createInjector(target) { | ||
return { | ||
parentNode: target, | ||
items: [], | ||
ctx: null, | ||
ptr: 0, | ||
// NB create `slots` placeholder to promote object to hidden class. | ||
// Do not use any additional function argument for adding value to `slots` | ||
// to reduce runtime checks and keep functions in monomorphic state | ||
slots: null, | ||
attributes: changeSet(), | ||
events: changeSet() | ||
}; | ||
} | ||
/** | ||
* Creates block for given injector | ||
* @param {Injector} injector | ||
* @returns {Block} | ||
*/ | ||
function block(injector) { | ||
return add(injector, { | ||
[blockKey]: true, | ||
inserted: 0, | ||
deleted: 0, | ||
size: 0, | ||
dispose: null | ||
}); | ||
} | ||
/** | ||
* Runs `fn` template function in context of given `block` | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {Function} fn | ||
* @param {Component} component | ||
* @param {*} data | ||
* @returns {*} Result of `fn` function call | ||
*/ | ||
function run(injector, block, fn, component, data) { | ||
let result; | ||
const ix = injector.items.indexOf(block); | ||
if (typeof fn === 'function') { | ||
const ctx = injector.ctx; | ||
injector.ptr = ix + 1; | ||
injector.ctx = block; | ||
result = fn(component, injector, data); | ||
injector.ctx = ctx; | ||
ctx ? consume(ctx, block) : reset(block); | ||
} | ||
injector.ptr = ix + block.size + 1; | ||
return result; | ||
} | ||
/** | ||
* Inserts given node into current context | ||
* @param {Injector} injector | ||
* @param {Node} node | ||
* @returns {Node} | ||
*/ | ||
function insert(injector, node, slotName = '') { | ||
let target; | ||
const { slots } = injector; | ||
if (slots) { | ||
target = slots[slotName] || (slots[slotName] = document.createDocumentFragment()); | ||
} else { | ||
target = injector.parentNode; | ||
} | ||
domInsert(node, target, getAnchorNode(injector.items, injector.ptr, target)); | ||
return add(injector, node); | ||
} | ||
/** | ||
* Moves contents of given block at `pos` location, effectively updating | ||
* inserted nodes in parent context | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {number} pos | ||
*/ | ||
function move(injector, block, pos) { | ||
const { items } = injector; | ||
if (items[pos] === block) { | ||
return; | ||
} | ||
// Move block contents at given position | ||
const curPos = items.indexOf(block); | ||
const blockItems = items.splice(curPos, block.size + 1); | ||
if (curPos < pos) { | ||
pos -= blockItems.length; | ||
} | ||
for (let i = blockItems.length - 1, item; i >= 0; i--) { | ||
item = /** @type {Element} */ (blockItems[i]); | ||
if (!isBlock(item)) { | ||
domInsert(item, item.parentNode, getAnchorNode(items, pos, item.parentNode)); | ||
} | ||
items.splice(pos, 0, item); | ||
} | ||
} | ||
if (name in definition) { | ||
return fn(definition[name], definition); | ||
/** | ||
* Disposes contents of given block | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {Object} scope | ||
* @param {boolean} self Remove block item as well | ||
*/ | ||
function dispose(injector, block, scope, self) { | ||
disposeBlock(block, scope, self); | ||
const { items, ctx } = injector; | ||
const ix = items.indexOf(block) + (self ? 0 : 1); | ||
const size = block.deleted; | ||
if (size) { | ||
ctx && consume(ctx, block); | ||
const removed = items.splice(ix, size); | ||
for (let i = 0; i < removed.length; i++) { | ||
domRemove(removed[i]); | ||
} | ||
} | ||
@@ -164,166 +299,464 @@ } | ||
/** | ||
* Invokes `name` hook for given component definition | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @param {Array} [args] | ||
* Disposes given block | ||
* @param {Block} block | ||
* @param {Object} scope | ||
* @param {boolean} self Dispose block itself | ||
* @returns {void} Should return nothing since function result will be used | ||
* as shorthand to reset cached value | ||
*/ | ||
function runHook(elem, name, args) { | ||
const hookArgs = args ? [elem].concat(args) : [elem]; | ||
function disposeBlock(block, scope, self) { | ||
if (block.dispose) { | ||
block.dispose(scope); | ||
block.dispose = null; | ||
} | ||
block.deleted += block.size + (self ? 1 : 0); | ||
block.size = 0; | ||
} | ||
return forEachHook(elem.componentModel.definition, name, hook => hook.apply(null, hookArgs)); | ||
/** | ||
* Adds given item into current injector position | ||
* @param {Injector} injector | ||
* @param {InjectorItem} item | ||
*/ | ||
function add(injector, item) { | ||
injector.items.splice(injector.ptr++, 0, item); | ||
injector.ctx && markInsert(injector.ctx); | ||
return item; | ||
} | ||
/** | ||
* Creates element with given tag name | ||
* @param {string} tagName | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
* Get DOM node nearest to given position of items list | ||
* @param {InjectorItem[]} items | ||
* @param {number} ix | ||
* @param {Node} parent Ensure element has given element as parent node | ||
* @returns {Node} | ||
*/ | ||
function elem(tagName, cssScope) { | ||
const el = document.createElement(tagName); | ||
cssScope && el.setAttribute(cssScope, ''); | ||
return el; | ||
function getAnchorNode(items, ix, parent) { | ||
while (ix < items.length) { | ||
const item = /** @type {Node} */ (items[ix++]); | ||
if (item.parentNode === parent) { | ||
return item; | ||
} | ||
} | ||
} | ||
/** | ||
* Creates element with given tag name under `ns` namespace | ||
* @param {string} tagName | ||
* @param {string} ns | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
* @param {Block} block | ||
*/ | ||
function elemNS(tagName, ns, cssScope) { | ||
const el = document.createElementNS(ns, tagName); | ||
cssScope && el.setAttribute(cssScope, ''); | ||
return el; | ||
function markInsert(block) { | ||
block.inserted++; | ||
block.size++; | ||
} | ||
/** | ||
* Creates element with given tag name and text | ||
* @param {string} tagName | ||
* @param {string} text | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
* Consumes data from given `child` block by parent `block` | ||
* @param {Block} block | ||
*/ | ||
function elemWithText(tagName, text, cssScope) { | ||
const el = elem(tagName, cssScope); | ||
el.textContent = textValue(text); | ||
return el; | ||
function consume(block, child) { | ||
block.inserted += child.inserted; | ||
block.deleted += child.deleted; | ||
block.size += child.inserted - child.deleted; | ||
reset(child); | ||
} | ||
/** | ||
* Creates element with given tag name under `ns` namespace and text | ||
* @param {string} tagName | ||
* @param {string} ns | ||
* @param {string} text | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
* Reset session data from given block | ||
* @param {Block} block | ||
*/ | ||
function elemNSWithText(tagName, ns, text, cssScope) { | ||
const el = elemNS(tagName, ns, cssScope); | ||
el.textContent = textValue(text); | ||
return el; | ||
function reset(block) { | ||
block.inserted = block.deleted = 0; | ||
} | ||
/** | ||
* Creates text node with given value | ||
* @param {String} value | ||
* @returns {Text} | ||
* Check if given value is a block | ||
* @param {*} obj | ||
* @returns {boolean} | ||
*/ | ||
function text(value) { | ||
const node = document.createTextNode(textValue(value)); | ||
node['$value'] = value; | ||
return node; | ||
function isBlock(obj$$1) { | ||
return blockKey in obj$$1; | ||
} | ||
/** | ||
* Updates given text node value, if required | ||
* @param {Text} node | ||
* @param {Node} node | ||
* @param {Node} parent | ||
* @param {Node} anchor | ||
* @returns {Node} Inserted item | ||
*/ | ||
function domInsert(node, parent, anchor) { | ||
return anchor | ||
? parent.insertBefore(node, anchor) | ||
: parent.appendChild(node); | ||
} | ||
/** | ||
* Removes given DOM node from its tree | ||
* @param {Node} node | ||
*/ | ||
function domRemove(node) { | ||
const parent = node.parentNode; | ||
parent && parent.removeChild(node); | ||
} | ||
/** | ||
* Enters new variable scope context | ||
* @param {Component} host | ||
* @param {object} incoming | ||
* @return {Object} | ||
*/ | ||
function enterScope(host, incoming) { | ||
return setScope(host, createScope(host, incoming)); | ||
} | ||
/** | ||
* Exit from current variable scope | ||
* @param {Component} host | ||
* @returns {Object} | ||
*/ | ||
function exitScope(host) { | ||
return setScope(host, Object.getPrototypeOf(host.componentModel.vars)); | ||
} | ||
/** | ||
* Creates new scope from given component state | ||
* @param {Component} host | ||
* @param {Object} [incoming] | ||
* @return {Object} | ||
*/ | ||
function createScope(host, incoming) { | ||
return assign(obj(host.componentModel.vars), incoming); | ||
} | ||
/** | ||
* Sets given object as current component scope | ||
* @param {Component} host | ||
* @param {Object} scope | ||
*/ | ||
function setScope(host, scope) { | ||
return host.componentModel.vars = scope; | ||
} | ||
/** | ||
* Returns current variable scope | ||
* @param {Component} elem | ||
* @returns {object} | ||
*/ | ||
function getScope(elem) { | ||
return elem.componentModel.vars; | ||
} | ||
/** | ||
* Returns property with given name from component | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @return {*} | ||
*/ | ||
function getProp(elem, name) { | ||
return elem.props[name]; | ||
} | ||
/** | ||
* Returns state value with given name from component | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @return {*} | ||
*/ | ||
function getState(elem, name) { | ||
return elem.state[name]; | ||
} | ||
/** | ||
* Returns value of given runtime variable from component | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @returns {*} | ||
*/ | ||
function getVar(elem, name) { | ||
return elem.componentModel.vars[name]; | ||
} | ||
/** | ||
* Sets value of given runtime variable for component | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @param {*} value | ||
* @returns {number} Returns `1` if text was updated, `0` otherwise | ||
*/ | ||
function updateText(node, value) { | ||
if (value !== node['$value']) { | ||
node.nodeValue = textValue(value); | ||
node['$value'] = value; | ||
return 1; | ||
function setVar(elem, name, value) { | ||
elem.componentModel.vars[name] = value; | ||
} | ||
/** | ||
* Initial block rendering | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {Function} get | ||
* @returns {BlockContext} | ||
*/ | ||
function mountBlock(host, injector, get) { | ||
/** @type {BlockContext} */ | ||
const ctx = { | ||
host, | ||
injector, | ||
block: block(injector), | ||
scope: getScope(host), | ||
get, | ||
fn: undefined, | ||
update: undefined | ||
}; | ||
updateBlock(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Updated block, described in `ctx` object | ||
* @param {BlockContext} ctx | ||
* @returns {number} Returns `1` if block was updated, `0` otherwise | ||
*/ | ||
function updateBlock(ctx) { | ||
let updated = 0; | ||
const { host, injector, scope, block: block$$1, update } = ctx; | ||
const fn = ctx.get(host, scope, injector); | ||
if (ctx.fn !== fn) { | ||
updated = 1; | ||
// Unmount previously rendered content | ||
ctx.fn && dispose(injector, block$$1, scope, false); | ||
// Mount new block content | ||
ctx.update = fn ? run(injector, block$$1, fn, host, scope) : null; | ||
ctx.fn = fn; | ||
} else if (update) { | ||
// Update rendered result | ||
updated = run(injector, block$$1, update, host, scope) ? 1 : 0; | ||
} | ||
return 0; | ||
return updated; | ||
} | ||
/** | ||
* Returns textual representation of given `value` object | ||
* @param {*} value | ||
* @returns {string} | ||
* @param {BlockContext} ctx | ||
*/ | ||
function textValue(value) { | ||
return value != null ? value : ''; | ||
function unmountBlock(ctx) { | ||
dispose(ctx.injector, ctx.block, ctx.scope, true); | ||
} | ||
/** | ||
* Adds pending event `name` handler | ||
* Mounts iterator block | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {string} name | ||
* @param {function} handler | ||
* @param {Function} get A function that returns collection to iterate | ||
* @param {Function} body A function that renders item of iterated collection | ||
* @returns {IteratorContext} | ||
*/ | ||
function addEvent(injector, name, handler) { | ||
injector.events.cur[name] = handler; | ||
function mountIterator(host, injector, get, body) { | ||
/** @type {IteratorContext} */ | ||
const ctx = { | ||
host, | ||
injector, | ||
get, | ||
body, | ||
block: block(injector), | ||
scope: getScope(host), | ||
index: 0, | ||
rendered: [], | ||
updated: 0 | ||
}; | ||
updateIterator(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Adds given `handler` as event `name` listener | ||
* @param {Element} elem | ||
* @param {string} name | ||
* @param {EventListener} handler | ||
* Updates iterator block defined in `ctx` | ||
* @param {IteratorContext} ctx | ||
* @returns {number} Returns `1` if iterator was updated, `0` otherwise | ||
*/ | ||
function addStaticEvent(elem, name, handler) { | ||
handler && elem.addEventListener(name, handler); | ||
function updateIterator(ctx) { | ||
run(ctx.injector, ctx.block, iteratorHost, ctx.host, ctx) ? 1 : 0; | ||
return ctx.updated; | ||
} | ||
/** | ||
* Finalizes events of given injector | ||
* | ||
* @param {IteratorContext} ctx | ||
*/ | ||
function unmountIterator(ctx) { | ||
const { rendered, injector } = ctx; | ||
let item; | ||
while (item = rendered.pop()) { | ||
dispose(injector, item[0], item[2], true); | ||
} | ||
} | ||
/** | ||
* | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @returns {number} Update status | ||
* @param {IteratorContext} ctx | ||
*/ | ||
function finalizeEvents(injector) { | ||
return finalizeItems(injector.events, changeEvent, injector.parentNode); | ||
function iteratorHost(host, injector, ctx) { | ||
ctx.index = 0; | ||
ctx.updated = 0; | ||
const collection = ctx.get(host, ctx.scope); | ||
if (collection && typeof collection.forEach === 'function') { | ||
collection.forEach(iterator, ctx); | ||
} | ||
// Remove remaining blocks | ||
let item; | ||
while (ctx.rendered.length > ctx.index) { | ||
ctx.updated = 1; | ||
item = ctx.rendered.pop(); | ||
dispose(injector, item[0], item[2], true); | ||
} | ||
} | ||
/** | ||
* Returns function that must be invoked as event handler for given component | ||
* @param {Component} component | ||
* @param {string} name Method name | ||
* @param {HTMLElement} ctx Context element where event listener was added | ||
* @returns {function?} | ||
* @this {IteratorContext} | ||
* @param {*} value | ||
* @param {*} key | ||
*/ | ||
function getEventHandler(component, name, ctx) { | ||
let fn; | ||
function iterator(value, key) { | ||
const { host, injector, rendered, index } = this; | ||
const localScope = { index, key, value }; | ||
if (typeof component[name] === 'function') { | ||
fn = component[name].bind(component); | ||
if (index < rendered.length) { | ||
// Update existing block | ||
const [b, update, scope] = rendered[index]; | ||
setScope(host, assign(scope, localScope)); | ||
if (run(injector, b, update, host, scope)) { | ||
this.updated = 1; | ||
} | ||
exitScope(host); | ||
} else { | ||
const handler = component.componentModel.definition[name]; | ||
if (typeof handler === 'function') { | ||
fn = handler.bind(ctx); | ||
// Create & render new block | ||
const b = block(injector); | ||
const scope = enterScope(host, localScope); | ||
const update = run(injector, b, this.body, host, scope); | ||
exitScope(host); | ||
rendered.push([b, update, scope]); | ||
this.updated = 1; | ||
} | ||
this.index++; | ||
} | ||
/** | ||
* Renders key iterator block | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {Function} get | ||
* @param {Function} keyExpr | ||
* @param {Function} body | ||
* @returns {KeyIteratorContext} | ||
*/ | ||
function mountKeyIterator(host, injector, get, keyExpr, body) { | ||
/** @type {KeyIteratorContext} */ | ||
const ctx = { | ||
host, | ||
injector, | ||
keyExpr, | ||
body, | ||
get, | ||
rendered: obj(), | ||
block: block(injector), | ||
scope: getScope(host), | ||
index: 0, | ||
updated: 1, | ||
used: null | ||
}; | ||
updateKeyIterator(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Updates iterator block defined in `ctx` | ||
* @param {KeyIteratorContext} ctx | ||
* @returns {number} Returns `1` if iterator was updated, `0` otherwise | ||
*/ | ||
function updateKeyIterator(ctx) { | ||
run(ctx.injector, ctx.block, keyIteratorHost, ctx.host, ctx); | ||
return ctx.updated; | ||
} | ||
/** | ||
* @param {KeyIteratorContext} ctx | ||
*/ | ||
function unmountKeyIterator(ctx) { | ||
const { rendered, injector } = ctx; | ||
let items, item; | ||
for (let k in rendered) { | ||
items = rendered[k]; | ||
while (item = items.pop()) { | ||
dispose(injector, item[0], item[2], true); | ||
} | ||
} | ||
} | ||
if (fn) { | ||
fn.displayName = name; | ||
/** | ||
* | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {KeyIteratorContext} ctx | ||
*/ | ||
function keyIteratorHost(host, injector, ctx) { | ||
ctx.used = obj(); | ||
ctx.index = 0; | ||
ctx.updated = 1; | ||
const collection = ctx.get(host, ctx.scope); | ||
if (collection && typeof collection.forEach === 'function') { | ||
collection.forEach(iterator$1, ctx); | ||
} | ||
return fn; | ||
// Remove remaining blocks | ||
for (let k in ctx.rendered) { | ||
for (let i = 0, items = ctx.rendered[k]; i < items.length; i++) { | ||
ctx.updated = 1; | ||
dispose(injector, items[i][0], items[i][2], true); | ||
} | ||
} | ||
ctx.rendered = ctx.used; | ||
} | ||
/** | ||
* Invoked when event handler was changed | ||
* @param {string} name | ||
* @param {EventListener} prevValue | ||
* @param {EventListener} newValue | ||
* @param {Element} elem | ||
* @this {KeyIteratorContext} | ||
* @param {*} value | ||
* @param {*} key | ||
*/ | ||
function changeEvent(name, prevValue, newValue, elem) { | ||
prevValue && elem.removeEventListener(name, prevValue); | ||
addStaticEvent(elem, name, newValue); | ||
function iterator$1(value, key) { | ||
const { host, injector, index, used, rendered, keyExpr, body } = this; | ||
const localScope = { index, key, value }; | ||
const id = keyExpr(value, createScope(host, localScope)); | ||
let entry = id in rendered && rendered[id].shift(); | ||
if (entry) { | ||
// Update existing block | ||
const [b, update, scope] = entry; | ||
setScope(host, assign(scope, localScope)); | ||
move(injector, b, injector.ptr); | ||
if (run(injector, b, update, host, scope)) { | ||
this.updated = 1; | ||
} | ||
exitScope(host); | ||
} else { | ||
// Create & render new block | ||
const b = block(injector); | ||
const scope = enterScope(host, localScope); | ||
const update = run(injector, b, body, host, scope); | ||
this.updated = 1; | ||
exitScope(host); | ||
entry = [b, update, scope]; | ||
} | ||
// Mark block as used. | ||
// We allow multiple items key in case of poorly prepared data. | ||
if (id in used) { | ||
used[id].push(entry); | ||
} else { | ||
used[id] = [entry]; | ||
} | ||
this.index++; | ||
} | ||
@@ -450,86 +883,98 @@ | ||
/** | ||
* Enters new variable scope context | ||
* @param {Component} host | ||
* @param {object} incoming | ||
* @return {Object} | ||
* Adds pending event `name` handler | ||
* @param {Injector} injector | ||
* @param {string} name | ||
* @param {function} handler | ||
*/ | ||
function enterScope(host, incoming) { | ||
return setScope(host, createScope(host, incoming)); | ||
function addEvent(injector, name, handler) { | ||
injector.events.cur[name] = handler; | ||
} | ||
/** | ||
* Exit from current variable scope | ||
* @param {Component} host | ||
* @returns {Object} | ||
* Adds given `handler` as event `name` listener | ||
* @param {Element} elem | ||
* @param {string} name | ||
* @param {EventListener} handler | ||
*/ | ||
function exitScope(host) { | ||
return setScope(host, Object.getPrototypeOf(host.componentModel.vars)); | ||
function addStaticEvent(elem, name, handler) { | ||
handler && elem.addEventListener(name, handler); | ||
} | ||
/** | ||
* Creates new scope from given component state | ||
* @param {Component} host | ||
* @param {Object} [incoming] | ||
* @return {Object} | ||
* Finalizes events of given injector | ||
* @param {Injector} injector | ||
* @returns {number} Update status | ||
*/ | ||
function createScope(host, incoming) { | ||
return assign(obj(host.componentModel.vars), incoming); | ||
function finalizeEvents(injector) { | ||
return finalizeItems(injector.events, changeEvent, injector.parentNode); | ||
} | ||
/** | ||
* Sets given object as current component scope | ||
* @param {Component} host | ||
* @param {Object} scope | ||
* Returns function that must be invoked as event handler for given component | ||
* @param {Component} component | ||
* @param {string} name Method name | ||
* @param {HTMLElement} ctx Context element where event listener was added | ||
* @returns {function?} | ||
*/ | ||
function setScope(host, scope) { | ||
return host.componentModel.vars = scope; | ||
} | ||
function getEventHandler(component, name, ctx) { | ||
let fn; | ||
/** | ||
* Returns current variable scope | ||
* @param {Component} elem | ||
* @returns {object} | ||
*/ | ||
function getScope(elem) { | ||
return elem.componentModel.vars; | ||
if (typeof component[name] === 'function') { | ||
fn = component[name].bind(component); | ||
} else { | ||
const handler = component.componentModel.definition[name]; | ||
if (typeof handler === 'function') { | ||
fn = handler.bind(ctx); | ||
} | ||
} | ||
if (fn) { | ||
fn.displayName = name; | ||
} | ||
return fn; | ||
} | ||
/** | ||
* Returns property with given name from component | ||
* @param {Component} elem | ||
* Invoked when event handler was changed | ||
* @param {string} name | ||
* @return {*} | ||
* @param {EventListener} prevValue | ||
* @param {EventListener} newValue | ||
* @param {Element} elem | ||
*/ | ||
function getProp(elem, name) { | ||
return elem.props[name]; | ||
function changeEvent(name, prevValue, newValue, elem) { | ||
prevValue && elem.removeEventListener(name, prevValue); | ||
addStaticEvent(elem, name, newValue); | ||
} | ||
/** | ||
* Returns state value with given name from component | ||
* @param {Component} elem | ||
* Invokes `fn` for each `name` hook in given definition | ||
* @param {ComponentDefinition} definition | ||
* @param {string} name | ||
* @return {*} | ||
* @param {function} fn | ||
*/ | ||
function getState(elem, name) { | ||
return elem.state[name]; | ||
function forEachHook(definition, name, fn) { | ||
const { plugins } = definition; | ||
if (plugins) { | ||
for (let i = 0; i < plugins.length; i++) { | ||
forEachHook(plugins[i], name, fn); | ||
} | ||
} | ||
if (name in definition) { | ||
return fn(definition[name], definition); | ||
} | ||
} | ||
/** | ||
* Returns value of given runtime variable from component | ||
* Invokes `name` hook for given component definition | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @returns {*} | ||
* @param {Array} [args] | ||
*/ | ||
function getVar(elem, name) { | ||
return elem.componentModel.vars[name]; | ||
} | ||
function runHook(elem, name, args) { | ||
const hookArgs = args ? [elem].concat(args) : [elem]; | ||
/** | ||
* Sets value of given runtime variable for component | ||
* @param {Component} elem | ||
* @param {string} name | ||
* @param {*} value | ||
*/ | ||
function setVar(elem, name, value) { | ||
elem.componentModel.vars[name] = value; | ||
return forEachHook(elem.componentModel.definition, name, hook => hook.apply(null, hookArgs)); | ||
} | ||
@@ -543,15 +988,41 @@ | ||
* @param {Function} [defaultContent] Function for rendering default slot content | ||
* @return {SlotContext} | ||
*/ | ||
function mountSlot(host, name, elem, defaultContent) { | ||
const blockEntry = (host, scope, injector) => { | ||
if (!renderSlot(host, injector)) { | ||
return defaultContent; | ||
} | ||
}; | ||
/** @type {SlotContext} */ | ||
const ctx = { host, name, defaultContent, isDefault: false }; | ||
const { slots } = host.componentModel; | ||
const { slots } = host.componentModel; | ||
/** | ||
* @param {Component} host | ||
* @param {Object} scope | ||
* @param {Injector} injector | ||
*/ | ||
function blockEntry(host, scope, injector) { | ||
ctx.isDefault = !renderSlot(host, injector); | ||
return ctx.isDefault ? ctx.defaultContent : null; | ||
} | ||
slots[name] = mountBlock(host, createInjector(elem), blockEntry); | ||
return ctx; | ||
} | ||
/** | ||
* Unmounts given slot | ||
* @param {SlotContext} ctx | ||
*/ | ||
function unmountSlot(ctx) { | ||
const { host, name } = ctx; | ||
const { slots } = host.componentModel; | ||
if (ctx.isDefault) { | ||
unmountBlock(slots[name]); | ||
} | ||
ctx.defaultContent = null; | ||
delete slots[name]; | ||
} | ||
/** | ||
* Sync slot content if necessary | ||
@@ -624,2 +1095,145 @@ * @param {Component} host | ||
/** | ||
* Sets runtime ref (e.g. ref which will be changed over time) to given host | ||
* @param {Component} host | ||
* @param {string} name | ||
* @param {HTMLElement} elem | ||
* @returns {number} Update status. Refs must be explicitly finalized, thus | ||
* we always return `0` as nothing was changed | ||
*/ | ||
function setRef(host, name, elem) { | ||
host.componentModel.refs.cur[name] = elem; | ||
return 0; | ||
} | ||
/** | ||
* Sets static ref (e.g. ref which won’t be changed over time) to given host | ||
* @param {Component} host | ||
* @param {string} name | ||
* @param {Element} value | ||
*/ | ||
function setStaticRef(host, name, value) { | ||
value && value.setAttribute(getRefAttr(name, host), ''); | ||
host.refs[name] = value; | ||
} | ||
/** | ||
* Finalizes refs on given scope | ||
* @param {Component} host | ||
* @returns {number} Update status | ||
*/ | ||
function finalizeRefs(host) { | ||
return finalizeItems(host.componentModel.refs, changeRef, host); | ||
} | ||
/** | ||
* Invoked when element reference was changed | ||
* @param {string} name | ||
* @param {Element} prevValue | ||
* @param {Element} newValue | ||
* @param {Component} host | ||
*/ | ||
function changeRef(name, prevValue, newValue, host) { | ||
prevValue && prevValue.removeAttribute(getRefAttr(name, host)); | ||
setStaticRef(host, name, newValue); | ||
} | ||
/** | ||
* Returns attribute name to identify element in CSS | ||
* @param {String} name | ||
* @param {Component} host | ||
*/ | ||
function getRefAttr(name, host) { | ||
const cssScope$$1 = host.componentModel.definition.cssScope; | ||
return 'ref-' + name + (cssScope$$1 ? '-' + cssScope$$1 : ''); | ||
} | ||
/** | ||
* Creates element with given tag name | ||
* @param {string} tagName | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
*/ | ||
function elem(tagName, cssScope) { | ||
const el = document.createElement(tagName); | ||
cssScope && el.setAttribute(cssScope, ''); | ||
return el; | ||
} | ||
/** | ||
* Creates element with given tag name under `ns` namespace | ||
* @param {string} tagName | ||
* @param {string} ns | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
*/ | ||
function elemNS(tagName, ns, cssScope) { | ||
const el = document.createElementNS(ns, tagName); | ||
cssScope && el.setAttribute(cssScope, ''); | ||
return el; | ||
} | ||
/** | ||
* Creates element with given tag name and text | ||
* @param {string} tagName | ||
* @param {string} text | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
*/ | ||
function elemWithText(tagName, text, cssScope) { | ||
const el = elem(tagName, cssScope); | ||
el.textContent = textValue(text); | ||
return el; | ||
} | ||
/** | ||
* Creates element with given tag name under `ns` namespace and text | ||
* @param {string} tagName | ||
* @param {string} ns | ||
* @param {string} text | ||
* @param {string} [cssScope] Scope for CSS isolation | ||
* @return {Element} | ||
*/ | ||
function elemNSWithText(tagName, ns, text, cssScope) { | ||
const el = elemNS(tagName, ns, cssScope); | ||
el.textContent = textValue(text); | ||
return el; | ||
} | ||
/** | ||
* Creates text node with given value | ||
* @param {String} value | ||
* @returns {Text} | ||
*/ | ||
function text(value) { | ||
const node = document.createTextNode(textValue(value)); | ||
node['$value'] = value; | ||
return node; | ||
} | ||
/** | ||
* Updates given text node value, if required | ||
* @param {Text} node | ||
* @param {*} value | ||
* @returns {number} Returns `1` if text was updated, `0` otherwise | ||
*/ | ||
function updateText(node, value) { | ||
if (value !== node['$value']) { | ||
node.nodeValue = textValue(value); | ||
node['$value'] = value; | ||
return 1; | ||
} | ||
return 0; | ||
} | ||
/** | ||
* Returns textual representation of given `value` object | ||
* @param {*} value | ||
* @returns {string} | ||
*/ | ||
function textValue(value) { | ||
return value != null ? value : ''; | ||
} | ||
/** | ||
* Creates internal lightweight Endorphin component with given definition | ||
@@ -711,2 +1325,3 @@ * @param {string} name | ||
events: attachStaticEvents(element, definition), | ||
dispose: null, | ||
defaultProps | ||
@@ -781,5 +1396,12 @@ }; | ||
* @param {Component} elem | ||
* @returns {void} Should return nothing since function result will be used | ||
* as shorthand to reset cached value | ||
*/ | ||
function unmountComponent(elem$$1) { | ||
const { componentModel } = elem$$1; | ||
const { slots, input, dispose: dispose$$1 } = componentModel; | ||
const scope = getScope(elem$$1); | ||
runHook(elem$$1, 'willUnmount'); | ||
componentModel.mounted = false; | ||
@@ -794,3 +1416,3 @@ detachStaticEvents(elem$$1, componentModel.events); | ||
// XXX doesn’t remove static events (via direct call of `addStaticEvent()`) | ||
const ownHandlers = componentModel.input.events.prev; | ||
const ownHandlers = input.events.prev; | ||
for (let p in ownHandlers) { | ||
@@ -800,2 +1422,8 @@ elem$$1.removeEventListener(p, ownHandlers[p]); | ||
dispose$$1 && dispose$$1(scope); | ||
for (const slotName in slots) { | ||
disposeBlock(slots[slotName].block, scope, true); | ||
} | ||
runHook(elem$$1, 'didUnmount'); | ||
@@ -808,7 +1436,7 @@ elem$$1.componentModel = null; | ||
* @param {Component} component | ||
* @param {string[]} keys | ||
* @param {string[]} [keys] | ||
*/ | ||
function subscribeStore(component, keys) { | ||
if (!component.store) { | ||
throw new Error(`Store is not defined in ${component.nodeName} component definition`); | ||
throw new Error(`Store is not defined for ${component.nodeName} component`); | ||
} | ||
@@ -1010,570 +1638,3 @@ | ||
const blockKey = '&block'; | ||
/** | ||
* Creates injector instance for given target, if required | ||
* @param {Element} target | ||
* @returns {Injector} | ||
*/ | ||
function createInjector(target) { | ||
return { | ||
parentNode: target, | ||
items: [], | ||
ctx: null, | ||
ptr: 0, | ||
// NB create `slots` placeholder to promote object to hidden class. | ||
// Do not use any additional function argument for adding value to `slots` | ||
// to reduce runtime checks and keep functions in monomorphic state | ||
slots: null, | ||
attributes: changeSet(), | ||
events: changeSet() | ||
}; | ||
} | ||
/** | ||
* Creates block for given injector | ||
* @param {Injector} injector | ||
* @returns {Block} | ||
*/ | ||
function block(injector) { | ||
return add(injector, { | ||
[blockKey]: true, | ||
inserted: 0, | ||
deleted: 0, | ||
size: 0 | ||
}); | ||
} | ||
/** | ||
* Runs `fn` template function in context of given `block` | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {Function} fn | ||
* @param {Component} component | ||
* @param {*} data | ||
* @returns {*} Result of `fn` function call | ||
*/ | ||
function run(injector, block, fn, component, data) { | ||
let result; | ||
const ix = injector.items.indexOf(block); | ||
if (typeof fn === 'function') { | ||
const ctx = injector.ctx; | ||
injector.ptr = ix + 1; | ||
injector.ctx = block; | ||
result = fn(component, injector, data); | ||
injector.ctx = ctx; | ||
ctx ? consume(ctx, block) : reset(block); | ||
} | ||
injector.ptr = ix + block.size + 1; | ||
return result; | ||
} | ||
/** | ||
* Inserts given node into current context | ||
* @param {Injector} injector | ||
* @param {Node} node | ||
* @returns {Node} | ||
*/ | ||
function insert(injector, node, slotName = '') { | ||
let target; | ||
const { slots } = injector; | ||
if (slots) { | ||
target = slots[slotName] || (slots[slotName] = document.createDocumentFragment()); | ||
} else { | ||
target = injector.parentNode; | ||
} | ||
domInsert(node, target, getAnchorNode(injector.items, injector.ptr, target)); | ||
return add(injector, node); | ||
} | ||
/** | ||
* Moves contents of given block at `pos` location, effectively updating | ||
* inserted nodes in parent context | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {number} pos | ||
*/ | ||
function move(injector, block, pos) { | ||
const { items } = injector; | ||
if (items[pos] === block) { | ||
return; | ||
} | ||
// Move block contents at given position | ||
const curPos = items.indexOf(block); | ||
const blockItems = items.splice(curPos, block.size + 1); | ||
if (curPos < pos) { | ||
pos -= blockItems.length; | ||
} | ||
for (let i = blockItems.length - 1, item; i >= 0; i--) { | ||
item = /** @type {Element} */ (blockItems[i]); | ||
if (!isBlock(item)) { | ||
domInsert(item, item.parentNode, getAnchorNode(items, pos, item.parentNode)); | ||
} | ||
items.splice(pos, 0, item); | ||
} | ||
} | ||
/** | ||
* Disposes contents of given block | ||
* @param {Injector} injector | ||
* @param {Block} block | ||
* @param {boolean} self Remove block item as well | ||
*/ | ||
function dispose(injector, block, self) { | ||
markDisposed(block, self); | ||
const { items, ctx } = injector; | ||
const ix = items.indexOf(block) + (self ? 0 : 1); | ||
const size = block.deleted; | ||
if (size) { | ||
ctx && consume(ctx, block); | ||
const removed = items.splice(ix, size); | ||
for (let i = 0, item; i < removed.length; i++) { | ||
item = removed[i]; | ||
if (!isBlock(item)) { | ||
disposeElement(/** @type {Node} */ (item)); | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Adds given item into current injector position | ||
* @param {Injector} injector | ||
* @param {InjectorItem} item | ||
*/ | ||
function add(injector, item) { | ||
injector.items.splice(injector.ptr++, 0, item); | ||
injector.ctx && markInsert(injector.ctx); | ||
return item; | ||
} | ||
/** | ||
* Get DOM node nearest to given position of items list | ||
* @param {InjectorItem[]} items | ||
* @param {number} ix | ||
* @param {Node} parent Ensure element has given element as parent node | ||
* @returns {Node} | ||
*/ | ||
function getAnchorNode(items, ix, parent) { | ||
while (ix < items.length) { | ||
const item = /** @type {Node} */ (items[ix++]); | ||
if (item.parentNode === parent) { | ||
return item; | ||
} | ||
} | ||
} | ||
/** | ||
* @param {Block} block | ||
*/ | ||
function markInsert(block) { | ||
block.inserted++; | ||
block.size++; | ||
} | ||
/** | ||
* Marks current block content as disposed | ||
* @param {Block} block | ||
* @param {boolean} self Marks block itself as removed | ||
*/ | ||
function markDisposed(block, self) { | ||
block.deleted += block.size + (self ? 1 : 0); | ||
block.size = 0; | ||
} | ||
/** | ||
* Consumes data from given `child` block by parent `block` | ||
* @param {Block} block | ||
*/ | ||
function consume(block, child) { | ||
block.inserted += child.inserted; | ||
block.deleted += child.deleted; | ||
block.size += child.inserted - child.deleted; | ||
reset(child); | ||
} | ||
/** | ||
* Reset session data from given block | ||
* @param {Block} block | ||
*/ | ||
function reset(block) { | ||
block.inserted = block.deleted = 0; | ||
} | ||
/** | ||
* Check if given value is a block | ||
* @param {*} obj | ||
* @returns {boolean} | ||
*/ | ||
function isBlock(obj$$1) { | ||
return blockKey in obj$$1; | ||
} | ||
/** | ||
* Disposes given element or component: notifies all descending components | ||
* with proper lifecycle hooks and detaches given element from DOM | ||
* @param {Node | Component} elem | ||
*/ | ||
function disposeElement(elem) { | ||
const components = collectComponents(elem, []); | ||
for (let i = 0; i < components.length; i++) { | ||
runHook(components[i], 'willUnmount'); | ||
} | ||
domRemove(elem); | ||
for (let i = 0; i < components.length; i++) { | ||
unmountComponent(components[i]); | ||
} | ||
} | ||
/** | ||
* Collects all nested components from given node (including node itself) | ||
* @param {Node | Component} node | ||
* @param {Array} to | ||
* @returns {Component[]} | ||
*/ | ||
function collectComponents(node, to) { | ||
if (/** @type {Component} */ (node).componentModel) { | ||
to.push(node); | ||
} | ||
let child = /** @type {Node} */ (node.firstChild); | ||
while (child) { | ||
collectComponents(child, to); | ||
child = child.nextSibling; | ||
} | ||
return to; | ||
} | ||
/** | ||
* @param {Node} node | ||
* @param {Node} parent | ||
* @param {Node} anchor | ||
* @returns {Node} Inserted item | ||
*/ | ||
function domInsert(node, parent, anchor) { | ||
return anchor | ||
? parent.insertBefore(node, anchor) | ||
: parent.appendChild(node); | ||
} | ||
/** | ||
* Removes given DOM node from its tree | ||
* @param {Node} node | ||
*/ | ||
function domRemove(node) { | ||
const parent = node.parentNode; | ||
parent && parent.removeChild(node); | ||
} | ||
/** | ||
* Initial block rendering | ||
* @param {Component} component | ||
* @param {Injector} injector | ||
* @param {Function} get | ||
* @returns {BlockContext} | ||
*/ | ||
function mountBlock(component, injector, get) { | ||
/** @type {BlockContext} */ | ||
const ctx = { | ||
component, | ||
injector, | ||
block: block(injector), | ||
get, | ||
fn: null, | ||
update: null | ||
}; | ||
updateBlock(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Updated block, described in `ctx` object | ||
* @param {BlockContext} ctx | ||
* @returns {number} Returns `1` if block was updated, `0` otherwise | ||
*/ | ||
function updateBlock(ctx) { | ||
let updated = 0; | ||
const { component, injector, block: block$$1, update } = ctx; | ||
const scope = getScope(component); | ||
const fn = ctx.get(component, scope, injector); | ||
if (ctx.fn !== fn) { | ||
updated = 1; | ||
// Unmount previously rendered content | ||
ctx.fn && dispose(injector, block$$1, false); | ||
// Mount new block content | ||
ctx.update = fn ? run(injector, block$$1, fn, component, scope) : null; | ||
ctx.fn = fn; | ||
} else if (update) { | ||
// Update rendered result | ||
updated = run(injector, block$$1, update, component, scope) ? 1 : 0; | ||
} | ||
return updated; | ||
} | ||
/** | ||
* Mounts iterator block | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {Function} get A function that returns collection to iterate | ||
* @param {Function} body A function that renders item of iterated collection | ||
* @returns {IteratorContext} | ||
*/ | ||
function mountIterator(host, injector, get, body) { | ||
/** @type {IteratorContext} */ | ||
const ctx = { | ||
host, | ||
injector, | ||
get, | ||
body, | ||
block: block(injector), | ||
index: 0, | ||
rendered: [], | ||
updated: 0 | ||
}; | ||
updateIterator(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Updates iterator block defined in `ctx` | ||
* @param {IteratorContext} ctx | ||
* @returns {number} Returns `1` if iterator was updated, `0` otherwise | ||
*/ | ||
function updateIterator(ctx) { | ||
run(ctx.injector, ctx.block, iteratorHost, ctx.host, ctx) ? 1 : 0; | ||
return ctx.updated; | ||
} | ||
/** | ||
* | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {IteratorContext} ctx | ||
*/ | ||
function iteratorHost(host, injector, ctx) { | ||
ctx.index = 0; | ||
ctx.updated = 0; | ||
const collection = ctx.get(host, getScope(host)); | ||
if (collection && typeof collection.forEach === 'function') { | ||
collection.forEach(iterator, ctx); | ||
} | ||
// Remove remaining blocks | ||
while (ctx.rendered.length > ctx.index) { | ||
ctx.updated = 1; | ||
dispose(injector, ctx.rendered.pop()[0], true); | ||
} | ||
} | ||
/** | ||
* @this {IteratorContext} | ||
* @param {*} value | ||
* @param {*} key | ||
*/ | ||
function iterator(value, key) { | ||
const { host, injector, rendered, index } = this; | ||
const localScope = { index, key, value }; | ||
if (index < rendered.length) { | ||
// Update existing block | ||
const [b, update, scope] = rendered[index]; | ||
setScope(host, assign(scope, localScope)); | ||
if (run(injector, b, update, host, scope)) { | ||
this.updated = 1; | ||
} | ||
exitScope(host); | ||
} else { | ||
// Create & render new block | ||
const b = block(injector); | ||
const scope = enterScope(host, localScope); | ||
const update = run(injector, b, this.body, host, scope); | ||
exitScope(host); | ||
rendered.push([b, update, scope]); | ||
this.updated = 1; | ||
} | ||
this.index++; | ||
} | ||
/** | ||
* Renders key iterator block | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {Function} get | ||
* @param {Function} keyExpr | ||
* @param {Function} body | ||
* @returns {KeyIteratorContext} | ||
*/ | ||
function mountKeyIterator(host, injector, get, keyExpr, body) { | ||
/** @type {KeyIteratorContext} */ | ||
const ctx = { | ||
host, | ||
injector, | ||
keyExpr, | ||
body, | ||
get, | ||
rendered: obj(), | ||
block: block(injector), | ||
index: 0, | ||
updated: 1, | ||
used: null | ||
}; | ||
updateKeyIterator(ctx); | ||
return ctx; | ||
} | ||
/** | ||
* Updates iterator block defined in `ctx` | ||
* @param {KeyIteratorContext} ctx | ||
* @returns {number} Returns `1` if iterator was updated, `0` otherwise | ||
*/ | ||
function updateKeyIterator(ctx) { | ||
run(ctx.injector, ctx.block, keyIteratorHost, ctx.host, ctx); | ||
return ctx.updated; | ||
} | ||
/** | ||
* | ||
* @param {Component} host | ||
* @param {Injector} injector | ||
* @param {KeyIteratorContext} ctx | ||
*/ | ||
function keyIteratorHost(host, injector, ctx) { | ||
ctx.used = obj(); | ||
ctx.index = 0; | ||
ctx.updated = 1; | ||
const collection = ctx.get(host, getScope(host)); | ||
if (collection && typeof collection.forEach === 'function') { | ||
collection.forEach(iterator$1, ctx); | ||
} | ||
// Remove remaining blocks | ||
for (let k in ctx.rendered) { | ||
for (let i = 0, items = ctx.rendered[k]; i < items.length; i++) { | ||
ctx.updated = 1; | ||
dispose(injector, items[i][0], true); | ||
} | ||
} | ||
ctx.rendered = ctx.used; | ||
} | ||
/** | ||
* @this {KeyIteratorContext} | ||
* @param {*} value | ||
* @param {*} key | ||
*/ | ||
function iterator$1(value, key) { | ||
const { host, injector, index, used, rendered, keyExpr, body } = this; | ||
const localScope = { index, key, value }; | ||
const id = keyExpr(value, createScope(host, localScope)); | ||
let entry = id in rendered && rendered[id].shift(); | ||
if (entry) { | ||
// Update existing block | ||
const [b, update, scope] = entry; | ||
setScope(host, assign(scope, localScope)); | ||
move(injector, b, injector.ptr); | ||
if (run(injector, b, update, host, scope)) { | ||
this.updated = 1; | ||
} | ||
exitScope(host); | ||
} else { | ||
// Create & render new block | ||
const b = block(injector); | ||
const scope = enterScope(host, localScope); | ||
const update = run(injector, b, body, host, scope); | ||
this.updated = 1; | ||
exitScope(host); | ||
entry = [b, update, scope]; | ||
} | ||
// Mark block as used. | ||
// We allow multiple items key in case of poorly prepared data. | ||
if (id in used) { | ||
used[id].push(entry); | ||
} else { | ||
used[id] = [entry]; | ||
} | ||
this.index++; | ||
} | ||
/** | ||
* Sets runtime ref (e.g. ref which will be changed over time) to given host | ||
* @param {Component} host | ||
* @param {string} name | ||
* @param {HTMLElement} elem | ||
* @returns {number} Update status. Refs must be explicitly finalized, thus | ||
* we always return `0` as nothing was changed | ||
*/ | ||
function setRef(host, name, elem) { | ||
host.componentModel.refs.cur[name] = elem; | ||
return 0; | ||
} | ||
/** | ||
* Sets static ref (e.g. ref which won’t be changed over time) to given host | ||
* @param {Component} host | ||
* @param {string} name | ||
* @param {Element} value | ||
*/ | ||
function setStaticRef(host, name, value) { | ||
value && value.setAttribute(getRefAttr(name, host), ''); | ||
host.refs[name] = value; | ||
} | ||
/** | ||
* Finalizes refs on given scope | ||
* @param {Component} host | ||
* @returns {number} Update status | ||
*/ | ||
function finalizeRefs(host) { | ||
return finalizeItems(host.componentModel.refs, changeRef, host); | ||
} | ||
/** | ||
* Invoked when element reference was changed | ||
* @param {string} name | ||
* @param {Element} prevValue | ||
* @param {Element} newValue | ||
* @param {Component} host | ||
*/ | ||
function changeRef(name, prevValue, newValue, host) { | ||
prevValue && prevValue.removeAttribute(getRefAttr(name, host)); | ||
setStaticRef(host, name, newValue); | ||
} | ||
/** | ||
* Returns attribute name to identify element in CSS | ||
* @param {String} name | ||
* @param {Component} host | ||
*/ | ||
function getRefAttr(name, host) { | ||
const cssScope$$1 = host.componentModel.definition.cssScope; | ||
return 'ref-' + name + (cssScope$$1 ? '-' + cssScope$$1 : ''); | ||
} | ||
/** | ||
* Renders code, returned from `get` function, as HTML | ||
@@ -1592,2 +1653,3 @@ * @param {Component} host | ||
block: block(injector), | ||
scope: getScope(host), | ||
get, | ||
@@ -1607,3 +1669,3 @@ code: null, | ||
function updateInnerHTML(ctx) { | ||
const { host, injector, block: block$$1 } = ctx; | ||
const { host, injector, block: block$$1, scope } = ctx; | ||
const code = ctx.get(host, injector); | ||
@@ -1615,3 +1677,3 @@ let updated = 0; | ||
ctx.code = code; | ||
dispose(injector, block$$1, false); | ||
dispose(injector, block$$1, scope, false); | ||
isDefined(code) && run(injector, block$$1, renderHTML, host, ctx); | ||
@@ -1624,2 +1686,9 @@ } | ||
/** | ||
* @param {InnerHtmlContext} ctx | ||
*/ | ||
function unmountInnerHTML(ctx) { | ||
dispose(ctx.injector, ctx.block, ctx.scope, true); | ||
} | ||
/** | ||
* @param {Component} host | ||
@@ -1664,4 +1733,2 @@ * @param {Injector} injector | ||
function mountPartial(host, injector, partial, args) { | ||
// NB freeze scope context so all partial runtime objects can be reused | ||
// across renders | ||
/** @type {PartialContext} */ | ||
@@ -1672,2 +1739,3 @@ const ctx = { | ||
block: block(injector), | ||
baseScope: getScope(host), | ||
scope: null, | ||
@@ -1689,3 +1757,3 @@ update: null, | ||
function updatePartial(ctx, partial, args) { | ||
const { host, injector, block: block$$1 } = ctx; | ||
const { host, injector, block: block$$1, baseScope } = ctx; | ||
let updated = 0; | ||
@@ -1695,6 +1763,7 @@ | ||
// Unmount previously rendered partial | ||
ctx.partial && dispose(injector, block$$1, false); | ||
ctx.partial && dispose(injector, block$$1, ctx.scope, false); | ||
// Mount new partial | ||
const scope = ctx.scope = enterScope(host, assign(obj(partial.defaults), args)); | ||
const scope = ctx.scope = assign(obj(baseScope), partial.defaults, args); | ||
setScope(host, scope); | ||
ctx.update = partial ? run(injector, block$$1, partial.body, host, scope) : null; | ||
@@ -1716,2 +1785,9 @@ ctx.partial = partial; | ||
/** | ||
* @param {PartialContext} ctx | ||
*/ | ||
function unmountPartial(ctx) { | ||
dispose(ctx.injector, ctx.block, ctx.scope, true); | ||
} | ||
const prefix = '$'; | ||
@@ -1884,3 +1960,3 @@ | ||
export { get, filter, mountBlock, updateBlock, mountIterator, updateIterator, mountKeyIterator, updateKeyIterator, createInjector, block, run, insert, move, dispose, enterScope, exitScope, createScope, setScope, getScope, getProp, getState, getVar, setVar, setAttribute, updateAttribute, updateProps, addClass, finalizeAttributes, normalizeClassName, addEvent, addStaticEvent, finalizeEvents, getEventHandler, mountSlot, updateSlots, markSlotUpdate, setRef, setStaticRef, finalizeRefs, createComponent, mountComponent, updateComponent, unmountComponent, subscribeStore, scheduleRender, renderComponent, mountInnerHTML, updateInnerHTML, elem, elemNS, elemWithText, elemNSWithText, text, updateText, mountPartial, updatePartial, Store }; | ||
export { get, filter, addDisposeCallback, mountBlock, updateBlock, unmountBlock, mountIterator, updateIterator, unmountIterator, mountKeyIterator, updateKeyIterator, unmountKeyIterator, createInjector, block, run, insert, move, dispose, disposeBlock, enterScope, exitScope, createScope, setScope, getScope, getProp, getState, getVar, setVar, setAttribute, updateAttribute, updateProps, addClass, finalizeAttributes, normalizeClassName, addEvent, addStaticEvent, finalizeEvents, getEventHandler, mountSlot, unmountSlot, updateSlots, markSlotUpdate, setRef, setStaticRef, finalizeRefs, createComponent, mountComponent, updateComponent, unmountComponent, subscribeStore, scheduleRender, renderComponent, mountInnerHTML, updateInnerHTML, unmountInnerHTML, elem, elemNS, elemWithText, elemNSWithText, text, updateText, mountPartial, updatePartial, unmountPartial, Store }; | ||
//# sourceMappingURL=runtime.es.js.map |
{ | ||
"name": "@endorphinjs/template-runtime", | ||
"version": "0.1.11", | ||
"version": "0.1.12", | ||
"description": "EndorphinJS template runtime, embedded with template bundles", | ||
@@ -5,0 +5,0 @@ "main": "./dist/runtime.cjs.js", |
@@ -7,2 +7,6 @@ import { Store } from './lib/store'; | ||
interface DisposeCallback { | ||
(scope: object): void | ||
} | ||
interface Component extends Element { | ||
@@ -136,2 +140,7 @@ /** | ||
defaultProps: object; | ||
/** | ||
* A function for disposing component contents | ||
*/ | ||
dispose?: DisposeCallback | ||
} | ||
@@ -322,2 +331,5 @@ | ||
size: number; | ||
/** A function to dispose block contents */ | ||
dispose?: DisposeCallback; | ||
} | ||
@@ -352,6 +364,10 @@ | ||
interface BlockContext { | ||
component: Component; | ||
interface BaseContext { | ||
host: Component; | ||
injector: Injector; | ||
block: Block, | ||
block: Block; | ||
scope: Object; | ||
} | ||
interface BlockContext extends BaseContext { | ||
get: Function; | ||
@@ -362,8 +378,5 @@ fn?: Function, | ||
interface IteratorContext { | ||
host: Component; | ||
injector: Injector; | ||
interface IteratorContext extends BaseContext { | ||
get: Function; | ||
body: Function; | ||
block: Block; | ||
index: number; | ||
@@ -384,6 +397,10 @@ updated: number; | ||
interface InnerHtmlContext { | ||
interface SlotContext { | ||
host: Component; | ||
injector: Injector; | ||
block: Block; | ||
name: string; | ||
isDefault: boolean; | ||
defaultContent: Function; | ||
} | ||
interface InnerHtmlContext extends BaseContext { | ||
get: Function; | ||
@@ -394,9 +411,6 @@ code?: string; | ||
interface PartialContext { | ||
host: Component; | ||
injector: Injector; | ||
block: Block, | ||
update?: Function, | ||
scope?: object, | ||
partial?: object | ||
interface PartialContext extends BaseContext { | ||
baseScope?: Object; | ||
update?: Function; | ||
partial?: Object; | ||
} | ||
@@ -403,0 +417,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
293782
3843