@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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
428213
5098