@endorphinjs/template-runtime
Advanced tools
Comparing version 0.4.5 to 0.5.0
@@ -1,34 +0,93 @@ | ||
import { Injector } from './injector'; | ||
import { Component } from './component'; | ||
export interface ChangeSet { | ||
p: { | ||
[name: string]: any; | ||
}; | ||
c: { | ||
[name: string]: any; | ||
}; | ||
} | ||
export interface AttributeChangeSet extends ChangeSet { | ||
n?: { | ||
[namespace: string]: ChangeSet; | ||
}; | ||
} | ||
/** | ||
* Sets value of attribute `name` to `value` | ||
* @return Update status. Always returns `0` since actual attribute value | ||
* is defined in `finalizeAttributes()` | ||
* Creates new attribute change set | ||
*/ | ||
export declare function setAttribute(injector: Injector, name: string, value: any): number; | ||
export declare function attributeSet(): AttributeChangeSet; | ||
/** | ||
* Sets value of attribute `name` under namespace of `nsURI` to `value` | ||
* Create pending props change set | ||
*/ | ||
export declare function setAttributeNS(injector: Injector, nsURI: string, name: string, value: any): void; | ||
export declare function propsSet(elem: Component): AttributeChangeSet; | ||
/** | ||
* Updates `attrName` value in `elem`, if required | ||
* @returns New attribute value | ||
* Alias for `elem.setAttribute` | ||
*/ | ||
export declare function updateAttribute(elem: HTMLElement, attrName: string, value: any, prevValue: any): any; | ||
export declare function setAttribute(elem: Element, name: string, value: any): any; | ||
/** | ||
* Updates props in given component, if required | ||
* @return Returns `true` if value was updated | ||
* Alias for `elem.className` | ||
*/ | ||
export declare function updateProps(elem: Component, data: object): boolean; | ||
export declare function setClass(elem: Element, value: any): any; | ||
/** | ||
* Adds given class name as pending attribute | ||
* Sets attribute value as expression. Unlike regular primitive attributes, | ||
* expression values must be represented, e.g. non-primitive values must be | ||
* converted to string representations. Also, expression resolved to `false`, | ||
* `null` or `undefined` will remove attribute from element | ||
*/ | ||
export declare function addClass(injector: Injector, value: string): void; | ||
export declare function setAttributeExpression(elem: Element, name: string, value: any): any; | ||
/** | ||
* Applies pending attributes changes to injector’s host element | ||
* Updates attribute value only if it’s not equal to previous value | ||
*/ | ||
export declare function finalizeAttributes(injector: Injector): number; | ||
export declare function updateAttributeExpression<T = any>(elem: Element, name: string, value: T, prevValue?: any): T; | ||
/** | ||
* Normalizes given class value: removes duplicates and trims whitespace | ||
* Alias for `elem.setAttributeNS` | ||
*/ | ||
export declare function normalizeClassName(str: string): string; | ||
export declare function setAttributeNS(elem: Element, ns: string, name: string, value: any): any; | ||
/** | ||
* Same as `setAttributeExpression()` but for namespaced attributes | ||
*/ | ||
export declare function setAttributeExpressionNS(elem: Element, ns: string, name: string, value: any): any; | ||
/** | ||
* Adds or removes (if value is `void`) attribute to given element | ||
*/ | ||
export declare function updateAttributeExpressionNS<T = any>(elem: Element, ns: string, name: string, value: T, prevValue?: any): T; | ||
/** | ||
* Alias for `elem.classList.add()` | ||
*/ | ||
export declare function addClass(elem: HTMLElement, className: string): void; | ||
/** | ||
* Adds class to given element if given condition is truthy | ||
*/ | ||
export declare function addClassIf(elem: HTMLElement, className: string, condition: any): boolean; | ||
/** | ||
* Toggles class on given element if condition is changed | ||
*/ | ||
export declare function toggleClassIf(elem: HTMLElement, className: string, condition: any, prevResult: boolean): boolean; | ||
/** | ||
* Sets pending attribute value which will be added to attribute later | ||
*/ | ||
export declare function setPendingAttribute(data: AttributeChangeSet, name: string, value: any): void; | ||
/** | ||
* Sets pending namespaced attribute value which will be added to attribute later | ||
*/ | ||
export declare function setPendingAttributeNS(data: AttributeChangeSet, ns: string, name: string, value: any): void; | ||
/** | ||
* Adds given class name to pending attribute set | ||
*/ | ||
export declare function addPendingClass(data: AttributeChangeSet, className: string): void; | ||
/** | ||
* Adds given class name to pending attribute set if condition is truthy | ||
*/ | ||
export declare function addPendingClassIf(data: AttributeChangeSet, className: string, condition: any): void; | ||
/** | ||
* Finalizes pending attributes | ||
*/ | ||
export declare function finalizeAttributes(elem: Element, data: AttributeChangeSet): number; | ||
/** | ||
* Finalizes pending namespaced attributes | ||
*/ | ||
export declare function finalizeAttributesNS(elem: Element, data: AttributeChangeSet): number; | ||
/** | ||
* Returns normalized list of class names from given string | ||
*/ | ||
export declare function classNames(str: string): string[]; |
import { Injector } from './injector'; | ||
import { Changes, Data, UpdateTemplate, ChangeSet, MountTemplate } from './types'; | ||
import { Changes, Data, UpdateTemplate, MountTemplate } from './types'; | ||
import { Store } from './store'; | ||
interface RefMap { | ||
[key: string]: Element; | ||
export interface RefMap { | ||
[key: string]: Element | null; | ||
} | ||
@@ -52,4 +52,2 @@ export declare type ComponentEventHandler = (component: Component, event: Event, target: HTMLElement) => void; | ||
input: Injector; | ||
/** Change set for component refs */ | ||
refs: ChangeSet; | ||
/** Runtime variables */ | ||
@@ -70,4 +68,2 @@ vars: object; | ||
rendering: boolean; | ||
/** Indicates that component is currently in finalization state (calling `did*` hooks) */ | ||
finalizing: boolean; | ||
/** Default props values */ | ||
@@ -158,7 +154,7 @@ defaultProps: object; | ||
*/ | ||
export declare function mountComponent(component: Component, initialProps?: object): void; | ||
export declare function mountComponent(component: Component, props?: object): void; | ||
/** | ||
* Updates given mounted component | ||
*/ | ||
export declare function updateComponent(component: Component): number; | ||
export declare function updateComponent(component: Component, props?: object): number; | ||
/** | ||
@@ -165,0 +161,0 @@ * Destroys given component: removes static event listeners and cleans things up |
@@ -5,2 +5,6 @@ declare type TextNode = Text & { | ||
/** | ||
* Shorthand for `elem.appendChild()` for better minification | ||
*/ | ||
export declare function appendChild(element: Element, node: Node): Node; | ||
/** | ||
* Creates element with given tag name | ||
@@ -7,0 +11,0 @@ * @param cssScope Scope for CSS isolation |
import { Scope, EventBinding } from './types'; | ||
import { Injector } from './injector'; | ||
import { Component } from './component'; | ||
interface PendingEvents { | ||
target: Element; | ||
host: Component; | ||
events: { | ||
[type: string]: EventBinding | void; | ||
}; | ||
} | ||
/** | ||
@@ -8,15 +14,15 @@ * Registers given event listener on `target` element and returns event binding | ||
*/ | ||
export declare function addStaticEvent(target: Element, type: string, handleEvent: EventListener, host: Component, scope: Scope): EventBinding; | ||
export declare function addEvent(target: Element, type: string, listener: EventListener, host: Component, scope: Scope): EventBinding; | ||
/** | ||
* Unregister given event binding | ||
*/ | ||
export declare function removeStaticEvent(binding: EventBinding): void; | ||
export declare function removeEvent(type: string, binding: EventBinding): void; | ||
/** | ||
* Adds pending event `name` handler | ||
* Creates structure for collecting pending events | ||
*/ | ||
export declare function addEvent(injector: Injector, type: string, handleEvent: EventListener, host: Component, scope: Scope): void; | ||
/** | ||
* Finalizes events of given injector | ||
*/ | ||
export declare function finalizeEvents(injector: Injector): number; | ||
export declare function pendingEvents(host: Component, target: Element): PendingEvents; | ||
export declare function setPendingEvent(pending: PendingEvents, type: string, listener: EventListener, scope: Scope): void; | ||
export declare function finalizePendingEvents(pending: PendingEvents): void; | ||
export declare function detachPendingEvents(pending: PendingEvents): void; | ||
export declare function safeEventListener(host: Component, handler: EventListener): EventListener; | ||
export {}; |
import { LinkedList, LinkedListItem } from './linked-list'; | ||
import { ChangeSet, EventBinding, Scope, MountBlock, UpdateBlock } from './types'; | ||
import { Scope, MountBlock, UpdateBlock } from './types'; | ||
import { Component } from './component'; | ||
@@ -12,16 +12,6 @@ import { SlotContext } from './slot'; | ||
ptr: LinkedListItem | null; | ||
/** | ||
* Slots container | ||
*/ | ||
slots?: { | ||
/** Slots container */ | ||
slots: { | ||
[name: string]: SlotContext; | ||
} | null; | ||
/** Pending attributes updates */ | ||
attributes: ChangeSet; | ||
/** Pending namespace updates */ | ||
attributesNS?: { | ||
[uri: string]: ChangeSet; | ||
}; | ||
/** Current event handlers */ | ||
events: ChangeSet<EventBinding>; | ||
} | ||
@@ -28,0 +18,0 @@ export interface Block { |
@@ -12,2 +12,6 @@ import { Injector, Block } from './injector'; | ||
} | ||
interface PartialsMap { | ||
[name: string]: PartialDefinition; | ||
} | ||
export declare function getPartial(host: Component, name: string, componentPartials: PartialsMap): PartialDefinition | undefined; | ||
/** | ||
@@ -14,0 +18,0 @@ * Mounts given partial into injector context |
@@ -1,16 +0,11 @@ | ||
import { Component } from './component'; | ||
import { Component, RefMap } from './component'; | ||
/** | ||
* Sets runtime ref (e.g. ref which will be changed over time) to given host | ||
* @returns Update status. Refs must be explicitly finalized, thus | ||
* we always return `0` as nothing was changed | ||
* Adds given element as a named ref | ||
*/ | ||
export declare function setRef(host: Component, name: string, elem: HTMLElement): number; | ||
export declare function setRef(host: Component, key: string, elem: Element): void; | ||
/** | ||
* Sets static ref (e.g. ref which won’t be changed over time) to given host | ||
* Removes ref for given key | ||
*/ | ||
export declare function setStaticRef(host: Component, name: string, value: Element): void; | ||
/** | ||
* Finalizes refs on given scope | ||
* @returns Update status | ||
*/ | ||
export declare function finalizeRefs(host: Component): number; | ||
export declare function removeRef(host: Component, key: string): void; | ||
export declare function setPendingRef(pending: RefMap, key: string | void, elem: Element | null): void; | ||
export declare function finalizePendingRefs(host: Component, pending: RefMap): void; |
@@ -6,2 +6,8 @@ 'use strict'; | ||
/** | ||
* Shorthand for `elem.appendChild()` for better minification | ||
*/ | ||
function appendChild(element, node) { | ||
return element.appendChild(node); | ||
} | ||
/** | ||
* Creates element with given tag name | ||
@@ -104,4 +110,2 @@ * @param cssScope Scope for CSS isolation | ||
* Check if given value id defined, e.g. not `null`, `undefined` or `NaN` | ||
* @param {*} value | ||
* @returns {boolean} | ||
*/ | ||
@@ -112,29 +116,2 @@ function isDefined(value) { | ||
/** | ||
* Finalizes updated items, defined in `items.prev` and `items.cur` | ||
* @param {object} items | ||
* @param {function} change | ||
* @param {*} [ctx] | ||
* @returns {number} Returns `1` if data was updated, `0` otherwise | ||
*/ | ||
function finalizeItems(items, change, ctx) { | ||
let updated = 0; | ||
const { cur, prev } = items; | ||
for (const name in cur) { | ||
const curValue = cur[name]; | ||
const prevValue = prev[name]; | ||
if (curValue !== prevValue) { | ||
updated = 1; | ||
change(name, prevValue, prev[name] = curValue, ctx); | ||
} | ||
cur[name] = null; | ||
} | ||
return updated; | ||
} | ||
/** | ||
* Creates object for storing change sets, e.g. current and previous values | ||
*/ | ||
function changeSet() { | ||
return { prev: obj(), cur: obj() }; | ||
} | ||
/** | ||
* Returns properties from `next` which were changed since `prev` state. | ||
@@ -228,7 +205,12 @@ * Returns `null` if there are no changes | ||
function runtimeError(host, error) { | ||
host.dispatchEvent(new CustomEvent('runtime-error', { | ||
bubbles: true, | ||
cancelable: true, | ||
detail: { error, host } | ||
})); | ||
if (typeof CustomEvent !== 'undefined') { | ||
host.dispatchEvent(new CustomEvent('runtime-error', { | ||
bubbles: true, | ||
cancelable: true, | ||
detail: { error, host } | ||
})); | ||
} | ||
else { | ||
throw error; | ||
} | ||
} | ||
@@ -240,5 +222,4 @@ | ||
*/ | ||
function addStaticEvent(target, type, handleEvent, host, scope) { | ||
handleEvent = safeEventListener(host, handleEvent); | ||
return registerBinding({ host, scope, type, handleEvent, target }); | ||
function addEvent(target, type, listener, host, scope) { | ||
return registerBinding(type, { host, scope, target, listener, handleEvent }); | ||
} | ||
@@ -248,29 +229,55 @@ /** | ||
*/ | ||
function removeStaticEvent(binding) { | ||
binding.target.removeEventListener(binding.type, binding); | ||
function removeEvent(type, binding) { | ||
binding.target.removeEventListener(type, binding); | ||
} | ||
/** | ||
* Adds pending event `name` handler | ||
* Creates structure for collecting pending events | ||
*/ | ||
function addEvent(injector, type, handleEvent, host, scope) { | ||
// We’ll use `ChangeSet` to bind and unbind events only: once binding is registered, | ||
// we will mutate binding props | ||
const { prev, cur } = injector.events; | ||
const binding = cur[type] || prev[type]; | ||
handleEvent = safeEventListener(host, handleEvent); | ||
function pendingEvents(host, target) { | ||
return { host, target, events: obj() }; | ||
} | ||
function setPendingEvent(pending, type, listener, scope) { | ||
let binding = pending.events[type]; | ||
if (binding) { | ||
binding.listener = listener; | ||
binding.scope = scope; | ||
binding.handleEvent = handleEvent; | ||
cur[type] = binding; | ||
} | ||
else { | ||
cur[type] = { host, scope, type, handleEvent, target: injector.parentNode }; | ||
binding = pending.events[type] = addEvent(pending.target, type, listener, pending.host, scope); | ||
} | ||
binding.pending = listener; | ||
} | ||
/** | ||
* Finalizes events of given injector | ||
*/ | ||
function finalizeEvents(injector) { | ||
return finalizeItems(injector.events, changeEvent, injector.parentNode); | ||
function finalizePendingEvents(pending) { | ||
// For event listeners, we should only bind or unbind events, depending | ||
// on current listener value | ||
const { events } = pending; | ||
for (const type in events) { | ||
const binding = events[type]; | ||
if (binding) { | ||
if (!binding.pending) { | ||
events[type] = removeEvent(type, binding); | ||
} | ||
binding.pending = void 0; | ||
} | ||
} | ||
} | ||
function detachPendingEvents(pending) { | ||
const { events } = pending; | ||
for (const type in events) { | ||
const binding = events[type]; | ||
if (binding) { | ||
removeEvent(type, binding); | ||
} | ||
} | ||
} | ||
function handleEvent(event) { | ||
try { | ||
this.listener && this.listener(event); | ||
} | ||
catch (error) { | ||
runtimeError(this.host, error); | ||
// tslint:disable-next-line:no-console | ||
console.error(error); | ||
} | ||
} | ||
function safeEventListener(host, handler) { | ||
@@ -289,97 +296,157 @@ // tslint:disable-next-line:only-arrow-functions | ||
} | ||
function registerBinding(binding) { | ||
binding.target.addEventListener(binding.type, binding); | ||
function registerBinding(type, binding) { | ||
binding.target.addEventListener(type, binding); | ||
return binding; | ||
} | ||
/** | ||
* Invoked when event handler was changed | ||
* Creates new attribute change set | ||
*/ | ||
function changeEvent(name, prevValue, newValue) { | ||
if (!prevValue && newValue) { | ||
// Should register new binding | ||
registerBinding(newValue); | ||
} | ||
else if (prevValue && !newValue) { | ||
removeStaticEvent(prevValue); | ||
} | ||
function attributeSet() { | ||
return { c: obj(), p: obj() }; | ||
} | ||
/** | ||
* Sets value of attribute `name` to `value` | ||
* @return Update status. Always returns `0` since actual attribute value | ||
* is defined in `finalizeAttributes()` | ||
* Create pending props change set | ||
*/ | ||
function setAttribute(injector, name, value) { | ||
injector.attributes.cur[name] = value; | ||
return 0; | ||
function propsSet(elem) { | ||
const props = assign(obj(), elem.componentModel.defaultProps); | ||
// NB in components, pending `c` props are tested against actual `.props`, | ||
// the `p` property is not used. To keep up with the same hidden JS class, | ||
// create `p` property as well and point it to `c` to reduce object allocations | ||
return { c: props, p: props }; | ||
} | ||
/** | ||
* Sets value of attribute `name` under namespace of `nsURI` to `value` | ||
* Alias for `elem.setAttribute` | ||
*/ | ||
function setAttributeNS(injector, nsURI, name, value) { | ||
if (!injector.attributesNS) { | ||
injector.attributesNS = obj(); | ||
} | ||
const { attributesNS } = injector; | ||
if (!attributesNS[nsURI]) { | ||
attributesNS[nsURI] = changeSet(); | ||
} | ||
attributesNS[nsURI].cur[name] = value; | ||
function setAttribute(elem, name, value) { | ||
elem.setAttribute(name, value); | ||
return value; | ||
} | ||
/** | ||
* Updates `attrName` value in `elem`, if required | ||
* @returns New attribute value | ||
* Alias for `elem.className` | ||
*/ | ||
function updateAttribute(elem, attrName, value, prevValue) { | ||
if (value !== prevValue) { | ||
changeAttribute(attrName, prevValue, value, elem); | ||
return value; | ||
function setClass(elem, value) { | ||
elem.className = value; | ||
return value; | ||
} | ||
/** | ||
* Sets attribute value as expression. Unlike regular primitive attributes, | ||
* expression values must be represented, e.g. non-primitive values must be | ||
* converted to string representations. Also, expression resolved to `false`, | ||
* `null` or `undefined` will remove attribute from element | ||
*/ | ||
function setAttributeExpression(elem, name, value) { | ||
const primitive = representedValue(value); | ||
primitive === null | ||
? elem.removeAttribute(name) | ||
: setAttribute(elem, name, primitive); | ||
return value; | ||
} | ||
/** | ||
* Updates attribute value only if it’s not equal to previous value | ||
*/ | ||
function updateAttributeExpression(elem, name, value, prevValue) { | ||
return prevValue !== value | ||
? setAttributeExpression(elem, name, value) | ||
: value; | ||
} | ||
/** | ||
* Alias for `elem.setAttributeNS` | ||
*/ | ||
function setAttributeNS(elem, ns, name, value) { | ||
elem.setAttributeNS(ns, name, value); | ||
return value; | ||
} | ||
/** | ||
* Same as `setAttributeExpression()` but for namespaced attributes | ||
*/ | ||
function setAttributeExpressionNS(elem, ns, name, value) { | ||
const primitive = representedValue(value); | ||
primitive === null | ||
? elem.removeAttributeNS(ns, name) | ||
: setAttributeNS(elem, ns, name, primitive); | ||
return value; | ||
} | ||
/** | ||
* Adds or removes (if value is `void`) attribute to given element | ||
*/ | ||
function updateAttributeExpressionNS(elem, ns, name, value, prevValue) { | ||
return prevValue !== value | ||
? setAttributeExpressionNS(elem, ns, name, value) | ||
: value; | ||
} | ||
/** | ||
* Alias for `elem.classList.add()` | ||
*/ | ||
function addClass(elem, className) { | ||
elem.classList.add(className); | ||
} | ||
/** | ||
* Adds class to given element if given condition is truthy | ||
*/ | ||
function addClassIf(elem, className, condition) { | ||
condition && addClass(elem, className); | ||
return condition; | ||
} | ||
/** | ||
* Toggles class on given element if condition is changed | ||
*/ | ||
function toggleClassIf(elem, className, condition, prevResult) { | ||
if (prevResult !== condition) { | ||
condition ? addClass(elem, className) : elem.classList.remove(className); | ||
} | ||
return prevValue; | ||
return condition; | ||
} | ||
/** | ||
* Updates props in given component, if required | ||
* @return Returns `true` if value was updated | ||
* Sets pending attribute value which will be added to attribute later | ||
*/ | ||
function updateProps(elem, data) { | ||
const { props } = elem; | ||
let updated; | ||
for (const p in data) { | ||
if (data.hasOwnProperty(p) && props[p] !== data[p]) { | ||
if (!updated) { | ||
updated = obj(); | ||
} | ||
updated[p] = data[p]; | ||
} | ||
function setPendingAttribute(data, name, value) { | ||
data.c[name] = value; | ||
} | ||
/** | ||
* Sets pending namespaced attribute value which will be added to attribute later | ||
*/ | ||
function setPendingAttributeNS(data, ns, name, value) { | ||
if (!data.n) { | ||
data.n = obj(); | ||
} | ||
if (updated) { | ||
elem.setProps(data); | ||
return true; | ||
if (!data.n[ns]) { | ||
data.n[ns] = attributeSet(); | ||
} | ||
return false; | ||
data.n[ns].c[name] = value; | ||
} | ||
/** | ||
* Adds given class name as pending attribute | ||
* Adds given class name to pending attribute set | ||
*/ | ||
function addClass(injector, value) { | ||
if (isDefined(value)) { | ||
const className = injector.attributes.cur.class; | ||
setAttribute(injector, 'class', isDefined(className) ? className + ' ' + value : value); | ||
function addPendingClass(data, className) { | ||
if (className != null) { | ||
const prev = data.c.class; | ||
data.c.class = prev ? prev + ' ' + className : String(className); | ||
} | ||
} | ||
/** | ||
* Applies pending attributes changes to injector’s host element | ||
* Adds given class name to pending attribute set if condition is truthy | ||
*/ | ||
function finalizeAttributes(injector) { | ||
const { attributes, attributesNS } = injector; | ||
if (isDefined(attributes.cur.class)) { | ||
attributes.cur.class = normalizeClassName(attributes.cur.class); | ||
} | ||
let updated = finalizeItems(attributes, changeAttribute, injector.parentNode); | ||
if (attributesNS) { | ||
const ctx = { node: injector.parentNode, ns: null }; | ||
for (const ns in attributesNS) { | ||
ctx.ns = ns; | ||
updated |= finalizeItems(attributesNS[ns], changeAttributeNS, ctx); | ||
function addPendingClassIf(data, className, condition) { | ||
condition && addPendingClass(data, className); | ||
} | ||
/** | ||
* Finalizes pending attributes | ||
*/ | ||
function finalizeAttributes(elem, data) { | ||
let updated = 0; | ||
const { c, p } = data; | ||
for (const name in c) { | ||
const curValue = c[name]; | ||
if (curValue !== p[name]) { | ||
updated = 1; | ||
if (name === 'class') { | ||
elem.className = classNames(curValue).join(' '); | ||
} | ||
else { | ||
setAttributeExpression(elem, name, curValue); | ||
} | ||
p[name] = curValue; | ||
} | ||
c[name] = null; | ||
} | ||
@@ -389,41 +456,62 @@ return updated; | ||
/** | ||
* Normalizes given class value: removes duplicates and trims whitespace | ||
* Finalizes pending namespaced attributes | ||
*/ | ||
function normalizeClassName(str) { | ||
const out = []; | ||
const parts = String(str).split(/\s+/); | ||
for (let i = 0, cl; i < parts.length; i++) { | ||
cl = parts[i]; | ||
if (cl && out.indexOf(cl) === -1) { | ||
out.push(cl); | ||
function finalizeAttributesNS(elem, data) { | ||
// NB use it as a separate function to use explicitly inside generated content. | ||
// It there’s no pending namespace attributes, this method will not be included | ||
// into final bundle | ||
if (!data.n) { | ||
return 0; | ||
} | ||
let updated = 0; | ||
for (const ns in data.n) { | ||
const { c, p } = data.n[ns]; | ||
for (const name in c) { | ||
const curValue = c[name]; | ||
if (curValue !== p[name]) { | ||
updated = 1; | ||
setAttributeExpressionNS(elem, ns, name, curValue); | ||
p[name] = curValue; | ||
} | ||
c[name] = null; | ||
} | ||
} | ||
return out.join(' '); | ||
return updated; | ||
} | ||
/** | ||
* Callback for changing attribute value | ||
* Returns normalized list of class names from given string | ||
*/ | ||
function changeAttribute(name, prevValue, newValue, elem) { | ||
if (isDefined(newValue)) { | ||
if (name === 'class') { | ||
elem.className = normalizeClassName(newValue); | ||
function classNames(str) { | ||
const out = []; | ||
if (isDefined(str)) { | ||
const parts = String(str).split(/\s+/); | ||
for (let i = 0, cl; i < parts.length; i++) { | ||
cl = parts[i]; | ||
if (cl && out.indexOf(cl) === -1) { | ||
out.push(cl); | ||
} | ||
} | ||
else { | ||
representAttributeValue(elem, name, newValue); | ||
} | ||
} | ||
else if (isDefined(prevValue)) { | ||
elem.removeAttribute(name); | ||
} | ||
return out; | ||
} | ||
/** | ||
* Callback for changing attribute value | ||
* Returns represented attribute value for given data | ||
*/ | ||
function changeAttributeNS(name, prevValue, newValue, ctx) { | ||
if (isDefined(newValue)) { | ||
ctx.node.setAttributeNS(ctx.ns, name, newValue); | ||
function representedValue(value) { | ||
if (value === false || !isDefined(value)) { | ||
return null; | ||
} | ||
else if (isDefined(prevValue)) { | ||
ctx.node.removeAttributeNS(ctx.ns, name); | ||
if (value === true) { | ||
return ''; | ||
} | ||
if (Array.isArray(value)) { | ||
return '[]'; | ||
} | ||
if (typeof value === 'function') { | ||
return '𝑓'; | ||
} | ||
if (typeof value === 'object') { | ||
return '{}'; | ||
} | ||
return value; | ||
} | ||
@@ -513,6 +601,3 @@ | ||
// to reduce runtime checks and keep functions in monomorphic state | ||
slots: null, | ||
attributes: changeSet(), | ||
attributesNS: void 0, | ||
events: changeSet() | ||
slots: null | ||
}; | ||
@@ -804,3 +889,3 @@ } | ||
if (block.update) { | ||
block.update(block.host, block.injector, block.scope); | ||
block.update(block.host, block.scope); | ||
} | ||
@@ -871,4 +956,4 @@ } | ||
const { props, state, extend, events } = prepare(element, definition); | ||
element.refs = {}; | ||
element.props = obj(props); | ||
element.refs = obj(); | ||
element.props = obj(); | ||
element.state = state; | ||
@@ -894,6 +979,4 @@ element.componentView = element; // XXX Should point to Shadow Root in Web Components | ||
vars: obj(), | ||
refs: changeSet(), | ||
mounted: false, | ||
rendering: false, | ||
finalizing: false, | ||
update: void 0, | ||
@@ -910,15 +993,7 @@ queued: false, | ||
*/ | ||
function mountComponent(component, initialProps) { | ||
function mountComponent(component, props) { | ||
const { componentModel } = component; | ||
const { input, definition, defaultProps } = componentModel; | ||
let changes = setPropsInternal(component, obj(), assign(obj(defaultProps), initialProps)); | ||
const runtimeChanges = setPropsInternal(component, input.attributes.prev, input.attributes.cur); | ||
if (changes && runtimeChanges) { | ||
assign(changes, runtimeChanges); | ||
} | ||
else if (runtimeChanges) { | ||
changes = runtimeChanges; | ||
} | ||
const { input, definition } = componentModel; | ||
const changes = setPropsInternal(component, props || componentModel.defaultProps); | ||
const arg = changes || {}; | ||
finalizeEvents(input); | ||
componentModel.rendering = true; | ||
@@ -937,6 +1012,4 @@ // Notify slot status | ||
componentModel.rendering = false; | ||
componentModel.finalizing = true; | ||
runHook(component, 'didRender', arg); | ||
runHook(component, 'didMount', arg); | ||
componentModel.finalizing = false; | ||
} | ||
@@ -946,6 +1019,4 @@ /** | ||
*/ | ||
function updateComponent(component) { | ||
const { input } = component.componentModel; | ||
const changes = setPropsInternal(component, input.attributes.prev, input.attributes.cur); | ||
finalizeEvents(input); | ||
function updateComponent(component, props) { | ||
const changes = props && setPropsInternal(component, props); | ||
if (changes || component.componentModel.queued) { | ||
@@ -964,3 +1035,2 @@ renderNext(component, changes); | ||
const { definition, events } = componentModel; | ||
const scope = getScope(component); | ||
runHook(component, 'willUnmount'); | ||
@@ -975,3 +1045,3 @@ componentModel.mounted = false; | ||
const dispose = definition.default && definition.default.dispose; | ||
captureError(component, dispose, scope); | ||
captureError(component, dispose, getScope(component)); | ||
runHook(component, 'didUnmount'); | ||
@@ -1031,6 +1101,4 @@ // @ts-ignore: Nulling disposed object | ||
componentModel.rendering = false; | ||
componentModel.finalizing = true; | ||
runHook(component, 'didRender', arg); | ||
runHook(component, 'didUpdate', arg); | ||
componentModel.finalizing = false; | ||
} | ||
@@ -1049,19 +1117,20 @@ /** | ||
} | ||
function setPropsInternal(component, prevProps, nextProps) { | ||
const changes = {}; | ||
let didChanged = false; | ||
function setPropsInternal(component, nextProps) { | ||
let changes; | ||
const { props } = component; | ||
const { defaultProps } = component.componentModel; | ||
for (const p in nextProps) { | ||
const prev = prevProps[p]; | ||
const prev = props[p]; | ||
let current = nextProps[p]; | ||
if (current == null) { | ||
current = defaultProps[p]; | ||
nextProps[p] = current = defaultProps[p]; | ||
} | ||
if (p === 'class' && current != null) { | ||
current = normalizeClassName(current); | ||
current = classNames(current).join(' '); | ||
} | ||
if (current !== prev) { | ||
didChanged = true; | ||
props[p] = prevProps[p] = current; | ||
if (!changes) { | ||
changes = obj(); | ||
} | ||
props[p] = current; | ||
changes[p] = { current, prev }; | ||
@@ -1072,5 +1141,4 @@ if (!/^partial:/.test(p)) { | ||
} | ||
nextProps[p] = null; | ||
} | ||
return didChanged ? changes : null; | ||
return changes; | ||
} | ||
@@ -1155,4 +1223,5 @@ /** | ||
if (value != null && componentModel && componentModel.mounted) { | ||
const changes = setPropsInternal(element, element.props, obj(value)); | ||
const changes = setPropsInternal(element, assign(obj(), value)); | ||
changes && renderNext(element, changes); | ||
return changes; | ||
} | ||
@@ -1223,3 +1292,3 @@ }; | ||
// Update rendered result | ||
updated = block.update(host, injector, scope) ? 1 : 0; | ||
updated = block.update(host, scope) ? 1 : 0; | ||
} | ||
@@ -1299,3 +1368,3 @@ block.injector.ptr = block.end; | ||
setScope(host, scope); | ||
if (rendered.update(host, injector, scope)) { | ||
if (rendered.update(host, scope)) { | ||
this.updated = 1; | ||
@@ -1362,3 +1431,3 @@ } | ||
const prevScope = getScope(host); | ||
collection.forEach(iterator$1, block); | ||
collection.forEach(keyIterator, block); | ||
setScope(host, prevScope); | ||
@@ -1384,3 +1453,3 @@ } | ||
} | ||
function iterator$1(value, key) { | ||
function keyIterator(value, key) { | ||
const { injector, index, rendered } = this; | ||
@@ -1422,6 +1491,6 @@ const id = this.keyExpr(value, prepareScope(this.scope, index, key, value)); | ||
if (entry.update) { | ||
const { host, injector } = entry; | ||
const { host } = entry; | ||
const scope = prepareScope(entry.scope, index, key, value); | ||
setScope(host, scope); | ||
if (entry.update(host, injector, scope)) { | ||
if (entry.update(host, scope)) { | ||
return 1; | ||
@@ -1476,30 +1545,33 @@ } | ||
/** | ||
* Sets runtime ref (e.g. ref which will be changed over time) to given host | ||
* @returns Update status. Refs must be explicitly finalized, thus | ||
* we always return `0` as nothing was changed | ||
* Adds given element as a named ref | ||
*/ | ||
function setRef(host, name, elem) { | ||
host.componentModel.refs.cur[name] = elem; | ||
return 0; | ||
function setRef(host, key, elem) { | ||
elem.setAttribute(getRefAttr(key, host), ''); | ||
host.refs[key] = elem; | ||
} | ||
/** | ||
* Sets static ref (e.g. ref which won’t be changed over time) to given host | ||
* Removes ref for given key | ||
*/ | ||
function setStaticRef(host, name, value) { | ||
value && value.setAttribute(getRefAttr(name, host), ''); | ||
host.refs[name] = value; | ||
function removeRef(host, key) { | ||
const elem = host.refs[key]; | ||
if (elem) { | ||
elem.removeAttribute(getRefAttr(key, host)); | ||
host.refs[key] = null; | ||
} | ||
} | ||
/** | ||
* Finalizes refs on given scope | ||
* @returns Update status | ||
*/ | ||
function finalizeRefs(host) { | ||
return finalizeItems(host.componentModel.refs, changeRef, host); | ||
function setPendingRef(pending, key, elem) { | ||
if (key && elem) { | ||
pending[key] = elem; | ||
} | ||
} | ||
/** | ||
* Invoked when element reference was changed | ||
*/ | ||
function changeRef(name, prevValue, newValue, host) { | ||
prevValue && prevValue.removeAttribute(getRefAttr(name, host)); | ||
setStaticRef(host, name, newValue); | ||
function finalizePendingRefs(host, pending) { | ||
for (const key in pending) { | ||
const prev = host.refs[key]; | ||
const next = pending[key]; | ||
if (prev !== next) { | ||
prev && removeRef(host, key); | ||
next && setRef(host, key, next); | ||
} | ||
pending[key] = null; | ||
} | ||
} | ||
@@ -1581,2 +1653,5 @@ /** | ||
function getPartial(host, name, componentPartials) { | ||
return host.props['partial:' + name] || componentPartials[name]; | ||
} | ||
/** | ||
@@ -1624,3 +1699,3 @@ * Mounts given partial into injector context | ||
const scope = setScope(host, assign(block.scope, args)); | ||
if (block.update(host, injector, scope)) { | ||
if (block.update(host, scope)) { | ||
updated = 1; | ||
@@ -1786,2 +1861,13 @@ } | ||
elem.style.animation = animation; | ||
// In case if callback is provided, we have to ensure that animation is actually applied. | ||
// In some testing environments, animations could be disabled via | ||
// `* { animation: none !important; }`. In this case, we should complete animation ASAP. | ||
if (callback) { | ||
nextTick(() => { | ||
const style = window.getComputedStyle(elem, null); | ||
if (!style.animationName || style.animationName === 'none') { | ||
stopAnimation(elem); | ||
} | ||
}); | ||
} | ||
} | ||
@@ -1918,4 +2004,2 @@ /** | ||
* Concatenates two strings with optional separator | ||
* @param {string} name | ||
* @param {string} suffix | ||
*/ | ||
@@ -1926,2 +2010,10 @@ function concat(name, suffix) { | ||
} | ||
function nextTick(fn) { | ||
if (typeof Promise !== 'undefined') { | ||
Promise.resolve().then(fn); | ||
} | ||
else { | ||
requestAnimationFrame(fn); | ||
} | ||
} | ||
@@ -2014,7 +2106,12 @@ /** | ||
exports.addClass = addClass; | ||
exports.addClassIf = addClassIf; | ||
exports.addEvent = addEvent; | ||
exports.addStaticEvent = addStaticEvent; | ||
exports.addPendingClass = addPendingClass; | ||
exports.addPendingClassIf = addPendingClassIf; | ||
exports.animate = animate; | ||
exports.appendChild = appendChild; | ||
exports.assign = assign; | ||
exports.attributeSet = attributeSet; | ||
exports.call = call; | ||
exports.classNames = classNames; | ||
exports.composeTween = composeTween; | ||
@@ -2028,2 +2125,3 @@ exports.createAnimation = createAnimation; | ||
exports.default = endorphin; | ||
exports.detachPendingEvents = detachPendingEvents; | ||
exports.disposeBlock = disposeBlock; | ||
@@ -2041,6 +2139,8 @@ exports.domInsert = domInsert; | ||
exports.finalizeAttributes = finalizeAttributes; | ||
exports.finalizeEvents = finalizeEvents; | ||
exports.finalizeRefs = finalizeRefs; | ||
exports.finalizeAttributesNS = finalizeAttributesNS; | ||
exports.finalizePendingEvents = finalizePendingEvents; | ||
exports.finalizePendingRefs = finalizePendingRefs; | ||
exports.find = find; | ||
exports.get = get; | ||
exports.getPartial = getPartial; | ||
exports.getProp = getProp; | ||
@@ -2062,6 +2162,9 @@ exports.getScope = getScope; | ||
exports.move = move; | ||
exports.normalizeClassName = normalizeClassName; | ||
exports.notifySlotUpdate = notifySlotUpdate; | ||
exports.obj = obj; | ||
exports.pendingEvents = pendingEvents; | ||
exports.prepareScope = prepareScope; | ||
exports.removeStaticEvent = removeStaticEvent; | ||
exports.propsSet = propsSet; | ||
exports.removeEvent = removeEvent; | ||
exports.removeRef = removeRef; | ||
exports.renderComponent = renderComponent; | ||
@@ -2071,6 +2174,12 @@ exports.safeEventListener = safeEventListener; | ||
exports.setAttribute = setAttribute; | ||
exports.setAttributeExpression = setAttributeExpression; | ||
exports.setAttributeExpressionNS = setAttributeExpressionNS; | ||
exports.setAttributeNS = setAttributeNS; | ||
exports.setClass = setClass; | ||
exports.setPendingAttribute = setPendingAttribute; | ||
exports.setPendingAttributeNS = setPendingAttributeNS; | ||
exports.setPendingEvent = setPendingEvent; | ||
exports.setPendingRef = setPendingRef; | ||
exports.setRef = setRef; | ||
exports.setScope = setScope; | ||
exports.setStaticRef = setStaticRef; | ||
exports.setVar = setVar; | ||
@@ -2080,2 +2189,3 @@ exports.stopAnimation = stopAnimation; | ||
exports.text = text; | ||
exports.toggleClassIf = toggleClassIf; | ||
exports.tweenAnimate = tweenAnimate; | ||
@@ -2089,3 +2199,4 @@ exports.unmountBlock = unmountBlock; | ||
exports.unmountSlot = unmountSlot; | ||
exports.updateAttribute = updateAttribute; | ||
exports.updateAttributeExpression = updateAttributeExpression; | ||
exports.updateAttributeExpressionNS = updateAttributeExpressionNS; | ||
exports.updateBlock = updateBlock; | ||
@@ -2099,4 +2210,3 @@ exports.updateComponent = updateComponent; | ||
exports.updatePartial = updatePartial; | ||
exports.updateProps = updateProps; | ||
exports.updateText = updateText; | ||
//# sourceMappingURL=runtime.cjs.js.map |
@@ -18,3 +18,3 @@ import { Component, ComponentDefinition } from './component'; | ||
export * from './animation'; | ||
export { assign } from './utils'; | ||
export { assign, obj } from './utils'; | ||
declare type FilterCallback<T> = (value: T, key: string | number) => boolean; | ||
@@ -21,0 +21,0 @@ interface ComponentOptions { |
/** | ||
* Shorthand for `elem.appendChild()` for better minification | ||
*/ | ||
function appendChild(element, node) { | ||
return element.appendChild(node); | ||
} | ||
/** | ||
* Creates element with given tag name | ||
@@ -99,4 +105,2 @@ * @param cssScope Scope for CSS isolation | ||
* Check if given value id defined, e.g. not `null`, `undefined` or `NaN` | ||
* @param {*} value | ||
* @returns {boolean} | ||
*/ | ||
@@ -107,29 +111,2 @@ function isDefined(value) { | ||
/** | ||
* Finalizes updated items, defined in `items.prev` and `items.cur` | ||
* @param {object} items | ||
* @param {function} change | ||
* @param {*} [ctx] | ||
* @returns {number} Returns `1` if data was updated, `0` otherwise | ||
*/ | ||
function finalizeItems(items, change, ctx) { | ||
let updated = 0; | ||
const { cur, prev } = items; | ||
for (const name in cur) { | ||
const curValue = cur[name]; | ||
const prevValue = prev[name]; | ||
if (curValue !== prevValue) { | ||
updated = 1; | ||
change(name, prevValue, prev[name] = curValue, ctx); | ||
} | ||
cur[name] = null; | ||
} | ||
return updated; | ||
} | ||
/** | ||
* Creates object for storing change sets, e.g. current and previous values | ||
*/ | ||
function changeSet() { | ||
return { prev: obj(), cur: obj() }; | ||
} | ||
/** | ||
* Returns properties from `next` which were changed since `prev` state. | ||
@@ -223,7 +200,12 @@ * Returns `null` if there are no changes | ||
function runtimeError(host, error) { | ||
host.dispatchEvent(new CustomEvent('runtime-error', { | ||
bubbles: true, | ||
cancelable: true, | ||
detail: { error, host } | ||
})); | ||
if (typeof CustomEvent !== 'undefined') { | ||
host.dispatchEvent(new CustomEvent('runtime-error', { | ||
bubbles: true, | ||
cancelable: true, | ||
detail: { error, host } | ||
})); | ||
} | ||
else { | ||
throw error; | ||
} | ||
} | ||
@@ -235,5 +217,4 @@ | ||
*/ | ||
function addStaticEvent(target, type, handleEvent, host, scope) { | ||
handleEvent = safeEventListener(host, handleEvent); | ||
return registerBinding({ host, scope, type, handleEvent, target }); | ||
function addEvent(target, type, listener, host, scope) { | ||
return registerBinding(type, { host, scope, target, listener, handleEvent }); | ||
} | ||
@@ -243,29 +224,55 @@ /** | ||
*/ | ||
function removeStaticEvent(binding) { | ||
binding.target.removeEventListener(binding.type, binding); | ||
function removeEvent(type, binding) { | ||
binding.target.removeEventListener(type, binding); | ||
} | ||
/** | ||
* Adds pending event `name` handler | ||
* Creates structure for collecting pending events | ||
*/ | ||
function addEvent(injector, type, handleEvent, host, scope) { | ||
// We’ll use `ChangeSet` to bind and unbind events only: once binding is registered, | ||
// we will mutate binding props | ||
const { prev, cur } = injector.events; | ||
const binding = cur[type] || prev[type]; | ||
handleEvent = safeEventListener(host, handleEvent); | ||
function pendingEvents(host, target) { | ||
return { host, target, events: obj() }; | ||
} | ||
function setPendingEvent(pending, type, listener, scope) { | ||
let binding = pending.events[type]; | ||
if (binding) { | ||
binding.listener = listener; | ||
binding.scope = scope; | ||
binding.handleEvent = handleEvent; | ||
cur[type] = binding; | ||
} | ||
else { | ||
cur[type] = { host, scope, type, handleEvent, target: injector.parentNode }; | ||
binding = pending.events[type] = addEvent(pending.target, type, listener, pending.host, scope); | ||
} | ||
binding.pending = listener; | ||
} | ||
/** | ||
* Finalizes events of given injector | ||
*/ | ||
function finalizeEvents(injector) { | ||
return finalizeItems(injector.events, changeEvent, injector.parentNode); | ||
function finalizePendingEvents(pending) { | ||
// For event listeners, we should only bind or unbind events, depending | ||
// on current listener value | ||
const { events } = pending; | ||
for (const type in events) { | ||
const binding = events[type]; | ||
if (binding) { | ||
if (!binding.pending) { | ||
events[type] = removeEvent(type, binding); | ||
} | ||
binding.pending = void 0; | ||
} | ||
} | ||
} | ||
function detachPendingEvents(pending) { | ||
const { events } = pending; | ||
for (const type in events) { | ||
const binding = events[type]; | ||
if (binding) { | ||
removeEvent(type, binding); | ||
} | ||
} | ||
} | ||
function handleEvent(event) { | ||
try { | ||
this.listener && this.listener(event); | ||
} | ||
catch (error) { | ||
runtimeError(this.host, error); | ||
// tslint:disable-next-line:no-console | ||
console.error(error); | ||
} | ||
} | ||
function safeEventListener(host, handler) { | ||
@@ -284,97 +291,157 @@ // tslint:disable-next-line:only-arrow-functions | ||
} | ||
function registerBinding(binding) { | ||
binding.target.addEventListener(binding.type, binding); | ||
function registerBinding(type, binding) { | ||
binding.target.addEventListener(type, binding); | ||
return binding; | ||
} | ||
/** | ||
* Invoked when event handler was changed | ||
* Creates new attribute change set | ||
*/ | ||
function changeEvent(name, prevValue, newValue) { | ||
if (!prevValue && newValue) { | ||
// Should register new binding | ||
registerBinding(newValue); | ||
} | ||
else if (prevValue && !newValue) { | ||
removeStaticEvent(prevValue); | ||
} | ||
function attributeSet() { | ||
return { c: obj(), p: obj() }; | ||
} | ||
/** | ||
* Sets value of attribute `name` to `value` | ||
* @return Update status. Always returns `0` since actual attribute value | ||
* is defined in `finalizeAttributes()` | ||
* Create pending props change set | ||
*/ | ||
function setAttribute(injector, name, value) { | ||
injector.attributes.cur[name] = value; | ||
return 0; | ||
function propsSet(elem) { | ||
const props = assign(obj(), elem.componentModel.defaultProps); | ||
// NB in components, pending `c` props are tested against actual `.props`, | ||
// the `p` property is not used. To keep up with the same hidden JS class, | ||
// create `p` property as well and point it to `c` to reduce object allocations | ||
return { c: props, p: props }; | ||
} | ||
/** | ||
* Sets value of attribute `name` under namespace of `nsURI` to `value` | ||
* Alias for `elem.setAttribute` | ||
*/ | ||
function setAttributeNS(injector, nsURI, name, value) { | ||
if (!injector.attributesNS) { | ||
injector.attributesNS = obj(); | ||
} | ||
const { attributesNS } = injector; | ||
if (!attributesNS[nsURI]) { | ||
attributesNS[nsURI] = changeSet(); | ||
} | ||
attributesNS[nsURI].cur[name] = value; | ||
function setAttribute(elem, name, value) { | ||
elem.setAttribute(name, value); | ||
return value; | ||
} | ||
/** | ||
* Updates `attrName` value in `elem`, if required | ||
* @returns New attribute value | ||
* Alias for `elem.className` | ||
*/ | ||
function updateAttribute(elem, attrName, value, prevValue) { | ||
if (value !== prevValue) { | ||
changeAttribute(attrName, prevValue, value, elem); | ||
return value; | ||
function setClass(elem, value) { | ||
elem.className = value; | ||
return value; | ||
} | ||
/** | ||
* Sets attribute value as expression. Unlike regular primitive attributes, | ||
* expression values must be represented, e.g. non-primitive values must be | ||
* converted to string representations. Also, expression resolved to `false`, | ||
* `null` or `undefined` will remove attribute from element | ||
*/ | ||
function setAttributeExpression(elem, name, value) { | ||
const primitive = representedValue(value); | ||
primitive === null | ||
? elem.removeAttribute(name) | ||
: setAttribute(elem, name, primitive); | ||
return value; | ||
} | ||
/** | ||
* Updates attribute value only if it’s not equal to previous value | ||
*/ | ||
function updateAttributeExpression(elem, name, value, prevValue) { | ||
return prevValue !== value | ||
? setAttributeExpression(elem, name, value) | ||
: value; | ||
} | ||
/** | ||
* Alias for `elem.setAttributeNS` | ||
*/ | ||
function setAttributeNS(elem, ns, name, value) { | ||
elem.setAttributeNS(ns, name, value); | ||
return value; | ||
} | ||
/** | ||
* Same as `setAttributeExpression()` but for namespaced attributes | ||
*/ | ||
function setAttributeExpressionNS(elem, ns, name, value) { | ||
const primitive = representedValue(value); | ||
primitive === null | ||
? elem.removeAttributeNS(ns, name) | ||
: setAttributeNS(elem, ns, name, primitive); | ||
return value; | ||
} | ||
/** | ||
* Adds or removes (if value is `void`) attribute to given element | ||
*/ | ||
function updateAttributeExpressionNS(elem, ns, name, value, prevValue) { | ||
return prevValue !== value | ||
? setAttributeExpressionNS(elem, ns, name, value) | ||
: value; | ||
} | ||
/** | ||
* Alias for `elem.classList.add()` | ||
*/ | ||
function addClass(elem, className) { | ||
elem.classList.add(className); | ||
} | ||
/** | ||
* Adds class to given element if given condition is truthy | ||
*/ | ||
function addClassIf(elem, className, condition) { | ||
condition && addClass(elem, className); | ||
return condition; | ||
} | ||
/** | ||
* Toggles class on given element if condition is changed | ||
*/ | ||
function toggleClassIf(elem, className, condition, prevResult) { | ||
if (prevResult !== condition) { | ||
condition ? addClass(elem, className) : elem.classList.remove(className); | ||
} | ||
return prevValue; | ||
return condition; | ||
} | ||
/** | ||
* Updates props in given component, if required | ||
* @return Returns `true` if value was updated | ||
* Sets pending attribute value which will be added to attribute later | ||
*/ | ||
function updateProps(elem, data) { | ||
const { props } = elem; | ||
let updated; | ||
for (const p in data) { | ||
if (data.hasOwnProperty(p) && props[p] !== data[p]) { | ||
if (!updated) { | ||
updated = obj(); | ||
} | ||
updated[p] = data[p]; | ||
} | ||
function setPendingAttribute(data, name, value) { | ||
data.c[name] = value; | ||
} | ||
/** | ||
* Sets pending namespaced attribute value which will be added to attribute later | ||
*/ | ||
function setPendingAttributeNS(data, ns, name, value) { | ||
if (!data.n) { | ||
data.n = obj(); | ||
} | ||
if (updated) { | ||
elem.setProps(data); | ||
return true; | ||
if (!data.n[ns]) { | ||
data.n[ns] = attributeSet(); | ||
} | ||
return false; | ||
data.n[ns].c[name] = value; | ||
} | ||
/** | ||
* Adds given class name as pending attribute | ||
* Adds given class name to pending attribute set | ||
*/ | ||
function addClass(injector, value) { | ||
if (isDefined(value)) { | ||
const className = injector.attributes.cur.class; | ||
setAttribute(injector, 'class', isDefined(className) ? className + ' ' + value : value); | ||
function addPendingClass(data, className) { | ||
if (className != null) { | ||
const prev = data.c.class; | ||
data.c.class = prev ? prev + ' ' + className : String(className); | ||
} | ||
} | ||
/** | ||
* Applies pending attributes changes to injector’s host element | ||
* Adds given class name to pending attribute set if condition is truthy | ||
*/ | ||
function finalizeAttributes(injector) { | ||
const { attributes, attributesNS } = injector; | ||
if (isDefined(attributes.cur.class)) { | ||
attributes.cur.class = normalizeClassName(attributes.cur.class); | ||
} | ||
let updated = finalizeItems(attributes, changeAttribute, injector.parentNode); | ||
if (attributesNS) { | ||
const ctx = { node: injector.parentNode, ns: null }; | ||
for (const ns in attributesNS) { | ||
ctx.ns = ns; | ||
updated |= finalizeItems(attributesNS[ns], changeAttributeNS, ctx); | ||
function addPendingClassIf(data, className, condition) { | ||
condition && addPendingClass(data, className); | ||
} | ||
/** | ||
* Finalizes pending attributes | ||
*/ | ||
function finalizeAttributes(elem, data) { | ||
let updated = 0; | ||
const { c, p } = data; | ||
for (const name in c) { | ||
const curValue = c[name]; | ||
if (curValue !== p[name]) { | ||
updated = 1; | ||
if (name === 'class') { | ||
elem.className = classNames(curValue).join(' '); | ||
} | ||
else { | ||
setAttributeExpression(elem, name, curValue); | ||
} | ||
p[name] = curValue; | ||
} | ||
c[name] = null; | ||
} | ||
@@ -384,41 +451,62 @@ return updated; | ||
/** | ||
* Normalizes given class value: removes duplicates and trims whitespace | ||
* Finalizes pending namespaced attributes | ||
*/ | ||
function normalizeClassName(str) { | ||
const out = []; | ||
const parts = String(str).split(/\s+/); | ||
for (let i = 0, cl; i < parts.length; i++) { | ||
cl = parts[i]; | ||
if (cl && out.indexOf(cl) === -1) { | ||
out.push(cl); | ||
function finalizeAttributesNS(elem, data) { | ||
// NB use it as a separate function to use explicitly inside generated content. | ||
// It there’s no pending namespace attributes, this method will not be included | ||
// into final bundle | ||
if (!data.n) { | ||
return 0; | ||
} | ||
let updated = 0; | ||
for (const ns in data.n) { | ||
const { c, p } = data.n[ns]; | ||
for (const name in c) { | ||
const curValue = c[name]; | ||
if (curValue !== p[name]) { | ||
updated = 1; | ||
setAttributeExpressionNS(elem, ns, name, curValue); | ||
p[name] = curValue; | ||
} | ||
c[name] = null; | ||
} | ||
} | ||
return out.join(' '); | ||
return updated; | ||
} | ||
/** | ||
* Callback for changing attribute value | ||
* Returns normalized list of class names from given string | ||
*/ | ||
function changeAttribute(name, prevValue, newValue, elem) { | ||
if (isDefined(newValue)) { | ||
if (name === 'class') { | ||
elem.className = normalizeClassName(newValue); | ||
function classNames(str) { | ||
const out = []; | ||
if (isDefined(str)) { | ||
const parts = String(str).split(/\s+/); | ||
for (let i = 0, cl; i < parts.length; i++) { | ||
cl = parts[i]; | ||
if (cl && out.indexOf(cl) === -1) { | ||
out.push(cl); | ||
} | ||
} | ||
else { | ||
representAttributeValue(elem, name, newValue); | ||
} | ||
} | ||
else if (isDefined(prevValue)) { | ||
elem.removeAttribute(name); | ||
} | ||
return out; | ||
} | ||
/** | ||
* Callback for changing attribute value | ||
* Returns represented attribute value for given data | ||
*/ | ||
function changeAttributeNS(name, prevValue, newValue, ctx) { | ||
if (isDefined(newValue)) { | ||
ctx.node.setAttributeNS(ctx.ns, name, newValue); | ||
function representedValue(value) { | ||
if (value === false || !isDefined(value)) { | ||
return null; | ||
} | ||
else if (isDefined(prevValue)) { | ||
ctx.node.removeAttributeNS(ctx.ns, name); | ||
if (value === true) { | ||
return ''; | ||
} | ||
if (Array.isArray(value)) { | ||
return '[]'; | ||
} | ||
if (typeof value === 'function') { | ||
return '𝑓'; | ||
} | ||
if (typeof value === 'object') { | ||
return '{}'; | ||
} | ||
return value; | ||
} | ||
@@ -508,6 +596,3 @@ | ||
// to reduce runtime checks and keep functions in monomorphic state | ||
slots: null, | ||
attributes: changeSet(), | ||
attributesNS: void 0, | ||
events: changeSet() | ||
slots: null | ||
}; | ||
@@ -799,3 +884,3 @@ } | ||
if (block.update) { | ||
block.update(block.host, block.injector, block.scope); | ||
block.update(block.host, block.scope); | ||
} | ||
@@ -866,4 +951,4 @@ } | ||
const { props, state, extend, events } = prepare(element, definition); | ||
element.refs = {}; | ||
element.props = obj(props); | ||
element.refs = obj(); | ||
element.props = obj(); | ||
element.state = state; | ||
@@ -889,6 +974,4 @@ element.componentView = element; // XXX Should point to Shadow Root in Web Components | ||
vars: obj(), | ||
refs: changeSet(), | ||
mounted: false, | ||
rendering: false, | ||
finalizing: false, | ||
update: void 0, | ||
@@ -905,15 +988,7 @@ queued: false, | ||
*/ | ||
function mountComponent(component, initialProps) { | ||
function mountComponent(component, props) { | ||
const { componentModel } = component; | ||
const { input, definition, defaultProps } = componentModel; | ||
let changes = setPropsInternal(component, obj(), assign(obj(defaultProps), initialProps)); | ||
const runtimeChanges = setPropsInternal(component, input.attributes.prev, input.attributes.cur); | ||
if (changes && runtimeChanges) { | ||
assign(changes, runtimeChanges); | ||
} | ||
else if (runtimeChanges) { | ||
changes = runtimeChanges; | ||
} | ||
const { input, definition } = componentModel; | ||
const changes = setPropsInternal(component, props || componentModel.defaultProps); | ||
const arg = changes || {}; | ||
finalizeEvents(input); | ||
componentModel.rendering = true; | ||
@@ -932,6 +1007,4 @@ // Notify slot status | ||
componentModel.rendering = false; | ||
componentModel.finalizing = true; | ||
runHook(component, 'didRender', arg); | ||
runHook(component, 'didMount', arg); | ||
componentModel.finalizing = false; | ||
} | ||
@@ -941,6 +1014,4 @@ /** | ||
*/ | ||
function updateComponent(component) { | ||
const { input } = component.componentModel; | ||
const changes = setPropsInternal(component, input.attributes.prev, input.attributes.cur); | ||
finalizeEvents(input); | ||
function updateComponent(component, props) { | ||
const changes = props && setPropsInternal(component, props); | ||
if (changes || component.componentModel.queued) { | ||
@@ -959,3 +1030,2 @@ renderNext(component, changes); | ||
const { definition, events } = componentModel; | ||
const scope = getScope(component); | ||
runHook(component, 'willUnmount'); | ||
@@ -970,3 +1040,3 @@ componentModel.mounted = false; | ||
const dispose = definition.default && definition.default.dispose; | ||
captureError(component, dispose, scope); | ||
captureError(component, dispose, getScope(component)); | ||
runHook(component, 'didUnmount'); | ||
@@ -1026,6 +1096,4 @@ // @ts-ignore: Nulling disposed object | ||
componentModel.rendering = false; | ||
componentModel.finalizing = true; | ||
runHook(component, 'didRender', arg); | ||
runHook(component, 'didUpdate', arg); | ||
componentModel.finalizing = false; | ||
} | ||
@@ -1044,19 +1112,20 @@ /** | ||
} | ||
function setPropsInternal(component, prevProps, nextProps) { | ||
const changes = {}; | ||
let didChanged = false; | ||
function setPropsInternal(component, nextProps) { | ||
let changes; | ||
const { props } = component; | ||
const { defaultProps } = component.componentModel; | ||
for (const p in nextProps) { | ||
const prev = prevProps[p]; | ||
const prev = props[p]; | ||
let current = nextProps[p]; | ||
if (current == null) { | ||
current = defaultProps[p]; | ||
nextProps[p] = current = defaultProps[p]; | ||
} | ||
if (p === 'class' && current != null) { | ||
current = normalizeClassName(current); | ||
current = classNames(current).join(' '); | ||
} | ||
if (current !== prev) { | ||
didChanged = true; | ||
props[p] = prevProps[p] = current; | ||
if (!changes) { | ||
changes = obj(); | ||
} | ||
props[p] = current; | ||
changes[p] = { current, prev }; | ||
@@ -1067,5 +1136,4 @@ if (!/^partial:/.test(p)) { | ||
} | ||
nextProps[p] = null; | ||
} | ||
return didChanged ? changes : null; | ||
return changes; | ||
} | ||
@@ -1150,4 +1218,5 @@ /** | ||
if (value != null && componentModel && componentModel.mounted) { | ||
const changes = setPropsInternal(element, element.props, obj(value)); | ||
const changes = setPropsInternal(element, assign(obj(), value)); | ||
changes && renderNext(element, changes); | ||
return changes; | ||
} | ||
@@ -1218,3 +1287,3 @@ }; | ||
// Update rendered result | ||
updated = block.update(host, injector, scope) ? 1 : 0; | ||
updated = block.update(host, scope) ? 1 : 0; | ||
} | ||
@@ -1294,3 +1363,3 @@ block.injector.ptr = block.end; | ||
setScope(host, scope); | ||
if (rendered.update(host, injector, scope)) { | ||
if (rendered.update(host, scope)) { | ||
this.updated = 1; | ||
@@ -1357,3 +1426,3 @@ } | ||
const prevScope = getScope(host); | ||
collection.forEach(iterator$1, block); | ||
collection.forEach(keyIterator, block); | ||
setScope(host, prevScope); | ||
@@ -1379,3 +1448,3 @@ } | ||
} | ||
function iterator$1(value, key) { | ||
function keyIterator(value, key) { | ||
const { injector, index, rendered } = this; | ||
@@ -1417,6 +1486,6 @@ const id = this.keyExpr(value, prepareScope(this.scope, index, key, value)); | ||
if (entry.update) { | ||
const { host, injector } = entry; | ||
const { host } = entry; | ||
const scope = prepareScope(entry.scope, index, key, value); | ||
setScope(host, scope); | ||
if (entry.update(host, injector, scope)) { | ||
if (entry.update(host, scope)) { | ||
return 1; | ||
@@ -1471,30 +1540,33 @@ } | ||
/** | ||
* Sets runtime ref (e.g. ref which will be changed over time) to given host | ||
* @returns Update status. Refs must be explicitly finalized, thus | ||
* we always return `0` as nothing was changed | ||
* Adds given element as a named ref | ||
*/ | ||
function setRef(host, name, elem) { | ||
host.componentModel.refs.cur[name] = elem; | ||
return 0; | ||
function setRef(host, key, elem) { | ||
elem.setAttribute(getRefAttr(key, host), ''); | ||
host.refs[key] = elem; | ||
} | ||
/** | ||
* Sets static ref (e.g. ref which won’t be changed over time) to given host | ||
* Removes ref for given key | ||
*/ | ||
function setStaticRef(host, name, value) { | ||
value && value.setAttribute(getRefAttr(name, host), ''); | ||
host.refs[name] = value; | ||
function removeRef(host, key) { | ||
const elem = host.refs[key]; | ||
if (elem) { | ||
elem.removeAttribute(getRefAttr(key, host)); | ||
host.refs[key] = null; | ||
} | ||
} | ||
/** | ||
* Finalizes refs on given scope | ||
* @returns Update status | ||
*/ | ||
function finalizeRefs(host) { | ||
return finalizeItems(host.componentModel.refs, changeRef, host); | ||
function setPendingRef(pending, key, elem) { | ||
if (key && elem) { | ||
pending[key] = elem; | ||
} | ||
} | ||
/** | ||
* Invoked when element reference was changed | ||
*/ | ||
function changeRef(name, prevValue, newValue, host) { | ||
prevValue && prevValue.removeAttribute(getRefAttr(name, host)); | ||
setStaticRef(host, name, newValue); | ||
function finalizePendingRefs(host, pending) { | ||
for (const key in pending) { | ||
const prev = host.refs[key]; | ||
const next = pending[key]; | ||
if (prev !== next) { | ||
prev && removeRef(host, key); | ||
next && setRef(host, key, next); | ||
} | ||
pending[key] = null; | ||
} | ||
} | ||
@@ -1576,2 +1648,5 @@ /** | ||
function getPartial(host, name, componentPartials) { | ||
return host.props['partial:' + name] || componentPartials[name]; | ||
} | ||
/** | ||
@@ -1619,3 +1694,3 @@ * Mounts given partial into injector context | ||
const scope = setScope(host, assign(block.scope, args)); | ||
if (block.update(host, injector, scope)) { | ||
if (block.update(host, scope)) { | ||
updated = 1; | ||
@@ -1781,2 +1856,13 @@ } | ||
elem.style.animation = animation; | ||
// In case if callback is provided, we have to ensure that animation is actually applied. | ||
// In some testing environments, animations could be disabled via | ||
// `* { animation: none !important; }`. In this case, we should complete animation ASAP. | ||
if (callback) { | ||
nextTick(() => { | ||
const style = window.getComputedStyle(elem, null); | ||
if (!style.animationName || style.animationName === 'none') { | ||
stopAnimation(elem); | ||
} | ||
}); | ||
} | ||
} | ||
@@ -1913,4 +1999,2 @@ /** | ||
* Concatenates two strings with optional separator | ||
* @param {string} name | ||
* @param {string} suffix | ||
*/ | ||
@@ -1921,2 +2005,10 @@ function concat(name, suffix) { | ||
} | ||
function nextTick(fn) { | ||
if (typeof Promise !== 'undefined') { | ||
Promise.resolve().then(fn); | ||
} | ||
else { | ||
requestAnimationFrame(fn); | ||
} | ||
} | ||
@@ -2008,3 +2100,3 @@ /** | ||
export default endorphin; | ||
export { Store, addClass, addEvent, addStaticEvent, animate, assign, call, composeTween, createAnimation, createComponent, createInjector, createScope, createSlot, cssAnimate, disposeBlock, domInsert, domRemove, elem, elemNS, elemNSWithText, elemWithText, emptyBlockContent, enterScope, exitScope, filter, finalizeAttributes, finalizeEvents, finalizeRefs, find, get, getProp, getScope, getSlotContext, getState, getVar, injectBlock, insert, isolateElement, mountBlock, mountComponent, mountInnerHTML, mountIterator, mountKeyIterator, mountPartial, mountSlot, move, normalizeClassName, notifySlotUpdate, prepareScope, removeStaticEvent, renderComponent, safeEventListener, scheduleRender, setAttribute, setAttributeNS, setRef, setScope, setStaticRef, setVar, stopAnimation, subscribeStore, text, tweenAnimate, unmountBlock, unmountComponent, unmountInnerHTML, unmountIterator, unmountKeyIterator, unmountPartial, unmountSlot, updateAttribute, updateBlock, updateComponent, updateDefaultSlot, updateIncomingSlot, updateInnerHTML, updateIterator, updateKeyIterator, updatePartial, updateProps, updateText }; | ||
export { Store, addClass, addClassIf, addEvent, addPendingClass, addPendingClassIf, animate, appendChild, assign, attributeSet, call, classNames, composeTween, createAnimation, createComponent, createInjector, createScope, createSlot, cssAnimate, detachPendingEvents, disposeBlock, domInsert, domRemove, elem, elemNS, elemNSWithText, elemWithText, emptyBlockContent, enterScope, exitScope, filter, finalizeAttributes, finalizeAttributesNS, finalizePendingEvents, finalizePendingRefs, find, get, getPartial, getProp, getScope, getSlotContext, getState, getVar, injectBlock, insert, isolateElement, mountBlock, mountComponent, mountInnerHTML, mountIterator, mountKeyIterator, mountPartial, mountSlot, move, notifySlotUpdate, obj, pendingEvents, prepareScope, propsSet, removeEvent, removeRef, renderComponent, safeEventListener, scheduleRender, setAttribute, setAttributeExpression, setAttributeExpressionNS, setAttributeNS, setClass, setPendingAttribute, setPendingAttributeNS, setPendingEvent, setPendingRef, setRef, setScope, setVar, stopAnimation, subscribeStore, text, toggleClassIf, tweenAnimate, unmountBlock, unmountComponent, unmountInnerHTML, unmountIterator, unmountKeyIterator, unmountPartial, unmountSlot, updateAttributeExpression, updateAttributeExpressionNS, updateBlock, updateComponent, updateDefaultSlot, updateIncomingSlot, updateInnerHTML, updateIterator, updateKeyIterator, updatePartial, updateText }; | ||
//# sourceMappingURL=runtime.es.js.map |
@@ -13,3 +13,3 @@ import { Component } from './component'; | ||
} | ||
export declare type UpdateBlock<D = Scope> = (host: Component, injector: Injector, data: D) => number | void; | ||
export declare type UpdateBlock<D = Scope> = (host: Component, data: D) => number | void; | ||
export declare type UnmountBlock = (scope: Scope, host: Component) => void; | ||
@@ -39,4 +39,5 @@ export interface Scope { | ||
scope: Scope; | ||
type: string; | ||
target: Element; | ||
listener?: EventListener; | ||
pending?: EventListener; | ||
} |
import { Changes, ChangeSet } from './types'; | ||
import { Component } from './component'; | ||
export declare const animatingKey = "$$animating"; | ||
declare type ChangeCallback = (name: string, prev: any, next: any, ctx?: any) => void; | ||
/** | ||
@@ -11,15 +10,5 @@ * Creates fast object | ||
* Check if given value id defined, e.g. not `null`, `undefined` or `NaN` | ||
* @param {*} value | ||
* @returns {boolean} | ||
*/ | ||
export declare function isDefined(value: any): boolean; | ||
/** | ||
* Finalizes updated items, defined in `items.prev` and `items.cur` | ||
* @param {object} items | ||
* @param {function} change | ||
* @param {*} [ctx] | ||
* @returns {number} Returns `1` if data was updated, `0` otherwise | ||
*/ | ||
export declare function finalizeItems(items: ChangeSet, change: ChangeCallback, ctx: any): number; | ||
/** | ||
* Creates object for storing change sets, e.g. current and previous values | ||
@@ -26,0 +15,0 @@ */ |
{ | ||
"name": "@endorphinjs/template-runtime", | ||
"version": "0.4.5", | ||
"version": "0.5.0", | ||
"description": "EndorphinJS template runtime, embedded with template bundles", | ||
@@ -26,3 +26,3 @@ "main": "./dist/runtime.cjs.js", | ||
"devDependencies": { | ||
"@endorphinjs/template-compiler": "^0.4.5", | ||
"@endorphinjs/template-compiler": "^0.5.0", | ||
"@types/mocha": "^5.2.6", | ||
@@ -53,3 +53,3 @@ "@types/node": "^11.13.10", | ||
}, | ||
"gitHead": "1e768e8d1b61a919ab64123831d73b738a336d07" | ||
"gitHead": "26809b00768921270e0b6ee4b0d6383e411644e7" | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
428213
5098