@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
293782
3843