@bikeshaving/crank
Advanced tools
Comparing version 0.3.5 to 0.3.6
@@ -5,3 +5,6 @@ /** | ||
* @remarks | ||
* Elements whose tags are strings or symbols are called “host” or “intrinsic” elements, and their behavior is determined by the renderer, while elements whose tags are components are called “component” elements, and their behavior is determined by the execution of the component. | ||
* Elements whose tags are strings or symbols are called “host” or “intrinsic” | ||
* elements, and their behavior is determined by the renderer, while elements | ||
* whose tags are components are called “component” elements, and their | ||
* behavior is determined by the execution of the component. | ||
*/ | ||
@@ -17,9 +20,15 @@ export declare type Tag = string | symbol | Component; | ||
* SPECIAL TAGS | ||
* | ||
* Crank provides a couple tags which have special meaning for the renderer. | ||
***/ | ||
/** | ||
* A special element tag for grouping multiple children within a parent. | ||
* A special tag for grouping multiple children within a parent. | ||
* | ||
* @remarks | ||
* All iterables which appear in the element tree are implicitly wrapped in fragments. The Fragment tag is just the empty string, and you can use the empty string in createElement calls/transpiler options to avoid having to reference the Fragment export directly. | ||
* All iterables which appear in the element tree are implicitly wrapped in a | ||
* fragment element. | ||
* | ||
* The tag is just the empty string, and you can use the empty string in | ||
* createElement calls or transpiler options to avoid having to reference this | ||
* export directly. | ||
*/ | ||
@@ -29,6 +38,10 @@ export declare const Fragment = ""; | ||
/** | ||
* A special element tag for creating a new element subtree with a different root, passed via the root prop. | ||
* A special tag for rendering into a root node passed via a root prop. | ||
* | ||
* @remarks | ||
* This element tag is useful for creating element trees with multiple roots. Renderer.prototype.render will implicitly wrap the children which have been passed in in an implicit Portal element. | ||
* This tag is useful for creating element trees with multiple roots, for | ||
* things like modals or tooltips. | ||
* | ||
* Renderer.prototype.render will implicitly wrap the passed in element tree in | ||
* an implicit Portal element. | ||
*/ | ||
@@ -38,6 +51,9 @@ export declare const Portal: any; | ||
/** | ||
* A special element tag which copies whatever child appeared previously in the element’s position. | ||
* A special tag which preserves whatever was previously rendered in the | ||
* element’s position. | ||
* | ||
* @remarks | ||
* Copy elements are useful when you want to prevent a subtree from updating as a performance optimization. | ||
* Copy elements are useful for when you want to prevent a subtree from | ||
* rerendering as a performance optimization. Copy elements can also be keyed, | ||
* in which case the previously rendered keyed element will be preserved. | ||
*/ | ||
@@ -47,6 +63,7 @@ export declare const Copy: any; | ||
/** | ||
* A special element tag for injecting raw nodes into an element tree via its value prop. | ||
* A special element tag for injecting raw nodes or strings via a value prop. | ||
* | ||
* @remarks | ||
* If the value prop is a string, Renderer.prototype.parse will be called on the string and the element’s rendered value will be the result. | ||
* If the value prop is a string, Renderer.prototype.parse will be called on | ||
* the string and the result of that method will be inserted. | ||
*/ | ||
@@ -59,3 +76,5 @@ export declare const Raw: any; | ||
* @remarks | ||
* Arbitrary objects can also be safely rendered but they will be converted to a string using the toString method. We exclude them from this type to catch potential mistakes. | ||
* Arbitrary objects can also be safely rendered, but will be converted to a | ||
* string using the toString method. We exclude them from this type to catch | ||
* potential mistakes. | ||
*/ | ||
@@ -66,10 +85,7 @@ export declare type Child = Element | string | number | boolean | null | undefined; | ||
/** | ||
* Describes all valid values of an element tree, including arbitrarily nested iterables of such values. | ||
* Describes all valid values of an element tree, including arbitrarily nested | ||
* iterables of such values. | ||
*/ | ||
export declare type Children = Child | ChildIterable; | ||
/** | ||
* All values in the element tree are narrowed from the union in Child to NarrowedChild during rendering. This greatly simplifies element diffing. | ||
*/ | ||
declare type NarrowedChild = Element | string | undefined; | ||
/** | ||
* Represents all functions which can be used as a component. | ||
@@ -83,5 +99,7 @@ * | ||
/** | ||
* Elements are the basic building blocks of Crank applications. They are JavaScript objects which are interpreted by special classes called renderers to produce and manage stateful nodes. | ||
* Elements are the basic building blocks of Crank applications. They are | ||
* JavaScript objects which are interpreted by special classes called renderers | ||
* to produce and manage stateful nodes. | ||
* | ||
* @typeparam TTag - the type of the tag of the element. | ||
* @typeparam TTag - The type of the tag of the element. | ||
* | ||
@@ -99,3 +117,4 @@ * @example | ||
* @remarks | ||
* Typically, you use the createElement function to create elements and not this class directly. | ||
* Typically, you use the createElement function to create elements rather than | ||
* instatiating this class directly. | ||
*/ | ||
@@ -105,3 +124,4 @@ export declare class Element<TTag extends Tag = Tag> { | ||
* @internal | ||
* A unique symbol to identify elements as elements across versions and realms, and to protect against basic injection attacks. | ||
* A unique symbol to identify elements as elements across versions and | ||
* realms, and to protect against basic injection attacks. | ||
* https://overreacted.io/why-do-react-elements-have-typeof-property/ | ||
@@ -116,14 +136,14 @@ */ | ||
/** | ||
* The tag of the element. Can be a function, string or symbol depending on the kind of element. | ||
* The tag of the element. Can be a string, symbol or function. | ||
*/ | ||
tag: TTag; | ||
/** | ||
* An object containing the “properties” of an element. These correspond to the attributes of the element when using JSX syntax. | ||
* | ||
* @remarks | ||
* The props of an object are passed to most renderer host methods, and as the first argument to components. | ||
* An object containing the “properties” of an element. These correspond to | ||
* the “attributes” of an element when using JSX syntax. | ||
*/ | ||
props: TagProps<TTag>; | ||
/** | ||
* A value which uniquely identifies an element from its siblings so that it can be added/updated/moved/removed by the identity of the key rather than its position within the parent. | ||
* A value which uniquely identifies an element from its siblings so that it | ||
* can be added/updated/moved/removed by the identity of the key rather than | ||
* its position within the parent. | ||
* | ||
@@ -135,3 +155,3 @@ * @remarks | ||
/** | ||
* A callback which is called with the element’s value when the value is committed. | ||
* A callback which is called with the element’s result when it is committed. | ||
* | ||
@@ -141,3 +161,3 @@ * @remarks | ||
*/ | ||
ref: Function | undefined; | ||
ref: ((value: unknown) => unknown) | undefined; | ||
/** | ||
@@ -153,3 +173,4 @@ * @internal | ||
* @remarks | ||
* Set by Renderer.prototype.create when the component is mounted. This property will only be set for host elements. | ||
* Set by Renderer.prototype.create when the component is mounted. | ||
* This property will only be set for host elements. | ||
*/ | ||
@@ -162,3 +183,4 @@ _n: any; | ||
* @remarks | ||
* Created and assigned by the Renderer for component elements when it mounts the element tree. | ||
* Created and assigned by the Renderer for component elements when it mounts | ||
* the element tree. | ||
*/ | ||
@@ -171,11 +193,15 @@ _ctx: Context<TagProps<TTag>> | undefined; | ||
* @remarks | ||
* Until an element commits for the first time, we show any previously rendered values in its place. | ||
* Until an element commits for the first time, we show any previously | ||
* rendered values in its place. This is mainly important when the nearest | ||
* host is rearranged concurrently. | ||
*/ | ||
_fb: Element | undefined; | ||
_fb: NarrowedChild; | ||
/** | ||
* @internal | ||
* inflightPromise - The current async run of the element. | ||
* inflight - The current async run of the element. | ||
* | ||
* @remarks | ||
* This value is used to make sure Copy element refs and as the yield value of async generator components with async children. It is unset when the element is committed. | ||
* This value is used to make sure Copy element refs fire at the correct | ||
* time, and is also used as the yield value of async generator components | ||
* with async children. It is unset when the element is committed. | ||
*/ | ||
@@ -185,6 +211,7 @@ _inf: Promise<any> | undefined; | ||
* @internal | ||
* onNewValues - the resolve function of a promise which represents the next child result. See the chase function for more info. | ||
* onvalues - The resolve function of a promise which represents the next | ||
* children result. | ||
*/ | ||
_onv: Function | undefined; | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref: Function | undefined); | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref: ((value: unknown) => unknown) | undefined); | ||
} | ||
@@ -196,6 +223,9 @@ export declare function isElement(value: any): value is Element; | ||
* @remarks | ||
* This function is usually used as a transpilation target for JSX transpilers, but it can also be called directly. It additionally extracts the crank-key and crank-ref props so they aren’t accessible to renderer methods or components, and assigns the children prop according to the arguments passed to the function. | ||
* This function is usually used as a transpilation target for JSX transpilers, | ||
* but it can also be called directly. It additionally extracts the crank-key | ||
* and crank-ref props so they aren’t accessible to renderer methods or | ||
* components, and assigns the children prop according to the arguments passed | ||
* to the function. | ||
*/ | ||
export declare function createElement<TTag extends Tag>(tag: TTag, props?: TagProps<TTag> | null | undefined, ...children: Array<unknown>): Element<TTag>; | ||
export declare function createElement<TTag extends Tag>(tag: TTag, props?: TagProps<TTag> | null | undefined, children?: unknown): Element<TTag>; | ||
/** | ||
@@ -205,7 +235,14 @@ * Clones a given element. Will also shallow copy the props object. | ||
* @remarks | ||
* Mainly used internally to make sure we don’t accidentally reuse elements in an element tree, because element have internal properties which are directly mutated by the renderer. | ||
* Mainly used internally to make sure we don’t accidentally reuse elements in | ||
* an element tree, because element have internal properties which are directly | ||
* mutated by the renderer. | ||
*/ | ||
export declare function cloneElement<TTag extends Tag>(el: Element<TTag>): Element<TTag>; | ||
/*** ELEMENT VALUE UTILITIES ***/ | ||
/*** ELEMENT UTILITIES ***/ | ||
/** | ||
* All values in the element tree are narrowed from the union in Child to | ||
* NarrowedChild during rendering, to simplify element diffing. | ||
*/ | ||
declare type NarrowedChild = Element | string | undefined; | ||
/** | ||
* A helper type which repesents all the possible rendered values of an element. | ||
@@ -216,12 +253,24 @@ * | ||
* @remarks | ||
* When asking the question, what is the value of a specific element, the answer varies depending on the tag of the element. For host or Raw elements, the answer is simply the nodes (DOM nodes in the case of the DOM renderer) created for the element. For fragments, the values are usually an array of nodes. For portals, the value is undefined, because a Portal element’s root and children are opaque to its parent. For components, the value can be any of the above, because the value of a component is determined by its children. Rendered values can also be strings or arrays of nodes and strings, in the case of component or fragment elements with strings for children. All of these possible values are reflected in this utility type. | ||
* When asking the question, what is the value of a specific element, the | ||
* answer varies depending on its tag. For host or Raw elements, the answer is | ||
* simply the nodes created for the element. For fragments, the values are | ||
* usually an array of nodes. For portals, the value is undefined, because a | ||
* Portal element’s root and children are opaque to its parent. For components, | ||
* the value can be any of the above, because the value of a component is | ||
* determined by its immediate children. Rendered values can also be strings or | ||
* arrays of nodes and strings, in the case of component or fragment elements | ||
* with strings for children. All of these possible values are reflected in | ||
* this utility type. | ||
*/ | ||
export declare type ElementValue<TNode> = Array<TNode | string> | TNode | string | undefined; | ||
/** | ||
* An abstract class which is subclassed to render to different target environments. This class is responsible for kicking off the rendering process, caching previous trees by root, and creating/mutating/disposing the nodes of the target environment. | ||
* An abstract class which is subclassed to render to different target | ||
* environments. This class is responsible for kicking off the rendering | ||
* process, caching previous trees by root, and creating, mutating and | ||
* disposing of nodes. | ||
* | ||
* @typeparam TNode - The type of the node for a specific rendering environment. | ||
* @typeparam TNode - The type of the node for a rendering environment. | ||
* @typeparam TScope - Data which is passed down the tree. | ||
* @typeparam TRoot - The type of the root for a specific rendering environment. | ||
* @typeparam TResult - The type of the exposed values. | ||
* @typeparam TRoot - The type of the root for a rendering environment. | ||
* @typeparam TResult - The type of exposed values. | ||
*/ | ||
@@ -238,17 +287,30 @@ export declare class Renderer<TNode, TScope, TRoot = TNode, TResult = ElementValue<TNode>> { | ||
* | ||
* @param children - An element tree. You can render null with a previously used root to delete the previously rendered element tree from the cache. | ||
* @param root - The node to be rendered into. The renderer will cache element trees per root. | ||
* @param ctx - An optional context that will be the ancestor context of all elements in the tree. Useful for connecting renderers which call each other so that events/provisions properly propagate. The context for a given root must be the same or an error will be thrown. | ||
* @param children - An element tree. You can render null with a previously | ||
* used root to delete the previously rendered element tree from the cache. | ||
* @param root - The node to be rendered into. The renderer will cache | ||
* element trees per root. | ||
* @param ctx - An optional context that will be the ancestor context of all | ||
* elements in the tree. Useful for connecting renderers which call each | ||
* other so that events/provisions properly propagate. The context for a | ||
* given root must be the same or an error will be thrown. | ||
* | ||
* @returns The read result of rendering the children, or a possible promise of the read result if the element tree renders asynchronously. | ||
* @returns The result of rendering the children, or a possible promise of | ||
* the result if the element tree renders asynchronously. | ||
*/ | ||
render(children: Children, root?: TRoot | undefined, ctx?: Context | undefined): Promise<TResult> | TResult; | ||
/** | ||
* Called when an element’s value is exposed via render, schedule, refresh, refs, or generator yield expressions. | ||
* Called when an element’s value is exposed via render, schedule, refresh, | ||
* refs, or generator yield expressions. | ||
* | ||
* @param value - The value of the element being read. Can be a node, a string, undefined, or an array of nodes and strings, depending on the element. | ||
* @returns Varies according to the specific renderer subclass. By default, it exposes the element’s value. | ||
* @param value - The value of the element being read. Can be a node, a | ||
* string, undefined, or an array of nodes and strings, depending on the | ||
* element. | ||
* | ||
* @returns Varies according to the specific renderer subclass. By default, | ||
* it exposes the element’s value. | ||
* | ||
* @remarks | ||
* This is useful for renderers which don’t want to expose their internal nodes. For instance, the HTML renderer will convert all internal nodes to strings. | ||
* This is useful for renderers which don’t want to expose their internal | ||
* nodes. For instance, the HTML renderer will convert all internal nodes to | ||
* strings. | ||
* | ||
@@ -261,3 +323,5 @@ */ | ||
* @remarks | ||
* Useful for passing data down the element tree. For instance, the DOM renderer uses this method to keep track of whether we’re in an SVG subtree. | ||
* Useful for passing data down the element tree. For instance, the DOM | ||
* renderer uses this method to keep track of whether we’re in an SVG | ||
* subtree. | ||
* | ||
@@ -267,6 +331,8 @@ * @param el - The host element. | ||
* | ||
* @returns The scope to be passed to create and scope for child host elements. | ||
* @returns The scope to be passed to create and scope for child host | ||
* elements. | ||
* | ||
* @remarks | ||
* This method sets the scope for child host elements, not the current host element. | ||
* This method sets the scope for child host elements, not the current host | ||
* element. | ||
*/ | ||
@@ -283,3 +349,6 @@ scope(el: Element<string | symbol>, scope: TScope | undefined): TScope; | ||
* @remarks | ||
* Rather than returning text nodes for whatever environment we’re rendering to, we defer that step for Renderer.prototype.arrange. We do this so that adjacent strings can be concatenated and the actual element tree can be rendered in a normalized form. | ||
* Rather than returning text nodes for whatever environment we’re rendering | ||
* to, we defer that step for Renderer.prototype.arrange. We do this so that | ||
* adjacent strings can be concatenated and the actual element tree can be | ||
* rendered in a normalized form. | ||
*/ | ||
@@ -310,3 +379,2 @@ escape(text: string, _scope: TScope): string; | ||
* @param node - The node associated with the host element. | ||
* @param scope - The current scope. | ||
* | ||
@@ -316,7 +384,8 @@ * @returns The return value is ignored. | ||
* @remarks | ||
* Used to mutate the node associated with an element when new props are passed. | ||
* Used to mutate the node associated with an element when new props are | ||
* passed. | ||
*/ | ||
patch(_el: Element<string | symbol>, _node: TNode): unknown; | ||
/** | ||
* Called for each host element after its children have committed with the actual values of the children. | ||
* Called for each host element so that elements can be arranged into a tree. | ||
* | ||
@@ -330,10 +399,10 @@ * @param el - The host element. | ||
* @remarks | ||
* This method is also called by child components contexts as the last step of a refresh. | ||
* This method is also called by child components contexts as the last step | ||
* of a refresh. | ||
*/ | ||
arrange(_el: Element<string | symbol>, _parent: TNode | TRoot, _children: Array<TNode | string>): unknown; | ||
arrange(_el: Element<string | symbol>, _node: TNode | TRoot, _children: Array<TNode | string>): unknown; | ||
/** | ||
* Called for each host element when it is unmounted. | ||
* | ||
* @param tag - The tag of the host element. | ||
* @param props - The props of the host element. | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
@@ -354,27 +423,21 @@ * | ||
/** | ||
* A map of event type strings to Event subclasses. Can be extended via TypeScript module augmentation to have strongly typed event listeners. | ||
* An interface which can be extended to provide strongly typed provisions (see | ||
* Context.prototype.get and Context.prototype.set) | ||
*/ | ||
export interface EventMap extends Crank.EventMap { | ||
[type: string]: Event; | ||
export interface ProvisionMap extends Crank.ProvisionMap { | ||
} | ||
declare type MappedEventListener<T extends string> = (ev: EventMap[T]) => unknown; | ||
declare type MappedEventListenerOrEventListenerObject<T extends string> = MappedEventListener<T> | { | ||
handleEvent: MappedEventListener<T>; | ||
}; | ||
interface EventListenerRecord { | ||
type: string; | ||
listener: MappedEventListenerOrEventListenerObject<any>; | ||
callback: MappedEventListener<any>; | ||
options: AddEventListenerOptions; | ||
export interface Context extends Crank.Context { | ||
} | ||
/** | ||
* An interface which can be extended to provide strongly typed provisions (see Context.prototype.get and Context.prototype.set) | ||
* A class which is instantiated and passed to every component as its this | ||
* value. Contexts form a tree just like elements and all components in the | ||
* element tree are connected via contexts. Components can use this tree to | ||
* communicate data upwards via events and downwards via provisions. | ||
* | ||
* @typeparam TProps - The expected shape of the props passed to the component. | ||
* Used to strongly type the Context iterator methods. | ||
* @typeparam TResult - The readable element value type. It is used in places | ||
* such as the return value of refresh and the argument passed to | ||
* schedule/cleanup callbacks. | ||
*/ | ||
export interface ProvisionMap extends Crank.ProvisionMap { | ||
} | ||
/** | ||
* A class which is instantiated and passed to every component as its this value. Contexts form a tree just like elements and all components in the element tree are connected via contexts. Components can use this tree to communicate data upwards via events and downwards via provisions. | ||
* @typeparam TProps - The expected shape of the props passed to the component. Used to strongly type the Context iterator methods. | ||
* @typeparam TResult - The readable element value type. It is used in places such as the return value of refresh and the argument passed to schedule/cleanup callbacks. | ||
*/ | ||
export declare class Context<TProps = any, TResult = any> implements EventTarget { | ||
@@ -393,3 +456,3 @@ /** | ||
* @internal | ||
* root - The root node set by an ancestor’s Portal prop. | ||
* root - The root node as set by the nearest ancestor portal. | ||
*/ | ||
@@ -401,3 +464,5 @@ _rt: unknown; | ||
* @remarks | ||
* When refresh is called, the host element will be arranged as the last step of the commit, to make sure the parent’s children properly reflects the components’s children. | ||
* When refresh is called, the host element will be arranged as the last step | ||
* of the commit, to make sure the parent’s children properly reflects the | ||
* components’s children. | ||
*/ | ||
@@ -427,5 +492,7 @@ _ho: Element<string | symbol>; | ||
* @internal | ||
* onProps - A callback used in conjunction with the Available flag to implement the props async iterator. See the Symbol.asyncIterator method and the resume function. | ||
* onavailable - A callback used in conjunction with the IsAvailable flag to | ||
* implement the props async iterator. See the Symbol.asyncIterator method | ||
* and the resumeCtx function. | ||
*/ | ||
_op: ((props: any) => unknown) | undefined; | ||
_oa: (() => unknown) | undefined; | ||
/** | ||
@@ -453,3 +520,4 @@ * @internal | ||
* @internal | ||
* listeners - An array of event listeners added to the context via Context.prototype.addEventListener | ||
* listeners - An array of event listeners added to the context via | ||
* Context.prototype.addEventListener | ||
*/ | ||
@@ -459,3 +527,4 @@ _ls: Array<EventListenerRecord> | undefined; | ||
* @internal | ||
* provisions - A map of values which can be set via Context.prototype.set and read from child contexts via Context.prototype.get | ||
* provisions - A map of values which can be set via Context.prototype.set | ||
* and read from child contexts via Context.prototype.get | ||
*/ | ||
@@ -465,3 +534,4 @@ _ps: Map<unknown, unknown> | undefined; | ||
* @internal | ||
* schedules - a set of callbacks registered via Context.prototype.schedule, which fire when the component has committed. | ||
* schedules - a set of callbacks registered via Context.prototype.schedule, | ||
* which fire when the component has committed. | ||
*/ | ||
@@ -471,3 +541,4 @@ _ss: Set<(value: TResult) => unknown> | undefined; | ||
* @internal | ||
* cleanups - a set of callbacks registered via Context.prototype.cleanup, which fire when the component has unmounted. | ||
* cleanups - a set of callbacks registered via Context.prototype.cleanup, | ||
* which fire when the component has unmounted. | ||
*/ | ||
@@ -479,6 +550,2 @@ _cs: Set<(value: TResult) => unknown> | undefined; | ||
constructor(renderer: Renderer<unknown, unknown, unknown, TResult>, root: unknown, host: Element<string | symbol>, parent: Context<unknown, TResult> | undefined, scope: unknown, el: Element<Component>); | ||
consume<TKey extends keyof ProvisionMap>(key: TKey): ProvisionMap[TKey]; | ||
consume(key: unknown): any; | ||
provide<TKey extends keyof ProvisionMap>(key: TKey, value: ProvisionMap[TKey]): void; | ||
provide(key: unknown, value: any): void; | ||
/** | ||
@@ -488,3 +555,5 @@ * The current props of the associated element. | ||
* @remarks | ||
* Typically, you should read props either via the first parameter of the component or via the context iterator methods. This property is mainly for plugins or utilities which wrap contexts. | ||
* Typically, you should read props either via the first parameter of the | ||
* component or via the context iterator methods. This property is mainly for | ||
* plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -496,3 +565,5 @@ get props(): TProps; | ||
* @remarks | ||
* Typically, you should read values via refs, generator yield expressions, or the refresh, schedule or cleanup methods. This property is mainly for plugins or utilities which wrap contexts. | ||
* Typically, you should read values via refs, generator yield expressions, | ||
* or the refresh, schedule or cleanup methods. This property is mainly for | ||
* plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -505,16 +576,26 @@ get value(): TResult; | ||
* | ||
* @returns The rendered value of the component or a promise of the rendered value if the component or its children execute asynchronously. | ||
* @returns The rendered value of the component or a promise of the rendered | ||
* value if the component or its children execute asynchronously. | ||
* | ||
* @remarks | ||
* The refresh method works a little differently for async generator components, in that it will resume the Context async iterator rather than resuming execution. This is because async generator components are perpetually resumed independent of updates/refresh. | ||
* The refresh method works a little differently for async generator | ||
* components, in that it will resume the Context async iterator rather than | ||
* resuming execution. This is because async generator components are | ||
* perpetually resumed independent of updates/refresh. | ||
*/ | ||
refresh(): Promise<TResult> | TResult; | ||
/** | ||
* Registers a callback which fires when the component commits. Will only fire once per callback and update. | ||
* Registers a callback which fires when the component commits. Will only | ||
* fire once per callback and update. | ||
*/ | ||
schedule(callback: (value: TResult) => unknown): void; | ||
/** | ||
* Registers a callback which fires when the component unmounts. Will only fire once per callback. | ||
* Registers a callback which fires when the component unmounts. Will only | ||
* fire once per callback. | ||
*/ | ||
cleanup(callback: (value: TResult) => unknown): void; | ||
consume<TKey extends keyof ProvisionMap>(key: TKey): ProvisionMap[TKey]; | ||
consume(key: unknown): any; | ||
provide<TKey extends keyof ProvisionMap>(key: TKey, value: ProvisionMap[TKey]): void; | ||
provide(key: unknown, value: any): void; | ||
addEventListener<T extends string>(type: T, listener: MappedEventListenerOrEventListenerObject<T> | null, options?: boolean | AddEventListenerOptions): void; | ||
@@ -524,4 +605,19 @@ removeEventListener<T extends string>(type: T, listener: MappedEventListenerOrEventListenerObject<T> | null, options?: EventListenerOptions | boolean): void; | ||
} | ||
export interface Context extends Crank.Context { | ||
/** | ||
* A map of event type strings to Event subclasses. Can be extended via | ||
* TypeScript module augmentation to have strongly typed event listeners. | ||
*/ | ||
export interface EventMap extends Crank.EventMap { | ||
[type: string]: Event; | ||
} | ||
declare type MappedEventListener<T extends string> = (ev: EventMap[T]) => unknown; | ||
declare type MappedEventListenerOrEventListenerObject<T extends string> = MappedEventListener<T> | { | ||
handleEvent: MappedEventListener<T>; | ||
}; | ||
interface EventListenerRecord { | ||
type: string; | ||
listener: MappedEventListenerOrEventListenerObject<any>; | ||
callback: MappedEventListener<any>; | ||
options: AddEventListenerOptions; | ||
} | ||
declare global { | ||
@@ -528,0 +624,0 @@ module Crank { |
1206
cjs/crank.js
@@ -13,8 +13,10 @@ 'use strict'; | ||
} | ||
function isIterable(value) { | ||
return value != null && typeof value[Symbol.iterator] === "function"; | ||
function arrayify(value) { | ||
return value == null | ||
? [] | ||
: typeof value !== "string" && | ||
typeof value[Symbol.iterator] === "function" | ||
? Array.from(value) | ||
: [value]; | ||
} | ||
function isNonStringIterable(value) { | ||
return typeof value !== "string" && isIterable(value); | ||
} | ||
function isIteratorLike(value) { | ||
@@ -28,61 +30,72 @@ return value != null && typeof value.next === "function"; | ||
* SPECIAL TAGS | ||
* | ||
* Crank provides a couple tags which have special meaning for the renderer. | ||
***/ | ||
/** | ||
* A special element tag for grouping multiple children within a parent. | ||
* A special tag for grouping multiple children within a parent. | ||
* | ||
* @remarks | ||
* All iterables which appear in the element tree are implicitly wrapped in fragments. The Fragment tag is just the empty string, and you can use the empty string in createElement calls/transpiler options to avoid having to reference the Fragment export directly. | ||
* All iterables which appear in the element tree are implicitly wrapped in a | ||
* fragment element. | ||
* | ||
* The tag is just the empty string, and you can use the empty string in | ||
* createElement calls or transpiler options to avoid having to reference this | ||
* export directly. | ||
*/ | ||
const Fragment = ""; | ||
// NOTE: We assert the following symbol tags to be any because typescript support for symbol tags in JSX does not exist yet. | ||
// NOTE: We assert the following symbol tags as any because typescript support | ||
// for symbol tags in JSX does not exist yet. | ||
// https://github.com/microsoft/TypeScript/issues/38367 | ||
/** | ||
* A special element tag for creating a new element subtree with a different root, passed via the root prop. | ||
* A special tag for rendering into a root node passed via a root prop. | ||
* | ||
* @remarks | ||
* This element tag is useful for creating element trees with multiple roots. Renderer.prototype.render will implicitly wrap the children which have been passed in in an implicit Portal element. | ||
* This tag is useful for creating element trees with multiple roots, for | ||
* things like modals or tooltips. | ||
* | ||
* Renderer.prototype.render will implicitly wrap the passed in element tree in | ||
* an implicit Portal element. | ||
*/ | ||
const Portal = Symbol.for("crank.Portal"); | ||
/** | ||
* A special element tag which copies whatever child appeared previously in the element’s position. | ||
* A special tag which preserves whatever was previously rendered in the | ||
* element’s position. | ||
* | ||
* @remarks | ||
* Copy elements are useful when you want to prevent a subtree from updating as a performance optimization. | ||
* Copy elements are useful for when you want to prevent a subtree from | ||
* rerendering as a performance optimization. Copy elements can also be keyed, | ||
* in which case the previously rendered keyed element will be preserved. | ||
*/ | ||
const Copy = Symbol.for("crank.Copy"); | ||
/** | ||
* A special element tag for injecting raw nodes into an element tree via its value prop. | ||
* A special element tag for injecting raw nodes or strings via a value prop. | ||
* | ||
* @remarks | ||
* If the value prop is a string, Renderer.prototype.parse will be called on the string and the element’s rendered value will be the result. | ||
* If the value prop is a string, Renderer.prototype.parse will be called on | ||
* the string and the result of that method will be inserted. | ||
*/ | ||
const Raw = Symbol.for("crank.Raw"); | ||
function narrow(child) { | ||
if (typeof child === "boolean" || child == null) { | ||
return undefined; | ||
} | ||
else if (typeof child === "string" || isElement(child)) { | ||
return child; | ||
} | ||
else { | ||
return child.toString(); | ||
} | ||
} | ||
const ElementSymbol = Symbol.for("crank.Element"); | ||
/*** ELEMENT FLAGS ***/ | ||
/** | ||
* A flag which is set when the component has been mounted. Used mainly to detect whether an element is being reused so that it can be cloned. | ||
* A flag which is set when the component has been mounted. Used mainly to | ||
* detect whether an element is being reused so that we clone it. | ||
*/ | ||
const Mounted = 1 << 0; | ||
const IsMounted = 1 << 0; | ||
/** | ||
* A flag which is set when the component has committed at least once. | ||
*/ | ||
const Committed = 1 << 1; | ||
// NOTE: To save on filesize, we mangle the internal properties of Crank classes by hand. These internal properties are prefixed with an underscore. Refer to their definitions to see their unabbreviated names. | ||
// NOTE: to maximize compatibility between Crank versions, starting with 0.2.0, the $$typeof property and the non-internal properties will not be changed, and any change to these properties will be considered a breaking change. This is to ensure maximum compatibility between components which use different Crank versions. | ||
const IsCommitted = 1 << 1; | ||
// NOTE: To save on filesize, we mangle the internal properties of Crank | ||
// classes by hand. These internal properties are prefixed with an underscore. | ||
// Refer to their definitions to see their unabbreviated names. | ||
// NOTE: to maximize compatibility between Crank versions, starting with 0.2.0, | ||
// any change to the $$typeof property or public properties will be considered | ||
// a breaking change. | ||
/** | ||
* Elements are the basic building blocks of Crank applications. They are JavaScript objects which are interpreted by special classes called renderers to produce and manage stateful nodes. | ||
* Elements are the basic building blocks of Crank applications. They are | ||
* JavaScript objects which are interpreted by special classes called renderers | ||
* to produce and manage stateful nodes. | ||
* | ||
* @typeparam TTag - the type of the tag of the element. | ||
* @typeparam TTag - The type of the tag of the element. | ||
* | ||
@@ -100,3 +113,4 @@ * @example | ||
* @remarks | ||
* Typically, you use the createElement function to create elements and not this class directly. | ||
* Typically, you use the createElement function to create elements rather than | ||
* instatiating this class directly. | ||
*/ | ||
@@ -114,3 +128,6 @@ class Element { | ||
this._ctx = undefined; | ||
// We don’t assign fallback (_fb), inflightPromise (_inf) or onNewValues (_onv) in the constructor to save on the shallow size of elements. This saves a couple bytes per element, especially when we aren’t rendering asynchronous components. This may or may not be a good idea. | ||
// NOTE: We don’t assign fallback (_fb), inflight (_inf) or onvalues (_onv) | ||
// in the constructor to save on the shallow size of elements. This saves a | ||
// couple bytes per element, especially when we aren’t rendering | ||
// asynchronous components. This may or may not be a good idea. | ||
} | ||
@@ -121,3 +138,13 @@ } | ||
} | ||
function createElement(tag, props, children) { | ||
/** | ||
* Creates an element with the specified tag, props and children. | ||
* | ||
* @remarks | ||
* This function is usually used as a transpilation target for JSX transpilers, | ||
* but it can also be called directly. It additionally extracts the crank-key | ||
* and crank-ref props so they aren’t accessible to renderer methods or | ||
* components, and assigns the children prop according to the arguments passed | ||
* to the function. | ||
*/ | ||
function createElement(tag, props, ...children) { | ||
let key; | ||
@@ -129,5 +156,6 @@ let ref; | ||
if (name === "crank-key") { | ||
// NOTE: We have to make sure we don’t assign null to the key because we don’t check for null keys in the diffing functions. | ||
if (props[name] != null) { | ||
key = props[name]; | ||
// NOTE: We have to make sure we don’t assign null to the key because | ||
// we don’t check for null keys in the diffing functions. | ||
if (props["crank-key"] != null) { | ||
key = props["crank-key"]; | ||
} | ||
@@ -137,3 +165,3 @@ } | ||
if (typeof props["crank-ref"] === "function") { | ||
ref = props[name]; | ||
ref = props["crank-ref"]; | ||
} | ||
@@ -146,13 +174,8 @@ } | ||
} | ||
let length = arguments.length; | ||
if (length > 3) { | ||
const children1 = []; | ||
while (length-- > 2) { | ||
children1[length - 2] = arguments[length]; | ||
} | ||
props1.children = children1; | ||
} | ||
else if (length > 2) { | ||
if (children.length > 1) { | ||
props1.children = children; | ||
} | ||
else if (children.length === 1) { | ||
props1.children = children[0]; | ||
} | ||
return new Element(tag, props1, key, ref); | ||
@@ -164,3 +187,5 @@ } | ||
* @remarks | ||
* Mainly used internally to make sure we don’t accidentally reuse elements in an element tree, because element have internal properties which are directly mutated by the renderer. | ||
* Mainly used internally to make sure we don’t accidentally reuse elements in | ||
* an element tree, because element have internal properties which are directly | ||
* mutated by the renderer. | ||
*/ | ||
@@ -173,8 +198,24 @@ function cloneElement(el) { | ||
} | ||
function narrow(value) { | ||
if (typeof value === "boolean" || value == null) { | ||
return undefined; | ||
} | ||
else if (typeof value === "string" || isElement(value)) { | ||
return value; | ||
} | ||
else if (typeof value[Symbol.iterator] === "function") { | ||
return createElement(Fragment, null, value); | ||
} | ||
return value.toString(); | ||
} | ||
/** | ||
* Takes an array of element values and normalizes the output as an array of nodes and strings. | ||
* Takes an array of element values and normalizes the output as an array of | ||
* nodes and strings. | ||
* | ||
* @returns Normalized array of nodes and/or strings. | ||
* | ||
* @remarks | ||
* Normalize will flatten only one level of nested arrays, because it is designed to be called once at each level of the tree. It will also concatenate adjacent strings and remove all undefined values. | ||
* Normalize will flatten only one level of nested arrays, because it is | ||
* designed to be called once at each level of the tree. It will also | ||
* concatenate adjacent strings and remove all undefined values. | ||
*/ | ||
@@ -190,3 +231,10 @@ function normalize(values) { | ||
} | ||
else if (Array.isArray(value)) { | ||
else if (!Array.isArray(value)) { | ||
if (buffer) { | ||
result.push(buffer); | ||
buffer = undefined; | ||
} | ||
result.push(value); | ||
} | ||
else { | ||
// We could use recursion here but it’s just easier to do it inline. | ||
@@ -208,10 +256,2 @@ for (let j = 0; j < value.length; j++) { | ||
} | ||
else { | ||
// value is of type TNode | ||
if (buffer) { | ||
result.push(buffer); | ||
buffer = undefined; | ||
} | ||
result.push(value); | ||
} | ||
} | ||
@@ -228,4 +268,7 @@ if (buffer) { | ||
function getValue(el) { | ||
if (typeof el._fb !== "undefined") { | ||
return getValue(el._fb); | ||
if (el._fb) { | ||
if (typeof el._fb === "object") { | ||
return getValue(el._fb); | ||
} | ||
return el._fb; | ||
} | ||
@@ -242,2 +285,3 @@ else if (el.tag === Portal) { | ||
* Walks an element’s children to find its child values. | ||
* | ||
* @returns A normalized array of nodes and strings. | ||
@@ -250,8 +294,5 @@ */ | ||
const child = children[i]; | ||
if (typeof child === "string") { | ||
values.push(child); | ||
if (child) { | ||
values.push(typeof child === "string" ? child : getValue(child)); | ||
} | ||
else if (typeof child !== "undefined") { | ||
values.push(getValue(child)); | ||
} | ||
} | ||
@@ -261,8 +302,11 @@ return normalize(values); | ||
/** | ||
* An abstract class which is subclassed to render to different target environments. This class is responsible for kicking off the rendering process, caching previous trees by root, and creating/mutating/disposing the nodes of the target environment. | ||
* An abstract class which is subclassed to render to different target | ||
* environments. This class is responsible for kicking off the rendering | ||
* process, caching previous trees by root, and creating, mutating and | ||
* disposing of nodes. | ||
* | ||
* @typeparam TNode - The type of the node for a specific rendering environment. | ||
* @typeparam TNode - The type of the node for a rendering environment. | ||
* @typeparam TScope - Data which is passed down the tree. | ||
* @typeparam TRoot - The type of the root for a specific rendering environment. | ||
* @typeparam TResult - The type of the exposed values. | ||
* @typeparam TRoot - The type of the root for a rendering environment. | ||
* @typeparam TResult - The type of exposed values. | ||
*/ | ||
@@ -276,7 +320,13 @@ class Renderer { | ||
* | ||
* @param children - An element tree. You can render null with a previously used root to delete the previously rendered element tree from the cache. | ||
* @param root - The node to be rendered into. The renderer will cache element trees per root. | ||
* @param ctx - An optional context that will be the ancestor context of all elements in the tree. Useful for connecting renderers which call each other so that events/provisions properly propagate. The context for a given root must be the same or an error will be thrown. | ||
* @param children - An element tree. You can render null with a previously | ||
* used root to delete the previously rendered element tree from the cache. | ||
* @param root - The node to be rendered into. The renderer will cache | ||
* element trees per root. | ||
* @param ctx - An optional context that will be the ancestor context of all | ||
* elements in the tree. Useful for connecting renderers which call each | ||
* other so that events/provisions properly propagate. The context for a | ||
* given root must be the same or an error will be thrown. | ||
* | ||
* @returns The read result of rendering the children, or a possible promise of the read result if the element tree renders asynchronously. | ||
* @returns The result of rendering the children, or a possible promise of | ||
* the result if the element tree renders asynchronously. | ||
*/ | ||
@@ -305,3 +355,4 @@ render(children, root, ctx) { | ||
const value = update(this, root, portal, ctx, undefined, portal); | ||
// NOTE: we return the read child values of the portal because portals themselves have no readable value. | ||
// NOTE: We return the child values of the portal because portal elements | ||
// themselves have no readable value. | ||
if (isPromiseLike(value)) { | ||
@@ -323,9 +374,16 @@ return value.then(() => { | ||
/** | ||
* Called when an element’s value is exposed via render, schedule, refresh, refs, or generator yield expressions. | ||
* Called when an element’s value is exposed via render, schedule, refresh, | ||
* refs, or generator yield expressions. | ||
* | ||
* @param value - The value of the element being read. Can be a node, a string, undefined, or an array of nodes and strings, depending on the element. | ||
* @returns Varies according to the specific renderer subclass. By default, it exposes the element’s value. | ||
* @param value - The value of the element being read. Can be a node, a | ||
* string, undefined, or an array of nodes and strings, depending on the | ||
* element. | ||
* | ||
* @returns Varies according to the specific renderer subclass. By default, | ||
* it exposes the element’s value. | ||
* | ||
* @remarks | ||
* This is useful for renderers which don’t want to expose their internal nodes. For instance, the HTML renderer will convert all internal nodes to strings. | ||
* This is useful for renderers which don’t want to expose their internal | ||
* nodes. For instance, the HTML renderer will convert all internal nodes to | ||
* strings. | ||
* | ||
@@ -340,3 +398,5 @@ */ | ||
* @remarks | ||
* Useful for passing data down the element tree. For instance, the DOM renderer uses this method to keep track of whether we’re in an SVG subtree. | ||
* Useful for passing data down the element tree. For instance, the DOM | ||
* renderer uses this method to keep track of whether we’re in an SVG | ||
* subtree. | ||
* | ||
@@ -346,6 +406,8 @@ * @param el - The host element. | ||
* | ||
* @returns The scope to be passed to create and scope for child host elements. | ||
* @returns The scope to be passed to create and scope for child host | ||
* elements. | ||
* | ||
* @remarks | ||
* This method sets the scope for child host elements, not the current host element. | ||
* This method sets the scope for child host elements, not the current host | ||
* element. | ||
*/ | ||
@@ -364,3 +426,6 @@ scope(el, scope) { | ||
* @remarks | ||
* Rather than returning text nodes for whatever environment we’re rendering to, we defer that step for Renderer.prototype.arrange. We do this so that adjacent strings can be concatenated and the actual element tree can be rendered in a normalized form. | ||
* Rather than returning text nodes for whatever environment we’re rendering | ||
* to, we defer that step for Renderer.prototype.arrange. We do this so that | ||
* adjacent strings can be concatenated and the actual element tree can be | ||
* rendered in a normalized form. | ||
*/ | ||
@@ -397,3 +462,2 @@ escape(text, _scope) { | ||
* @param node - The node associated with the host element. | ||
* @param scope - The current scope. | ||
* | ||
@@ -403,3 +467,4 @@ * @returns The return value is ignored. | ||
* @remarks | ||
* Used to mutate the node associated with an element when new props are passed. | ||
* Used to mutate the node associated with an element when new props are | ||
* passed. | ||
*/ | ||
@@ -411,3 +476,3 @@ patch(_el, _node) { | ||
/** | ||
* Called for each host element after its children have committed with the actual values of the children. | ||
* Called for each host element so that elements can be arranged into a tree. | ||
* | ||
@@ -421,13 +486,14 @@ * @param el - The host element. | ||
* @remarks | ||
* This method is also called by child components contexts as the last step of a refresh. | ||
* This method is also called by child components contexts as the last step | ||
* of a refresh. | ||
*/ | ||
arrange(_el, _parent, _children) { | ||
arrange(_el, _node, _children) { | ||
return; | ||
} | ||
// TODO: remove(): a method which is called to remove a child from a parent to optimize arrange | ||
// TODO: remove(): a method which is called to remove a child from a parent | ||
// to optimize arrange | ||
/** | ||
* Called for each host element when it is unmounted. | ||
* | ||
* @param tag - The tag of the host element. | ||
* @param props - The props of the host element. | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
@@ -452,4 +518,55 @@ * | ||
/*** PRIVATE RENDERER FUNCTIONS ***/ | ||
function diff(renderer, root, host, ctx, scope, oldChild, newChild) { | ||
let value; | ||
if (typeof oldChild === "object" && | ||
typeof newChild === "object" && | ||
oldChild.tag === newChild.tag) { | ||
// TODO: implement Raw element parse caching | ||
if (oldChild.tag === Portal) { | ||
if (oldChild.props.root !== newChild.props.root) { | ||
renderer.arrange(oldChild, oldChild.props.root, []); | ||
} | ||
} | ||
if (oldChild !== newChild) { | ||
oldChild.props = newChild.props; | ||
oldChild.ref = newChild.ref; | ||
newChild = oldChild; | ||
} | ||
value = update(renderer, root, host, ctx, scope, newChild); | ||
} | ||
else if (typeof newChild === "object") { | ||
if (newChild.tag === Copy) { | ||
if (typeof oldChild === "object") { | ||
value = oldChild._inf || getValue(oldChild); | ||
} | ||
else { | ||
value = oldChild; | ||
} | ||
if (typeof newChild.ref === "function") { | ||
if (isPromiseLike(value)) { | ||
value.then(newChild.ref).catch(NOOP); | ||
} | ||
else { | ||
newChild.ref(value); | ||
} | ||
} | ||
newChild = oldChild; | ||
} | ||
else { | ||
if (newChild._f & IsMounted) { | ||
newChild = cloneElement(newChild); | ||
} | ||
value = mount(renderer, root, host, ctx, scope, newChild); | ||
if (isPromiseLike(value)) { | ||
newChild._fb = oldChild; | ||
} | ||
} | ||
} | ||
else if (typeof newChild === "string") { | ||
value = renderer.escape(newChild, scope); | ||
} | ||
return [newChild, value]; | ||
} | ||
function mount(renderer, root, host, ctx, scope, el) { | ||
el._f |= Mounted; | ||
el._f |= IsMounted; | ||
if (typeof el.tag === "function") { | ||
@@ -469,22 +586,11 @@ el._ctx = new Context(renderer, root, host, ctx, scope, el); | ||
} | ||
// NOTE: The primary benefit of having a separate codepath for mounting is that it’s slightly faster because we don’t have to align and diff children against old children. But for singular child values, updateChild is sufficient. | ||
if (isNonStringIterable(el.props.children)) { | ||
return mountChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
} | ||
return updateChild(renderer, root, host, ctx, scope, el, el.props.children); | ||
return mountChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
} | ||
function mountChildren(renderer, root, host, ctx, scope, parent, children) { | ||
function mountChildren(renderer, root, host, ctx, scope, el, children) { | ||
const newChildren = arrayify(children); | ||
const values = []; | ||
const newChildren = Array.from(children); | ||
let async = false; | ||
let seen; | ||
for (let i = 0; i < newChildren.length; i++) { | ||
let value; | ||
let child = newChildren[i]; | ||
if (isNonStringIterable(child)) { | ||
child = createElement(Fragment, null, child); | ||
} | ||
else { | ||
child = narrow(child); | ||
} | ||
let child = narrow(newChildren[i]); | ||
if (typeof child === "object" && typeof child.key !== "undefined") { | ||
@@ -496,3 +602,2 @@ if (seen === undefined) { | ||
if (seen.has(child.key)) { | ||
// eslint-disable-next-line no-console | ||
console.error("Duplicate key", child.key); | ||
@@ -503,2 +608,3 @@ } | ||
} | ||
let value; | ||
[child, value] = diff(renderer, root, host, ctx, scope, undefined, // oldChild | ||
@@ -512,18 +618,26 @@ child); | ||
} | ||
parent._ch = unwrap(newChildren); | ||
let values1; | ||
el._ch = unwrap(newChildren); | ||
if (async) { | ||
values1 = Promise.all(values); | ||
let onvalues; | ||
const values1 = Promise.race([ | ||
Promise.all(values), | ||
new Promise((resolve) => (onvalues = resolve)), | ||
]); | ||
if (el._onv) { | ||
el._onv(values1); | ||
} | ||
el._onv = onvalues; | ||
el._inf = values1.then((values) => commit(renderer, scope, el, normalize(values))); | ||
return el._inf; | ||
} | ||
else { | ||
values1 = values; | ||
if (el._onv) { | ||
el._onv(values); | ||
el._onv = undefined; | ||
} | ||
return chase(renderer, host, ctx, scope, parent, values1); | ||
return commit(renderer, scope, el, normalize(values)); | ||
} | ||
function update(renderer, root, host, ctx, scope, el) { | ||
if (typeof el.tag === "function") { | ||
if (typeof el._ctx === "object") { | ||
return updateCtx(el._ctx); | ||
} | ||
return undefined; | ||
// el._ctx should probably never be undefined here | ||
return el._ctx ? updateCtx(el._ctx) : undefined; | ||
} | ||
@@ -540,31 +654,5 @@ else if (el.tag === Raw) { | ||
} | ||
if (isNonStringIterable(el.props.children)) { | ||
return updateChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
} | ||
else if (Array.isArray(el._ch)) { | ||
return updateChildren(renderer, root, host, ctx, scope, el, [ | ||
el.props.children, | ||
]); | ||
} | ||
return updateChild(renderer, root, host, ctx, scope, el, el.props.children); | ||
return updateChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
} | ||
function updateChild(renderer, root, host, ctx, scope, parent, child) { | ||
let oldChild = parent._ch; | ||
let newChild = narrow(child); | ||
if (typeof oldChild === "object" && | ||
typeof newChild === "object" && | ||
oldChild.key !== newChild.key) { | ||
oldChild = undefined; | ||
} | ||
let value; | ||
[newChild, value] = diff(renderer, root, host, ctx, scope, oldChild, newChild); | ||
if (typeof oldChild === "object" && oldChild !== newChild) { | ||
unmount(renderer, host, ctx, oldChild); | ||
} | ||
parent._ch = newChild; | ||
// TODO: allow single values to be passed to chase | ||
const values = isPromiseLike(value) ? value.then(wrap) : wrap(value); | ||
return chase(renderer, host, ctx, scope, parent, values); | ||
} | ||
function mapChildrenByKey(children) { | ||
function createChildrenByKey(children) { | ||
const childrenByKey = new Map(); | ||
@@ -579,9 +667,9 @@ for (let i = 0; i < children.length; i++) { | ||
} | ||
function updateChildren(renderer, root, host, ctx, scope, parent, children) { | ||
if (typeof parent._ch === "undefined") { | ||
return mountChildren(renderer, root, host, ctx, scope, parent, children); | ||
function updateChildren(renderer, root, host, ctx, scope, el, children) { | ||
if (typeof el._ch === "undefined") { | ||
return mountChildren(renderer, root, host, ctx, scope, el, children); | ||
} | ||
const oldChildren = wrap(el._ch); | ||
const newChildren = arrayify(children); | ||
const values = []; | ||
const oldChildren = wrap(parent._ch); | ||
const newChildren = Array.from(children); | ||
const graveyard = []; | ||
@@ -595,9 +683,3 @@ let i = 0; | ||
let oldChild = oldChildren[i]; | ||
let newChild = newChildren[j]; | ||
if (isNonStringIterable(newChild)) { | ||
newChild = createElement(Fragment, null, newChild); | ||
} | ||
else { | ||
newChild = narrow(newChild); | ||
} | ||
let newChild = narrow(newChildren[j]); | ||
// ALIGNMENT | ||
@@ -607,3 +689,2 @@ let oldKey = typeof oldChild === "object" ? oldChild.key : undefined; | ||
if (seen !== undefined && seen.has(newKey)) { | ||
// eslint-disable-next-line no-console | ||
console.error("Duplicate key", newKey); | ||
@@ -614,3 +695,3 @@ newKey = undefined; | ||
if (!childrenByKey) { | ||
childrenByKey = mapChildrenByKey(oldChildren.slice(i)); | ||
childrenByKey = createChildrenByKey(oldChildren.slice(i)); | ||
} | ||
@@ -654,3 +735,3 @@ if (newKey === undefined) { | ||
} | ||
parent._ch = unwrap(newChildren); | ||
el._ch = unwrap(newChildren); | ||
// cleanup | ||
@@ -667,84 +748,20 @@ for (; i < oldChildren.length; i++) { | ||
} | ||
let values1; | ||
if (async) { | ||
values1 = Promise.all(values).finally(() => graveyard.forEach((child) => unmount(renderer, host, ctx, child))); | ||
} | ||
else { | ||
values1 = values; | ||
graveyard.forEach((child) => unmount(renderer, host, ctx, child)); | ||
} | ||
return chase(renderer, host, ctx, scope, parent, values1); | ||
} | ||
function diff(renderer, root, host, ctx, scope, oldChild, newChild) { | ||
let value; | ||
if (typeof oldChild === "object" && | ||
typeof newChild === "object" && | ||
oldChild.tag === newChild.tag) { | ||
// TODO: implement Raw element parse caching | ||
if (oldChild.tag === Portal) { | ||
if (oldChild.props.root !== newChild.props.root) { | ||
renderer.arrange(oldChild, oldChild.props.root, []); | ||
} | ||
let values1 = Promise.all(values).finally(() => { | ||
graveyard.forEach((child) => unmount(renderer, host, ctx, child)); | ||
}); | ||
let onvalues; | ||
values1 = Promise.race([ | ||
values1, | ||
new Promise((resolve) => (onvalues = resolve)), | ||
]); | ||
if (el._onv) { | ||
el._onv(values1); | ||
} | ||
if (oldChild !== newChild) { | ||
oldChild.props = newChild.props; | ||
oldChild.ref = newChild.ref; | ||
newChild = oldChild; | ||
} | ||
value = update(renderer, root, host, ctx, scope, newChild); | ||
el._onv = onvalues; | ||
el._inf = values1.then((values) => commit(renderer, scope, el, normalize(values))); | ||
return el._inf; | ||
} | ||
else if (typeof newChild === "object") { | ||
if (newChild.tag === Copy) { | ||
if (typeof oldChild === "object") { | ||
value = oldChild._inf || getValue(oldChild); | ||
} | ||
else { | ||
value = oldChild; | ||
} | ||
if (typeof newChild.ref === "function") { | ||
if (isPromiseLike(value)) { | ||
value.then(newChild.ref).catch(NOOP); | ||
} | ||
else { | ||
newChild.ref(value); | ||
} | ||
} | ||
newChild = oldChild; | ||
} | ||
else { | ||
if (newChild._f & Mounted) { | ||
newChild = cloneElement(newChild); | ||
} | ||
value = mount(renderer, root, host, ctx, scope, newChild); | ||
if (typeof oldChild === "object" && isPromiseLike(value)) { | ||
newChild._fb = oldChild; | ||
} | ||
} | ||
} | ||
else if (typeof newChild === "string") { | ||
newChild = renderer.escape(newChild, scope); | ||
value = newChild; | ||
} | ||
return [newChild, value]; | ||
} | ||
/** | ||
* A function to race current child values with future child values. | ||
* | ||
* @remarks | ||
* When an element’s children update asynchronously, we race the resulting promise with the next update of the element’s children. By induction, this ensures that when any update to an element settles, all past updates to that same element will have settled as well. This prevents deadlocks and unnecessary awaiting when an element’s children have been cleared, for instance. | ||
*/ | ||
function chase(renderer, host, ctx, scope, el, values) { | ||
if (isPromiseLike(values)) { | ||
let onNewValues; | ||
const newValues = new Promise((resolve) => (onNewValues = resolve)); | ||
const valuesP = Promise.race([values, newValues]); | ||
if (typeof el._onv === "function") { | ||
el._onv(valuesP); | ||
} | ||
el._onv = onNewValues; | ||
const value = valuesP.then((values) => commit(renderer, scope, el, normalize(values))); | ||
el._inf = value; | ||
return value; | ||
} | ||
if (typeof el._onv === "function") { | ||
graveyard.forEach((child) => unmount(renderer, host, ctx, child)); | ||
if (el._onv) { | ||
el._onv(values); | ||
@@ -763,2 +780,5 @@ el._onv = undefined; | ||
else if (el.tag === Portal) { | ||
if (!(el._f & IsCommitted)) { | ||
el._f |= IsCommitted; | ||
} | ||
renderer.arrange(el, el.props.root, values); | ||
@@ -778,4 +798,5 @@ renderer.complete(el.props.root); | ||
else if (el.tag !== Fragment) { | ||
if (!(el._f & Committed)) { | ||
if (!(el._f & IsCommitted)) { | ||
el._n = renderer.create(el, scope); | ||
el._f |= IsCommitted; | ||
} | ||
@@ -786,10 +807,9 @@ renderer.patch(el, el._n); | ||
} | ||
el._f |= Committed; | ||
if (typeof el.ref === "function") { | ||
if (el.ref) { | ||
el.ref(renderer.read(value)); | ||
} | ||
if (typeof el._inf !== "undefined") { | ||
if (el._inf) { | ||
el._inf = undefined; | ||
} | ||
if (typeof el._fb !== "undefined") { | ||
if (el._fb) { | ||
el._fb = undefined; | ||
@@ -809,3 +829,3 @@ } | ||
renderer.arrange(host, host.props.root, []); | ||
renderer.complete(el.props.root); | ||
renderer.complete(host.props.root); | ||
} | ||
@@ -833,81 +853,39 @@ else if (el.tag !== Fragment) { | ||
} | ||
/*** EVENT UTILITIES ***/ | ||
// EVENT PHASE CONSTANTS (https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase) | ||
const NONE = 0; | ||
const CAPTURING_PHASE = 1; | ||
const AT_TARGET = 2; | ||
const BUBBLING_PHASE = 3; | ||
function normalizeOptions(options) { | ||
if (typeof options === "boolean") { | ||
return { capture: options }; | ||
} | ||
else if (options == null) { | ||
return {}; | ||
} | ||
else { | ||
return options; | ||
} | ||
} | ||
function isEventTarget(value) { | ||
return (value != null && | ||
typeof value.addEventListener === "function" && | ||
typeof value.removeEventListener === "function" && | ||
typeof value.dispatchEvent === "function"); | ||
} | ||
function setEventProperty(ev, key, value) { | ||
Object.defineProperty(ev, key, { value, writable: false, configurable: true }); | ||
} | ||
/** | ||
* A function to reconstruct an array of every listener given a context and a host element. | ||
* | ||
* @remarks | ||
* This function exploits the fact that contexts retain their nearest ancestor host element. We can determine all the contexts which are directly listening to an element by traversing up the context tree and checking that the host element passed in matches the context’s host property. | ||
*/ | ||
function getListeners(ctx, host) { | ||
let listeners; | ||
while (ctx !== undefined && ctx._ho === host) { | ||
if (typeof ctx._ls !== "undefined") { | ||
listeners = (listeners || []).concat(ctx._ls); | ||
} | ||
ctx = ctx._pa; | ||
} | ||
return listeners; | ||
} | ||
function clearEventListeners(ctx) { | ||
if (typeof ctx._ls !== "undefined" && ctx._ls.length > 0) { | ||
for (const value of getChildValues(ctx._el)) { | ||
if (isEventTarget(value)) { | ||
for (const record of ctx._ls) { | ||
value.removeEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
} | ||
ctx._ls = undefined; | ||
} | ||
} | ||
// CONTEXT FLAGS | ||
/** | ||
* A flag which is set when the component is being updated by the parent and cleared when the component has committed. Used to determine whether the nearest host ancestor needs to be rearranged. | ||
* A flag which is set when the component is being updated by the parent and | ||
* cleared when the component has committed. Used to determine whether the | ||
* nearest host ancestor needs to be rearranged. | ||
*/ | ||
const Updating = 1 << 0; | ||
const IsUpdating = 1 << 0; | ||
/** | ||
* A flag which is set when the component function is called or the component generator is resumed. This flags is used to ensure that a component which synchronously triggers a second update in the course of rendering does not cause an stack overflow or a generator error. | ||
* A flag which is set when the component function is called or the component | ||
* generator is resumed. This flags is used to ensure that a component which | ||
* synchronously triggers a second update in the course of rendering does not | ||
* cause an stack overflow or a generator error. | ||
*/ | ||
const Executing = 1 << 1; | ||
const IsExecuting = 1 << 1; | ||
/** | ||
* A flag used to make sure multiple values are not pulled from context prop iterators without a yield. | ||
* A flag used to make sure multiple values are not pulled from context prop | ||
* iterators without a yield. | ||
*/ | ||
const Iterating = 1 << 2; | ||
const IsIterating = 1 << 2; | ||
/** | ||
* A flag used by async generator components in conjunction with the onProps functions (_op) to mark whether new props can be pulled via the context iterator methods. | ||
* A flag used by async generator components in conjunction with the | ||
* onIsAvailable (_oa) callback to mark whether new props can be pulled via the | ||
* context async iterator. | ||
*/ | ||
const Available = 1 << 3; | ||
const IsAvailable = 1 << 3; | ||
/** | ||
* A flag which is set when generator components return. Set whenever an iterator returns an iteration with the done property set to true or throws. Done components will stick to their last rendered value and ignore further updates. | ||
* A flag which is set when generator components return. Set whenever an | ||
* iterator returns an iteration with the done property set to true or throws. | ||
* Done components will stick to their last rendered value and ignore further | ||
* updates. | ||
*/ | ||
const Done = 1 << 4; | ||
const IsDone = 1 << 4; | ||
/** | ||
* A flag which is set when the component is unmounted. Unmounted components are no longer in the element tree, and cannot run or refresh. | ||
* A flag which is set when the component is unmounted. Unmounted components | ||
* are no longer in the element tree, and cannot run or refresh. | ||
*/ | ||
const Unmounted = 1 << 5; | ||
const IsUnmounted = 1 << 5; | ||
/** | ||
@@ -922,5 +900,12 @@ * A flag which indicates that the component is a sync generator component. | ||
/** | ||
* A class which is instantiated and passed to every component as its this value. Contexts form a tree just like elements and all components in the element tree are connected via contexts. Components can use this tree to communicate data upwards via events and downwards via provisions. | ||
* @typeparam TProps - The expected shape of the props passed to the component. Used to strongly type the Context iterator methods. | ||
* @typeparam TResult - The readable element value type. It is used in places such as the return value of refresh and the argument passed to schedule/cleanup callbacks. | ||
* A class which is instantiated and passed to every component as its this | ||
* value. Contexts form a tree just like elements and all components in the | ||
* element tree are connected via contexts. Components can use this tree to | ||
* communicate data upwards via events and downwards via provisions. | ||
* | ||
* @typeparam TProps - The expected shape of the props passed to the component. | ||
* Used to strongly type the Context iterator methods. | ||
* @typeparam TResult - The readable element value type. It is used in places | ||
* such as the return value of refresh and the argument passed to | ||
* schedule/cleanup callbacks. | ||
*/ | ||
@@ -940,15 +925,2 @@ class Context { | ||
} | ||
consume(key) { | ||
for (let parent = this._pa; parent !== undefined; parent = parent._pa) { | ||
if (typeof parent._ps === "object" && parent._ps.has(key)) { | ||
return parent._ps.get(key); | ||
} | ||
} | ||
} | ||
provide(key, value) { | ||
if (typeof this._ps === "undefined") { | ||
this._ps = new Map(); | ||
} | ||
this._ps.set(key, value); | ||
} | ||
/** | ||
@@ -958,3 +930,5 @@ * The current props of the associated element. | ||
* @remarks | ||
* Typically, you should read props either via the first parameter of the component or via the context iterator methods. This property is mainly for plugins or utilities which wrap contexts. | ||
* Typically, you should read props either via the first parameter of the | ||
* component or via the context iterator methods. This property is mainly for | ||
* plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -968,3 +942,5 @@ get props() { | ||
* @remarks | ||
* Typically, you should read values via refs, generator yield expressions, or the refresh, schedule or cleanup methods. This property is mainly for plugins or utilities which wrap contexts. | ||
* Typically, you should read values via refs, generator yield expressions, | ||
* or the refresh, schedule or cleanup methods. This property is mainly for | ||
* plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -975,5 +951,4 @@ get value() { | ||
*[Symbol.iterator]() { | ||
const el = this._el; | ||
while (!(this._f & Unmounted)) { | ||
if (this._f & Iterating) { | ||
while (!(this._f & IsDone)) { | ||
if (this._f & IsIterating) { | ||
throw new Error("Context iterated twice without a yield"); | ||
@@ -984,10 +959,11 @@ } | ||
} | ||
this._f |= Iterating; | ||
yield el.props; | ||
this._f |= IsIterating; | ||
yield this._el.props; | ||
} | ||
} | ||
async *[Symbol.asyncIterator]() { | ||
const el = this._el; | ||
// We use a do while loop rather than a while loop to handle an edge case | ||
// where an async generator component is unmounted synchronously. | ||
do { | ||
if (this._f & Iterating) { | ||
if (this._f & IsIterating) { | ||
throw new Error("Context iterated twice without a yield"); | ||
@@ -998,14 +974,14 @@ } | ||
} | ||
this._f |= Iterating; | ||
if (this._f & Available) { | ||
this._f &= ~Available; | ||
yield el.props; | ||
this._f |= IsIterating; | ||
if (this._f & IsAvailable) { | ||
this._f &= ~IsAvailable; | ||
} | ||
else { | ||
const props = await new Promise((resolve) => (this._op = resolve)); | ||
if (!(this._f & Unmounted)) { | ||
yield props; | ||
await new Promise((resolve) => (this._oa = resolve)); | ||
if (this._f & IsDone) { | ||
break; | ||
} | ||
} | ||
} while (!(this._f & Unmounted)); | ||
yield this._el.props; | ||
} while (!(this._f & IsDone)); | ||
} | ||
@@ -1015,27 +991,29 @@ /** | ||
* | ||
* @returns The rendered value of the component or a promise of the rendered value if the component or its children execute asynchronously. | ||
* @returns The rendered value of the component or a promise of the rendered | ||
* value if the component or its children execute asynchronously. | ||
* | ||
* @remarks | ||
* The refresh method works a little differently for async generator components, in that it will resume the Context async iterator rather than resuming execution. This is because async generator components are perpetually resumed independent of updates/refresh. | ||
* The refresh method works a little differently for async generator | ||
* components, in that it will resume the Context async iterator rather than | ||
* resuming execution. This is because async generator components are | ||
* perpetually resumed independent of updates/refresh. | ||
*/ | ||
refresh() { | ||
if (this._f & Unmounted) { | ||
// eslint-disable-next-line no-console | ||
if (this._f & IsUnmounted) { | ||
console.error("Component is unmounted"); | ||
return this._re.read(undefined); | ||
} | ||
else if (this._f & Executing) { | ||
// eslint-disable-next-line no-console | ||
else if (this._f & IsExecuting) { | ||
console.error("Component is already executing"); | ||
return this._re.read(undefined); | ||
} | ||
this._f &= ~Updating; | ||
resume(this); | ||
return this._re.read(run(this)); | ||
resumeCtx(this); | ||
return this._re.read(runCtx(this)); | ||
} | ||
/** | ||
* Registers a callback which fires when the component commits. Will only fire once per callback and update. | ||
* Registers a callback which fires when the component commits. Will only | ||
* fire once per callback and update. | ||
*/ | ||
schedule(callback) { | ||
if (typeof this._ss === "undefined") { | ||
if (!this._ss) { | ||
this._ss = new Set(); | ||
@@ -1046,6 +1024,7 @@ } | ||
/** | ||
* Registers a callback which fires when the component unmounts. Will only fire once per callback. | ||
* Registers a callback which fires when the component unmounts. Will only | ||
* fire once per callback. | ||
*/ | ||
cleanup(callback) { | ||
if (typeof this._cs === "undefined") { | ||
if (!this._cs) { | ||
this._cs = new Set(); | ||
@@ -1055,2 +1034,15 @@ } | ||
} | ||
consume(key) { | ||
for (let parent = this._pa; parent !== undefined; parent = parent._pa) { | ||
if (parent._ps && parent._ps.has(key)) { | ||
return parent._ps.get(key); | ||
} | ||
} | ||
} | ||
provide(key, value) { | ||
if (!this._ps) { | ||
this._ps = new Map(); | ||
} | ||
this._ps.set(key, value); | ||
} | ||
addEventListener(type, listener, options) { | ||
@@ -1060,3 +1052,3 @@ if (listener == null) { | ||
} | ||
else if (typeof this._ls === "undefined") { | ||
else if (!this._ls) { | ||
this._ls = []; | ||
@@ -1076,3 +1068,3 @@ } | ||
record.callback = function () { | ||
if (typeof self._ls !== "undefined") { | ||
if (self._ls) { | ||
self._ls = self._ls.filter((record1) => record !== record1); | ||
@@ -1099,3 +1091,3 @@ if (self._ls.length === 0) { | ||
removeEventListener(type, listener, options) { | ||
if (listener == null || typeof this._ls === "undefined") { | ||
if (listener == null || !this._ls) { | ||
return; | ||
@@ -1112,2 +1104,5 @@ } | ||
this._ls.splice(i, 1); | ||
if (this._ls.length === 0) { | ||
this._ls = undefined; | ||
} | ||
for (const value of getChildValues(this._el)) { | ||
@@ -1118,5 +1113,2 @@ if (isEventTarget(value)) { | ||
} | ||
if (this._ls.length === 0) { | ||
this._ls = undefined; | ||
} | ||
} | ||
@@ -1128,35 +1120,36 @@ dispatchEvent(ev) { | ||
} | ||
let stopped = false; | ||
// We patch the stopImmediatePropagation method because ev.cancelBubble | ||
// only informs us if stopPropagation was called. | ||
let immediateCancelBubble = false; | ||
const stopImmediatePropagation = ev.stopImmediatePropagation; | ||
setEventProperty(ev, "stopImmediatePropagation", () => { | ||
stopped = true; | ||
immediateCancelBubble = true; | ||
return stopImmediatePropagation.call(ev); | ||
}); | ||
setEventProperty(ev, "target", this); | ||
setEventProperty(ev, "eventPhase", CAPTURING_PHASE); | ||
// The only possible errors in this block are errors thrown by listener | ||
// callbacks, and dispatchEvent will only log the error rather than | ||
// rethrowing it. We return true because the return value is overridden in | ||
// the finally block but TypeScript (justifiably) does not recognize the | ||
// unsafe return statement. | ||
try { | ||
setEventProperty(ev, "eventPhase", CAPTURING_PHASE); | ||
for (let i = path.length - 1; i >= 0; i--) { | ||
const et = path[i]; | ||
if (typeof et._ls !== "undefined") { | ||
if (et._ls) { | ||
setEventProperty(ev, "currentTarget", et); | ||
for (const record of et._ls) { | ||
if (record.type === ev.type && record.options.capture) { | ||
try { | ||
record.callback.call(this, ev); | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
catch (err) { | ||
// eslint-disable-next-line no-console | ||
console.error(err); | ||
} | ||
if (stopped) { | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
if (stopped || ev.cancelBubble) { | ||
return !ev.defaultPrevented; | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
} | ||
if (typeof this._ls !== "undefined") { | ||
if (this._ls) { | ||
setEventProperty(ev, "eventPhase", AT_TARGET); | ||
@@ -1166,16 +1159,10 @@ setEventProperty(ev, "currentTarget", this); | ||
if (record.type === ev.type) { | ||
try { | ||
record.callback.call(this, ev); | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
catch (err) { | ||
// eslint-disable-next-line no-console | ||
console.error(err); | ||
} | ||
if (stopped) { | ||
break; | ||
} | ||
} | ||
} | ||
if (stopped || ev.cancelBubble) { | ||
return !ev.defaultPrevented; | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
@@ -1186,29 +1173,27 @@ } | ||
for (const et of path) { | ||
if (typeof et._ls !== "undefined") { | ||
if (et._ls) { | ||
setEventProperty(ev, "currentTarget", et); | ||
for (const record of et._ls) { | ||
if (record.type === ev.type && !record.options.capture) { | ||
try { | ||
record.callback.call(this, ev); | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
catch (err) { | ||
// eslint-disable-next-line no-console | ||
console.error(err); | ||
} | ||
if (stopped) { | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
if (stopped || ev.cancelBubble) { | ||
return !ev.defaultPrevented; | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
return !ev.defaultPrevented; | ||
} | ||
catch (err) { | ||
console.error(err); | ||
} | ||
finally { | ||
setEventProperty(ev, "eventPhase", NONE); | ||
setEventProperty(ev, "currentTarget", null); | ||
// eslint-disable-next-line no-unsafe-finally | ||
return !ev.defaultPrevented; | ||
} | ||
@@ -1218,99 +1203,43 @@ } | ||
/*** PRIVATE CONTEXT FUNCTIONS ***/ | ||
/** | ||
* Called to make props available to the Context async iterator for async generator components. | ||
/* | ||
* NOTE: The functions stepCtx, advanceCtx and runCtx work together to | ||
* implement the async queueing behavior of components. The runCtx function | ||
* calls the stepCtx function, which returns two results in a tuple. The first | ||
* result, called the “block,” is a possible promise which represents the | ||
* duration for which the component is blocked from accepting new updates. The | ||
* second result, called the “value,” is the actual result of the update. The | ||
* runCtx function caches block/value from the stepCtx function on the context, | ||
* according to whether the component is currently blocked. The “inflight” | ||
* block/value properties are the currently executing update, and the | ||
* “enqueued” block/value properties represent an enqueued next stepCtx. | ||
* Enqueued steps are dequeued in a finally callback on the blocking promise. | ||
*/ | ||
function resume(ctx) { | ||
if (typeof ctx._op === "function") { | ||
ctx._op(ctx._el.props); | ||
ctx._op = undefined; | ||
} | ||
else { | ||
ctx._f |= Available; | ||
} | ||
} | ||
// NOTE: The functions run, step and advance work together to implement the async queueing behavior of components. The run function calls the step function, which returns two results in a tuple. The first result, called “block,” is a possible promise which represents the duration for which the component is blocked from accepting new updates. The second result, called “value,” is the actual result of the update. The run function caches block/value from the step function on the context, according to whether the component is currently blocked. The “inflight” block/value properties are the currently executing update, and the “enqueued” block/value properties represent an enqueued next step. Enqueued steps are dequeed in a finally callback on the inflight block. | ||
/** | ||
* Enqueues and executes the component associated with the context. | ||
*/ | ||
function run(ctx) { | ||
if (typeof ctx._ib === "undefined") { | ||
try { | ||
let [block, value] = step(ctx); | ||
if (isPromiseLike(block)) { | ||
ctx._ib = block | ||
.catch((err) => { | ||
if (!(ctx._f & Updating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}) | ||
.finally(() => advance(ctx)); | ||
} | ||
if (isPromiseLike(value)) { | ||
ctx._iv = value; | ||
ctx._el._inf = value; | ||
} | ||
return value; | ||
} | ||
catch (err) { | ||
if (!(ctx._f & Updating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
throw err; | ||
} | ||
} | ||
else if (ctx._f & IsAsyncGen) { | ||
return ctx._iv; | ||
} | ||
else if (typeof ctx._eb === "undefined") { | ||
let resolve; | ||
ctx._eb = ctx._ib | ||
.then(() => { | ||
try { | ||
const [block, value] = step(ctx); | ||
resolve(value); | ||
if (isPromiseLike(value)) { | ||
ctx._el._inf = value; | ||
} | ||
if (isPromiseLike(block)) { | ||
return block.catch((err) => { | ||
if (!(ctx._f & Updating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}); | ||
} | ||
} | ||
catch (err) { | ||
if (!(ctx._f & Updating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
} | ||
}) | ||
.finally(() => advance(ctx)); | ||
ctx._ev = new Promise((resolve1) => (resolve = resolve1)); | ||
} | ||
return ctx._ev; | ||
} | ||
/** | ||
* The step function is responsible for executing the component and handling all the different component types. | ||
* This function is responsible for executing the component and handling all | ||
* the different component types. | ||
* | ||
* @returns A tuple [block, value] | ||
* block - A possible promise which represents the duration during which the component is blocked from updating. | ||
* block - A possible promise which represents the duration during which the | ||
* component is blocked from updating. | ||
* value - A possible promise resolving to the rendered value of children. | ||
* | ||
* @remarks | ||
* Each component type will block/unblock according to the type of the component. | ||
* Sync function components never block and will transparently pass updates to children. | ||
* Async function components and async generator components block while executing itself, but will not block for async children. | ||
* Sync generator components block while any children are executing, because they are expected to only resume when they’ve actually rendered. Additionally, they have no mechanism for awaiting async children. | ||
* Each component type will block according to the type of the component. | ||
* Sync function components never block and will transparently pass updates to | ||
* children. | ||
* Async function components and async generator components block while | ||
* executing itself, but will not block for async children. | ||
* Sync generator components block while any children are executing, because | ||
* they are expected to only resume when they’ve actually rendered. | ||
* Additionally, they have no mechanism for awaiting async children. | ||
*/ | ||
function step(ctx) { | ||
function stepCtx(ctx) { | ||
const el = ctx._el; | ||
if (ctx._f & Done) { | ||
if (ctx._f & IsDone) { | ||
return [undefined, getValue(el)]; | ||
} | ||
let initial = false; | ||
const initial = !ctx._it; | ||
try { | ||
ctx._f |= Executing; | ||
if (typeof ctx._it === "undefined") { | ||
initial = true; | ||
ctx._f |= IsExecuting; | ||
if (initial) { | ||
clearEventListeners(ctx); | ||
@@ -1335,6 +1264,6 @@ const result = el.tag.call(ctx, el.props); | ||
finally { | ||
ctx._f &= ~Executing; | ||
ctx._f &= ~IsExecuting; | ||
} | ||
let oldValue; | ||
if (typeof ctx._el._inf === "object") { | ||
if (ctx._el._inf) { | ||
oldValue = ctx._el._inf.then((value) => ctx._re.read(value), () => ctx._re.read(undefined)); | ||
@@ -1350,11 +1279,11 @@ } | ||
try { | ||
ctx._f |= Executing; | ||
ctx._f |= IsExecuting; | ||
iteration = ctx._it.next(oldValue); | ||
} | ||
catch (err) { | ||
ctx._f |= Done; | ||
ctx._f |= IsDone; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~Executing; | ||
ctx._f &= ~IsExecuting; | ||
} | ||
@@ -1368,8 +1297,8 @@ if (isPromiseLike(iteration)) { | ||
const value = iteration.then((iteration) => { | ||
if (!(ctx._f & Iterating)) { | ||
ctx._f &= ~Available; | ||
if (!(ctx._f & IsIterating)) { | ||
ctx._f &= ~IsAvailable; | ||
} | ||
ctx._f &= ~Iterating; | ||
ctx._f &= ~IsIterating; | ||
if (iteration.done) { | ||
ctx._f |= Done; | ||
ctx._f |= IsDone; | ||
} | ||
@@ -1387,3 +1316,3 @@ try { | ||
}, (err) => { | ||
ctx._f |= Done; | ||
ctx._f |= IsDone; | ||
throw err; | ||
@@ -1397,5 +1326,5 @@ }); | ||
} | ||
ctx._f &= ~Iterating; | ||
ctx._f &= ~IsIterating; | ||
if (iteration.done) { | ||
ctx._f |= Done; | ||
ctx._f |= IsDone; | ||
} | ||
@@ -1421,3 +1350,3 @@ let value; | ||
*/ | ||
function advance(ctx) { | ||
function advanceCtx(ctx) { | ||
// _ib: inflightBlock | ||
@@ -1431,79 +1360,94 @@ // _iv: inflightValue | ||
ctx._ev = undefined; | ||
if (ctx._f & IsAsyncGen && !(ctx._f & Done)) { | ||
run(ctx); | ||
if (ctx._f & IsAsyncGen && !(ctx._f & IsDone)) { | ||
runCtx(ctx); | ||
} | ||
} | ||
// TODO: generator components which throw errors should be recoverable | ||
function handleChildError(ctx, err) { | ||
if (ctx._f & Done || | ||
typeof ctx._it !== "object" || | ||
typeof ctx._it.throw !== "function") { | ||
throw err; | ||
/** | ||
* Enqueues and executes the component associated with the context. | ||
*/ | ||
function runCtx(ctx) { | ||
if (!ctx._ib) { | ||
try { | ||
let [block, value] = stepCtx(ctx); | ||
if (isPromiseLike(block)) { | ||
ctx._ib = block | ||
.catch((err) => { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}) | ||
.finally(() => advanceCtx(ctx)); | ||
} | ||
if (isPromiseLike(value)) { | ||
ctx._iv = value; | ||
ctx._el._inf = value; | ||
} | ||
return value; | ||
} | ||
catch (err) { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
throw err; | ||
} | ||
} | ||
resume(ctx); | ||
let iteration; | ||
try { | ||
ctx._f |= Executing; | ||
iteration = ctx._it.throw(err); | ||
else if (ctx._f & IsAsyncGen) { | ||
return ctx._iv; | ||
} | ||
catch (err) { | ||
ctx._f |= Done; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~Executing; | ||
} | ||
if (isPromiseLike(iteration)) { | ||
return iteration.then((iteration) => { | ||
if (iteration.done) { | ||
ctx._f |= Done; | ||
else if (!ctx._eb) { | ||
let resolve; | ||
ctx._eb = ctx._ib | ||
.then(() => { | ||
try { | ||
const [block, value] = stepCtx(ctx); | ||
resolve(value); | ||
if (isPromiseLike(value)) { | ||
ctx._el._inf = value; | ||
} | ||
if (isPromiseLike(block)) { | ||
return block.catch((err) => { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}); | ||
} | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
}, (err) => { | ||
ctx._f |= Done; | ||
throw err; | ||
}); | ||
catch (err) { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
} | ||
}) | ||
.finally(() => advanceCtx(ctx)); | ||
ctx._ev = new Promise((resolve1) => (resolve = resolve1)); | ||
} | ||
if (iteration.done) { | ||
ctx._f |= Done; | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
return ctx._ev; | ||
} | ||
function propagateError(ctx, err) { | ||
if (ctx === undefined) { | ||
throw err; | ||
/** | ||
* Called to make props available to the Context async iterator for async | ||
* generator components. | ||
*/ | ||
function resumeCtx(ctx) { | ||
if (ctx._oa) { | ||
ctx._oa(); | ||
ctx._oa = undefined; | ||
} | ||
let result; | ||
try { | ||
result = handleChildError(ctx, err); | ||
else { | ||
ctx._f |= IsAvailable; | ||
} | ||
catch (err) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
if (isPromiseLike(result)) { | ||
return result.catch((err) => propagateError(ctx._pa, err)); | ||
} | ||
return result; | ||
} | ||
function updateCtx(ctx) { | ||
ctx._f |= Updating; | ||
resume(ctx); | ||
return run(ctx); | ||
ctx._f |= IsUpdating; | ||
resumeCtx(ctx); | ||
return runCtx(ctx); | ||
} | ||
function updateCtxChildren(ctx, children) { | ||
let child; | ||
if (isNonStringIterable(children)) { | ||
child = createElement(Fragment, null, children); | ||
} | ||
else { | ||
child = children; | ||
} | ||
return updateChild(ctx._re, ctx._rt, // root | ||
return updateChildren(ctx._re, ctx._rt, // root | ||
ctx._ho, // host | ||
ctx, ctx._sc, // scope | ||
ctx._el, // element | ||
child); | ||
narrow(children)); | ||
} | ||
function commitCtx(ctx, value) { | ||
if (ctx._f & Unmounted) { | ||
if (ctx._f & IsUnmounted) { | ||
return; | ||
@@ -1520,3 +1464,3 @@ } | ||
} | ||
if (!(ctx._f & Updating)) { | ||
if (!(ctx._f & IsUpdating)) { | ||
const listeners = getListeners(ctx._pa, ctx._ho); | ||
@@ -1533,5 +1477,5 @@ if (listeners !== undefined && listeners.length > 0) { | ||
} | ||
// TODO: we don’t need to call arrange if none of the nodes have changed or moved | ||
// TODO: avoid calling arrange if none of the nodes have changed or moved | ||
const host = ctx._ho; | ||
if (host._f & Committed) { | ||
if (host._f & IsCommitted) { | ||
ctx._re.arrange(host, host.tag === Portal ? host.props.root : host._n, getChildValues(host)); | ||
@@ -1541,5 +1485,7 @@ } | ||
} | ||
ctx._f &= ~Updating; | ||
if (typeof ctx._ss === "object" && ctx._ss.size > 0) { | ||
// NOTE: We have to clear the set of callbacks before calling them, because a callback which refreshes the component would otherwise cause a stack overflow. | ||
ctx._f &= ~IsUpdating; | ||
if (ctx._ss && ctx._ss.size > 0) { | ||
// NOTE: We have to clear the set of callbacks before calling them, because | ||
// a callback which refreshes the component would otherwise cause a stack | ||
// overflow. | ||
const callbacks = Array.from(ctx._ss); | ||
@@ -1555,5 +1501,5 @@ ctx._ss.clear(); | ||
function unmountCtx(ctx) { | ||
ctx._f |= Unmounted; | ||
ctx._f |= IsUnmounted; | ||
clearEventListeners(ctx); | ||
if (typeof ctx._cs === "object") { | ||
if (ctx._cs) { | ||
const value = ctx._re.read(getValue(ctx._el)); | ||
@@ -1565,13 +1511,13 @@ for (const cleanup of ctx._cs) { | ||
} | ||
if (!(ctx._f & Done)) { | ||
ctx._f |= Done; | ||
resume(ctx); | ||
if (typeof ctx._it === "object" && typeof ctx._it.return === "function") { | ||
if (!(ctx._f & IsDone)) { | ||
ctx._f |= IsDone; | ||
resumeCtx(ctx); | ||
if (ctx._it && typeof ctx._it.return === "function") { | ||
let iteration; | ||
try { | ||
ctx._f |= Executing; | ||
ctx._f |= IsExecuting; | ||
iteration = ctx._it.return(); | ||
} | ||
finally { | ||
ctx._f &= ~Executing; | ||
ctx._f &= ~IsExecuting; | ||
} | ||
@@ -1584,2 +1530,110 @@ if (isPromiseLike(iteration)) { | ||
} | ||
/*** EVENT TARGET UTILITIES ***/ | ||
// EVENT PHASE CONSTANTS | ||
// https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase | ||
const NONE = 0; | ||
const CAPTURING_PHASE = 1; | ||
const AT_TARGET = 2; | ||
const BUBBLING_PHASE = 3; | ||
function normalizeOptions(options) { | ||
if (typeof options === "boolean") { | ||
return { capture: options }; | ||
} | ||
else if (options == null) { | ||
return {}; | ||
} | ||
return options; | ||
} | ||
function isEventTarget(value) { | ||
return (value != null && | ||
typeof value.addEventListener === "function" && | ||
typeof value.removeEventListener === "function" && | ||
typeof value.dispatchEvent === "function"); | ||
} | ||
function setEventProperty(ev, key, value) { | ||
Object.defineProperty(ev, key, { value, writable: false, configurable: true }); | ||
} | ||
/** | ||
* A function to reconstruct an array of every listener given a context and a | ||
* host element. | ||
* | ||
* @remarks | ||
* This function exploits the fact that contexts retain their nearest ancestor | ||
* host element. We can determine all the contexts which are directly listening | ||
* to an element by traversing up the context tree and checking that the host | ||
* element passed in matches the context’s host property. | ||
*/ | ||
function getListeners(ctx, host) { | ||
let listeners; | ||
while (ctx !== undefined && ctx._ho === host) { | ||
if (typeof ctx._ls !== "undefined") { | ||
listeners = (listeners || []).concat(ctx._ls); | ||
} | ||
ctx = ctx._pa; | ||
} | ||
return listeners; | ||
} | ||
function clearEventListeners(ctx) { | ||
if (ctx._ls && ctx._ls.length > 0) { | ||
for (const value of getChildValues(ctx._el)) { | ||
if (isEventTarget(value)) { | ||
for (const { type, callback, options } of ctx._ls) { | ||
value.removeEventListener(type, callback, options); | ||
} | ||
} | ||
} | ||
ctx._ls = undefined; | ||
} | ||
} | ||
/*** ERROR HANDLING UTILITIES ***/ | ||
// TODO: generator components which throw errors should be recoverable | ||
function handleChildError(ctx, err) { | ||
if (ctx._f & IsDone || !ctx._it || typeof ctx._it.throw !== "function") { | ||
throw err; | ||
} | ||
resumeCtx(ctx); | ||
let iteration; | ||
try { | ||
ctx._f |= IsExecuting; | ||
iteration = ctx._it.throw(err); | ||
} | ||
catch (err) { | ||
ctx._f |= IsDone; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~IsExecuting; | ||
} | ||
if (isPromiseLike(iteration)) { | ||
return iteration.then((iteration) => { | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
}, (err) => { | ||
ctx._f |= IsDone; | ||
throw err; | ||
}); | ||
} | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
} | ||
function propagateError(ctx, err) { | ||
if (ctx === undefined) { | ||
throw err; | ||
} | ||
let result; | ||
try { | ||
result = handleChildError(ctx, err); | ||
} | ||
catch (err) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
if (isPromiseLike(result)) { | ||
return result.catch((err) => propagateError(ctx._pa, err)); | ||
} | ||
return result; | ||
} | ||
@@ -1586,0 +1640,0 @@ exports.Context = Context; |
@@ -8,3 +8,3 @@ import { Children, Context, Element as CrankElement, ElementValue, Renderer } from "./crank"; | ||
patch(el: CrankElement<string | symbol>, node: Element): void; | ||
arrange(el: CrankElement<string | symbol>, parent: Node, children: Array<Node | string>): void; | ||
arrange(el: CrankElement<string | symbol>, node: Node, children: Array<Node | string>): void; | ||
} | ||
@@ -11,0 +11,0 @@ export declare const renderer: DOMRenderer; |
@@ -113,2 +113,5 @@ 'use strict'; | ||
} | ||
else if (typeof value === "function" || typeof value === "object") { | ||
node[name] = value; | ||
} | ||
else if (!forceAttribute && !isSVG && name in node) { | ||
@@ -130,14 +133,14 @@ node[name] = value; | ||
} | ||
arrange(el, parent, children) { | ||
arrange(el, node, children) { | ||
if (el.tag === crank.Portal && | ||
(parent == null || typeof parent.nodeType !== "number")) { | ||
throw new TypeError(`Portal root is not a node. Received: ${JSON.stringify(parent && parent.toString())}`); | ||
(node == null || typeof node.nodeType !== "number")) { | ||
throw new TypeError(`Portal root is not a node. Received: ${JSON.stringify(node && node.toString())}`); | ||
} | ||
if (!("innerHTML" in el.props) && | ||
(children.length !== 0 || parent.__cranky)) { | ||
(children.length !== 0 || node.__cranky)) { | ||
if (children.length === 0) { | ||
parent.textContent = ""; | ||
node.textContent = ""; | ||
return; | ||
} | ||
let oldChild = parent.firstChild; | ||
let oldChild = node.firstChild; | ||
let i = 0; | ||
@@ -156,3 +159,3 @@ while (oldChild !== null && i < children.length) { | ||
else { | ||
parent.insertBefore(document.createTextNode(newChild), oldChild); | ||
node.insertBefore(document.createTextNode(newChild), oldChild); | ||
} | ||
@@ -163,7 +166,7 @@ i++; | ||
const nextSibling = oldChild.nextSibling; | ||
parent.removeChild(oldChild); | ||
node.removeChild(oldChild); | ||
oldChild = nextSibling; | ||
} | ||
else { | ||
parent.insertBefore(newChild, oldChild); | ||
node.insertBefore(newChild, oldChild); | ||
i++; | ||
@@ -173,3 +176,3 @@ // TODO: this is an optimization for the js frameworks benchmark swap rows, but we need to think a little more about other cases like prepending. | ||
const nextSibling = oldChild.nextSibling; | ||
parent.removeChild(oldChild); | ||
node.removeChild(oldChild); | ||
oldChild = nextSibling; | ||
@@ -181,3 +184,3 @@ } | ||
const nextSibling = oldChild.nextSibling; | ||
parent.removeChild(oldChild); | ||
node.removeChild(oldChild); | ||
oldChild = nextSibling; | ||
@@ -187,3 +190,3 @@ } | ||
const newChild = children[i]; | ||
parent.appendChild(typeof newChild === "string" | ||
node.appendChild(typeof newChild === "string" | ||
? document.createTextNode(newChild) | ||
@@ -193,6 +196,6 @@ : newChild); | ||
if (children.length > 0) { | ||
parent.__cranky = true; | ||
node.__cranky = true; | ||
} | ||
else if (parent.__cranky) { | ||
parent.__cranky = false; | ||
else if (node.__cranky) { | ||
node.__cranky = false; | ||
} | ||
@@ -199,0 +202,0 @@ } |
300
crank.d.ts
@@ -5,3 +5,6 @@ /** | ||
* @remarks | ||
* Elements whose tags are strings or symbols are called “host” or “intrinsic” elements, and their behavior is determined by the renderer, while elements whose tags are components are called “component” elements, and their behavior is determined by the execution of the component. | ||
* Elements whose tags are strings or symbols are called “host” or “intrinsic” | ||
* elements, and their behavior is determined by the renderer, while elements | ||
* whose tags are components are called “component” elements, and their | ||
* behavior is determined by the execution of the component. | ||
*/ | ||
@@ -17,9 +20,15 @@ export declare type Tag = string | symbol | Component; | ||
* SPECIAL TAGS | ||
* | ||
* Crank provides a couple tags which have special meaning for the renderer. | ||
***/ | ||
/** | ||
* A special element tag for grouping multiple children within a parent. | ||
* A special tag for grouping multiple children within a parent. | ||
* | ||
* @remarks | ||
* All iterables which appear in the element tree are implicitly wrapped in fragments. The Fragment tag is just the empty string, and you can use the empty string in createElement calls/transpiler options to avoid having to reference the Fragment export directly. | ||
* All iterables which appear in the element tree are implicitly wrapped in a | ||
* fragment element. | ||
* | ||
* The tag is just the empty string, and you can use the empty string in | ||
* createElement calls or transpiler options to avoid having to reference this | ||
* export directly. | ||
*/ | ||
@@ -29,6 +38,10 @@ export declare const Fragment = ""; | ||
/** | ||
* A special element tag for creating a new element subtree with a different root, passed via the root prop. | ||
* A special tag for rendering into a root node passed via a root prop. | ||
* | ||
* @remarks | ||
* This element tag is useful for creating element trees with multiple roots. Renderer.prototype.render will implicitly wrap the children which have been passed in in an implicit Portal element. | ||
* This tag is useful for creating element trees with multiple roots, for | ||
* things like modals or tooltips. | ||
* | ||
* Renderer.prototype.render will implicitly wrap the passed in element tree in | ||
* an implicit Portal element. | ||
*/ | ||
@@ -38,6 +51,9 @@ export declare const Portal: any; | ||
/** | ||
* A special element tag which copies whatever child appeared previously in the element’s position. | ||
* A special tag which preserves whatever was previously rendered in the | ||
* element’s position. | ||
* | ||
* @remarks | ||
* Copy elements are useful when you want to prevent a subtree from updating as a performance optimization. | ||
* Copy elements are useful for when you want to prevent a subtree from | ||
* rerendering as a performance optimization. Copy elements can also be keyed, | ||
* in which case the previously rendered keyed element will be preserved. | ||
*/ | ||
@@ -47,6 +63,7 @@ export declare const Copy: any; | ||
/** | ||
* A special element tag for injecting raw nodes into an element tree via its value prop. | ||
* A special element tag for injecting raw nodes or strings via a value prop. | ||
* | ||
* @remarks | ||
* If the value prop is a string, Renderer.prototype.parse will be called on the string and the element’s rendered value will be the result. | ||
* If the value prop is a string, Renderer.prototype.parse will be called on | ||
* the string and the result of that method will be inserted. | ||
*/ | ||
@@ -59,3 +76,5 @@ export declare const Raw: any; | ||
* @remarks | ||
* Arbitrary objects can also be safely rendered but they will be converted to a string using the toString method. We exclude them from this type to catch potential mistakes. | ||
* Arbitrary objects can also be safely rendered, but will be converted to a | ||
* string using the toString method. We exclude them from this type to catch | ||
* potential mistakes. | ||
*/ | ||
@@ -66,10 +85,7 @@ export declare type Child = Element | string | number | boolean | null | undefined; | ||
/** | ||
* Describes all valid values of an element tree, including arbitrarily nested iterables of such values. | ||
* Describes all valid values of an element tree, including arbitrarily nested | ||
* iterables of such values. | ||
*/ | ||
export declare type Children = Child | ChildIterable; | ||
/** | ||
* All values in the element tree are narrowed from the union in Child to NarrowedChild during rendering. This greatly simplifies element diffing. | ||
*/ | ||
declare type NarrowedChild = Element | string | undefined; | ||
/** | ||
* Represents all functions which can be used as a component. | ||
@@ -83,5 +99,7 @@ * | ||
/** | ||
* Elements are the basic building blocks of Crank applications. They are JavaScript objects which are interpreted by special classes called renderers to produce and manage stateful nodes. | ||
* Elements are the basic building blocks of Crank applications. They are | ||
* JavaScript objects which are interpreted by special classes called renderers | ||
* to produce and manage stateful nodes. | ||
* | ||
* @typeparam TTag - the type of the tag of the element. | ||
* @typeparam TTag - The type of the tag of the element. | ||
* | ||
@@ -99,3 +117,4 @@ * @example | ||
* @remarks | ||
* Typically, you use the createElement function to create elements and not this class directly. | ||
* Typically, you use the createElement function to create elements rather than | ||
* instatiating this class directly. | ||
*/ | ||
@@ -105,3 +124,4 @@ export declare class Element<TTag extends Tag = Tag> { | ||
* @internal | ||
* A unique symbol to identify elements as elements across versions and realms, and to protect against basic injection attacks. | ||
* A unique symbol to identify elements as elements across versions and | ||
* realms, and to protect against basic injection attacks. | ||
* https://overreacted.io/why-do-react-elements-have-typeof-property/ | ||
@@ -116,14 +136,14 @@ */ | ||
/** | ||
* The tag of the element. Can be a function, string or symbol depending on the kind of element. | ||
* The tag of the element. Can be a string, symbol or function. | ||
*/ | ||
tag: TTag; | ||
/** | ||
* An object containing the “properties” of an element. These correspond to the attributes of the element when using JSX syntax. | ||
* | ||
* @remarks | ||
* The props of an object are passed to most renderer host methods, and as the first argument to components. | ||
* An object containing the “properties” of an element. These correspond to | ||
* the “attributes” of an element when using JSX syntax. | ||
*/ | ||
props: TagProps<TTag>; | ||
/** | ||
* A value which uniquely identifies an element from its siblings so that it can be added/updated/moved/removed by the identity of the key rather than its position within the parent. | ||
* A value which uniquely identifies an element from its siblings so that it | ||
* can be added/updated/moved/removed by the identity of the key rather than | ||
* its position within the parent. | ||
* | ||
@@ -135,3 +155,3 @@ * @remarks | ||
/** | ||
* A callback which is called with the element’s value when the value is committed. | ||
* A callback which is called with the element’s result when it is committed. | ||
* | ||
@@ -141,3 +161,3 @@ * @remarks | ||
*/ | ||
ref: Function | undefined; | ||
ref: ((value: unknown) => unknown) | undefined; | ||
/** | ||
@@ -153,3 +173,4 @@ * @internal | ||
* @remarks | ||
* Set by Renderer.prototype.create when the component is mounted. This property will only be set for host elements. | ||
* Set by Renderer.prototype.create when the component is mounted. | ||
* This property will only be set for host elements. | ||
*/ | ||
@@ -162,3 +183,4 @@ _n: any; | ||
* @remarks | ||
* Created and assigned by the Renderer for component elements when it mounts the element tree. | ||
* Created and assigned by the Renderer for component elements when it mounts | ||
* the element tree. | ||
*/ | ||
@@ -171,11 +193,15 @@ _ctx: Context<TagProps<TTag>> | undefined; | ||
* @remarks | ||
* Until an element commits for the first time, we show any previously rendered values in its place. | ||
* Until an element commits for the first time, we show any previously | ||
* rendered values in its place. This is mainly important when the nearest | ||
* host is rearranged concurrently. | ||
*/ | ||
_fb: Element | undefined; | ||
_fb: NarrowedChild; | ||
/** | ||
* @internal | ||
* inflightPromise - The current async run of the element. | ||
* inflight - The current async run of the element. | ||
* | ||
* @remarks | ||
* This value is used to make sure Copy element refs and as the yield value of async generator components with async children. It is unset when the element is committed. | ||
* This value is used to make sure Copy element refs fire at the correct | ||
* time, and is also used as the yield value of async generator components | ||
* with async children. It is unset when the element is committed. | ||
*/ | ||
@@ -185,6 +211,7 @@ _inf: Promise<any> | undefined; | ||
* @internal | ||
* onNewValues - the resolve function of a promise which represents the next child result. See the chase function for more info. | ||
* onvalues - The resolve function of a promise which represents the next | ||
* children result. | ||
*/ | ||
_onv: Function | undefined; | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref: Function | undefined); | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref: ((value: unknown) => unknown) | undefined); | ||
} | ||
@@ -196,6 +223,9 @@ export declare function isElement(value: any): value is Element; | ||
* @remarks | ||
* This function is usually used as a transpilation target for JSX transpilers, but it can also be called directly. It additionally extracts the crank-key and crank-ref props so they aren’t accessible to renderer methods or components, and assigns the children prop according to the arguments passed to the function. | ||
* This function is usually used as a transpilation target for JSX transpilers, | ||
* but it can also be called directly. It additionally extracts the crank-key | ||
* and crank-ref props so they aren’t accessible to renderer methods or | ||
* components, and assigns the children prop according to the arguments passed | ||
* to the function. | ||
*/ | ||
export declare function createElement<TTag extends Tag>(tag: TTag, props?: TagProps<TTag> | null | undefined, ...children: Array<unknown>): Element<TTag>; | ||
export declare function createElement<TTag extends Tag>(tag: TTag, props?: TagProps<TTag> | null | undefined, children?: unknown): Element<TTag>; | ||
/** | ||
@@ -205,7 +235,14 @@ * Clones a given element. Will also shallow copy the props object. | ||
* @remarks | ||
* Mainly used internally to make sure we don’t accidentally reuse elements in an element tree, because element have internal properties which are directly mutated by the renderer. | ||
* Mainly used internally to make sure we don’t accidentally reuse elements in | ||
* an element tree, because element have internal properties which are directly | ||
* mutated by the renderer. | ||
*/ | ||
export declare function cloneElement<TTag extends Tag>(el: Element<TTag>): Element<TTag>; | ||
/*** ELEMENT VALUE UTILITIES ***/ | ||
/*** ELEMENT UTILITIES ***/ | ||
/** | ||
* All values in the element tree are narrowed from the union in Child to | ||
* NarrowedChild during rendering, to simplify element diffing. | ||
*/ | ||
declare type NarrowedChild = Element | string | undefined; | ||
/** | ||
* A helper type which repesents all the possible rendered values of an element. | ||
@@ -216,12 +253,24 @@ * | ||
* @remarks | ||
* When asking the question, what is the value of a specific element, the answer varies depending on the tag of the element. For host or Raw elements, the answer is simply the nodes (DOM nodes in the case of the DOM renderer) created for the element. For fragments, the values are usually an array of nodes. For portals, the value is undefined, because a Portal element’s root and children are opaque to its parent. For components, the value can be any of the above, because the value of a component is determined by its children. Rendered values can also be strings or arrays of nodes and strings, in the case of component or fragment elements with strings for children. All of these possible values are reflected in this utility type. | ||
* When asking the question, what is the value of a specific element, the | ||
* answer varies depending on its tag. For host or Raw elements, the answer is | ||
* simply the nodes created for the element. For fragments, the values are | ||
* usually an array of nodes. For portals, the value is undefined, because a | ||
* Portal element’s root and children are opaque to its parent. For components, | ||
* the value can be any of the above, because the value of a component is | ||
* determined by its immediate children. Rendered values can also be strings or | ||
* arrays of nodes and strings, in the case of component or fragment elements | ||
* with strings for children. All of these possible values are reflected in | ||
* this utility type. | ||
*/ | ||
export declare type ElementValue<TNode> = Array<TNode | string> | TNode | string | undefined; | ||
/** | ||
* An abstract class which is subclassed to render to different target environments. This class is responsible for kicking off the rendering process, caching previous trees by root, and creating/mutating/disposing the nodes of the target environment. | ||
* An abstract class which is subclassed to render to different target | ||
* environments. This class is responsible for kicking off the rendering | ||
* process, caching previous trees by root, and creating, mutating and | ||
* disposing of nodes. | ||
* | ||
* @typeparam TNode - The type of the node for a specific rendering environment. | ||
* @typeparam TNode - The type of the node for a rendering environment. | ||
* @typeparam TScope - Data which is passed down the tree. | ||
* @typeparam TRoot - The type of the root for a specific rendering environment. | ||
* @typeparam TResult - The type of the exposed values. | ||
* @typeparam TRoot - The type of the root for a rendering environment. | ||
* @typeparam TResult - The type of exposed values. | ||
*/ | ||
@@ -238,17 +287,30 @@ export declare class Renderer<TNode, TScope, TRoot = TNode, TResult = ElementValue<TNode>> { | ||
* | ||
* @param children - An element tree. You can render null with a previously used root to delete the previously rendered element tree from the cache. | ||
* @param root - The node to be rendered into. The renderer will cache element trees per root. | ||
* @param ctx - An optional context that will be the ancestor context of all elements in the tree. Useful for connecting renderers which call each other so that events/provisions properly propagate. The context for a given root must be the same or an error will be thrown. | ||
* @param children - An element tree. You can render null with a previously | ||
* used root to delete the previously rendered element tree from the cache. | ||
* @param root - The node to be rendered into. The renderer will cache | ||
* element trees per root. | ||
* @param ctx - An optional context that will be the ancestor context of all | ||
* elements in the tree. Useful for connecting renderers which call each | ||
* other so that events/provisions properly propagate. The context for a | ||
* given root must be the same or an error will be thrown. | ||
* | ||
* @returns The read result of rendering the children, or a possible promise of the read result if the element tree renders asynchronously. | ||
* @returns The result of rendering the children, or a possible promise of | ||
* the result if the element tree renders asynchronously. | ||
*/ | ||
render(children: Children, root?: TRoot | undefined, ctx?: Context | undefined): Promise<TResult> | TResult; | ||
/** | ||
* Called when an element’s value is exposed via render, schedule, refresh, refs, or generator yield expressions. | ||
* Called when an element’s value is exposed via render, schedule, refresh, | ||
* refs, or generator yield expressions. | ||
* | ||
* @param value - The value of the element being read. Can be a node, a string, undefined, or an array of nodes and strings, depending on the element. | ||
* @returns Varies according to the specific renderer subclass. By default, it exposes the element’s value. | ||
* @param value - The value of the element being read. Can be a node, a | ||
* string, undefined, or an array of nodes and strings, depending on the | ||
* element. | ||
* | ||
* @returns Varies according to the specific renderer subclass. By default, | ||
* it exposes the element’s value. | ||
* | ||
* @remarks | ||
* This is useful for renderers which don’t want to expose their internal nodes. For instance, the HTML renderer will convert all internal nodes to strings. | ||
* This is useful for renderers which don’t want to expose their internal | ||
* nodes. For instance, the HTML renderer will convert all internal nodes to | ||
* strings. | ||
* | ||
@@ -261,3 +323,5 @@ */ | ||
* @remarks | ||
* Useful for passing data down the element tree. For instance, the DOM renderer uses this method to keep track of whether we’re in an SVG subtree. | ||
* Useful for passing data down the element tree. For instance, the DOM | ||
* renderer uses this method to keep track of whether we’re in an SVG | ||
* subtree. | ||
* | ||
@@ -267,6 +331,8 @@ * @param el - The host element. | ||
* | ||
* @returns The scope to be passed to create and scope for child host elements. | ||
* @returns The scope to be passed to create and scope for child host | ||
* elements. | ||
* | ||
* @remarks | ||
* This method sets the scope for child host elements, not the current host element. | ||
* This method sets the scope for child host elements, not the current host | ||
* element. | ||
*/ | ||
@@ -283,3 +349,6 @@ scope(el: Element<string | symbol>, scope: TScope | undefined): TScope; | ||
* @remarks | ||
* Rather than returning text nodes for whatever environment we’re rendering to, we defer that step for Renderer.prototype.arrange. We do this so that adjacent strings can be concatenated and the actual element tree can be rendered in a normalized form. | ||
* Rather than returning text nodes for whatever environment we’re rendering | ||
* to, we defer that step for Renderer.prototype.arrange. We do this so that | ||
* adjacent strings can be concatenated and the actual element tree can be | ||
* rendered in a normalized form. | ||
*/ | ||
@@ -310,3 +379,2 @@ escape(text: string, _scope: TScope): string; | ||
* @param node - The node associated with the host element. | ||
* @param scope - The current scope. | ||
* | ||
@@ -316,7 +384,8 @@ * @returns The return value is ignored. | ||
* @remarks | ||
* Used to mutate the node associated with an element when new props are passed. | ||
* Used to mutate the node associated with an element when new props are | ||
* passed. | ||
*/ | ||
patch(_el: Element<string | symbol>, _node: TNode): unknown; | ||
/** | ||
* Called for each host element after its children have committed with the actual values of the children. | ||
* Called for each host element so that elements can be arranged into a tree. | ||
* | ||
@@ -330,10 +399,10 @@ * @param el - The host element. | ||
* @remarks | ||
* This method is also called by child components contexts as the last step of a refresh. | ||
* This method is also called by child components contexts as the last step | ||
* of a refresh. | ||
*/ | ||
arrange(_el: Element<string | symbol>, _parent: TNode | TRoot, _children: Array<TNode | string>): unknown; | ||
arrange(_el: Element<string | symbol>, _node: TNode | TRoot, _children: Array<TNode | string>): unknown; | ||
/** | ||
* Called for each host element when it is unmounted. | ||
* | ||
* @param tag - The tag of the host element. | ||
* @param props - The props of the host element. | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
@@ -354,27 +423,21 @@ * | ||
/** | ||
* A map of event type strings to Event subclasses. Can be extended via TypeScript module augmentation to have strongly typed event listeners. | ||
* An interface which can be extended to provide strongly typed provisions (see | ||
* Context.prototype.get and Context.prototype.set) | ||
*/ | ||
export interface EventMap extends Crank.EventMap { | ||
[type: string]: Event; | ||
export interface ProvisionMap extends Crank.ProvisionMap { | ||
} | ||
declare type MappedEventListener<T extends string> = (ev: EventMap[T]) => unknown; | ||
declare type MappedEventListenerOrEventListenerObject<T extends string> = MappedEventListener<T> | { | ||
handleEvent: MappedEventListener<T>; | ||
}; | ||
interface EventListenerRecord { | ||
type: string; | ||
listener: MappedEventListenerOrEventListenerObject<any>; | ||
callback: MappedEventListener<any>; | ||
options: AddEventListenerOptions; | ||
export interface Context extends Crank.Context { | ||
} | ||
/** | ||
* An interface which can be extended to provide strongly typed provisions (see Context.prototype.get and Context.prototype.set) | ||
* A class which is instantiated and passed to every component as its this | ||
* value. Contexts form a tree just like elements and all components in the | ||
* element tree are connected via contexts. Components can use this tree to | ||
* communicate data upwards via events and downwards via provisions. | ||
* | ||
* @typeparam TProps - The expected shape of the props passed to the component. | ||
* Used to strongly type the Context iterator methods. | ||
* @typeparam TResult - The readable element value type. It is used in places | ||
* such as the return value of refresh and the argument passed to | ||
* schedule/cleanup callbacks. | ||
*/ | ||
export interface ProvisionMap extends Crank.ProvisionMap { | ||
} | ||
/** | ||
* A class which is instantiated and passed to every component as its this value. Contexts form a tree just like elements and all components in the element tree are connected via contexts. Components can use this tree to communicate data upwards via events and downwards via provisions. | ||
* @typeparam TProps - The expected shape of the props passed to the component. Used to strongly type the Context iterator methods. | ||
* @typeparam TResult - The readable element value type. It is used in places such as the return value of refresh and the argument passed to schedule/cleanup callbacks. | ||
*/ | ||
export declare class Context<TProps = any, TResult = any> implements EventTarget { | ||
@@ -393,3 +456,3 @@ /** | ||
* @internal | ||
* root - The root node set by an ancestor’s Portal prop. | ||
* root - The root node as set by the nearest ancestor portal. | ||
*/ | ||
@@ -401,3 +464,5 @@ _rt: unknown; | ||
* @remarks | ||
* When refresh is called, the host element will be arranged as the last step of the commit, to make sure the parent’s children properly reflects the components’s children. | ||
* When refresh is called, the host element will be arranged as the last step | ||
* of the commit, to make sure the parent’s children properly reflects the | ||
* components’s children. | ||
*/ | ||
@@ -427,5 +492,7 @@ _ho: Element<string | symbol>; | ||
* @internal | ||
* onProps - A callback used in conjunction with the Available flag to implement the props async iterator. See the Symbol.asyncIterator method and the resume function. | ||
* onavailable - A callback used in conjunction with the IsAvailable flag to | ||
* implement the props async iterator. See the Symbol.asyncIterator method | ||
* and the resumeCtx function. | ||
*/ | ||
_op: ((props: any) => unknown) | undefined; | ||
_oa: (() => unknown) | undefined; | ||
/** | ||
@@ -453,3 +520,4 @@ * @internal | ||
* @internal | ||
* listeners - An array of event listeners added to the context via Context.prototype.addEventListener | ||
* listeners - An array of event listeners added to the context via | ||
* Context.prototype.addEventListener | ||
*/ | ||
@@ -459,3 +527,4 @@ _ls: Array<EventListenerRecord> | undefined; | ||
* @internal | ||
* provisions - A map of values which can be set via Context.prototype.set and read from child contexts via Context.prototype.get | ||
* provisions - A map of values which can be set via Context.prototype.set | ||
* and read from child contexts via Context.prototype.get | ||
*/ | ||
@@ -465,3 +534,4 @@ _ps: Map<unknown, unknown> | undefined; | ||
* @internal | ||
* schedules - a set of callbacks registered via Context.prototype.schedule, which fire when the component has committed. | ||
* schedules - a set of callbacks registered via Context.prototype.schedule, | ||
* which fire when the component has committed. | ||
*/ | ||
@@ -471,3 +541,4 @@ _ss: Set<(value: TResult) => unknown> | undefined; | ||
* @internal | ||
* cleanups - a set of callbacks registered via Context.prototype.cleanup, which fire when the component has unmounted. | ||
* cleanups - a set of callbacks registered via Context.prototype.cleanup, | ||
* which fire when the component has unmounted. | ||
*/ | ||
@@ -479,6 +550,2 @@ _cs: Set<(value: TResult) => unknown> | undefined; | ||
constructor(renderer: Renderer<unknown, unknown, unknown, TResult>, root: unknown, host: Element<string | symbol>, parent: Context<unknown, TResult> | undefined, scope: unknown, el: Element<Component>); | ||
consume<TKey extends keyof ProvisionMap>(key: TKey): ProvisionMap[TKey]; | ||
consume(key: unknown): any; | ||
provide<TKey extends keyof ProvisionMap>(key: TKey, value: ProvisionMap[TKey]): void; | ||
provide(key: unknown, value: any): void; | ||
/** | ||
@@ -488,3 +555,5 @@ * The current props of the associated element. | ||
* @remarks | ||
* Typically, you should read props either via the first parameter of the component or via the context iterator methods. This property is mainly for plugins or utilities which wrap contexts. | ||
* Typically, you should read props either via the first parameter of the | ||
* component or via the context iterator methods. This property is mainly for | ||
* plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -496,3 +565,5 @@ get props(): TProps; | ||
* @remarks | ||
* Typically, you should read values via refs, generator yield expressions, or the refresh, schedule or cleanup methods. This property is mainly for plugins or utilities which wrap contexts. | ||
* Typically, you should read values via refs, generator yield expressions, | ||
* or the refresh, schedule or cleanup methods. This property is mainly for | ||
* plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -505,16 +576,26 @@ get value(): TResult; | ||
* | ||
* @returns The rendered value of the component or a promise of the rendered value if the component or its children execute asynchronously. | ||
* @returns The rendered value of the component or a promise of the rendered | ||
* value if the component or its children execute asynchronously. | ||
* | ||
* @remarks | ||
* The refresh method works a little differently for async generator components, in that it will resume the Context async iterator rather than resuming execution. This is because async generator components are perpetually resumed independent of updates/refresh. | ||
* The refresh method works a little differently for async generator | ||
* components, in that it will resume the Context async iterator rather than | ||
* resuming execution. This is because async generator components are | ||
* perpetually resumed independent of updates/refresh. | ||
*/ | ||
refresh(): Promise<TResult> | TResult; | ||
/** | ||
* Registers a callback which fires when the component commits. Will only fire once per callback and update. | ||
* Registers a callback which fires when the component commits. Will only | ||
* fire once per callback and update. | ||
*/ | ||
schedule(callback: (value: TResult) => unknown): void; | ||
/** | ||
* Registers a callback which fires when the component unmounts. Will only fire once per callback. | ||
* Registers a callback which fires when the component unmounts. Will only | ||
* fire once per callback. | ||
*/ | ||
cleanup(callback: (value: TResult) => unknown): void; | ||
consume<TKey extends keyof ProvisionMap>(key: TKey): ProvisionMap[TKey]; | ||
consume(key: unknown): any; | ||
provide<TKey extends keyof ProvisionMap>(key: TKey, value: ProvisionMap[TKey]): void; | ||
provide(key: unknown, value: any): void; | ||
addEventListener<T extends string>(type: T, listener: MappedEventListenerOrEventListenerObject<T> | null, options?: boolean | AddEventListenerOptions): void; | ||
@@ -524,4 +605,19 @@ removeEventListener<T extends string>(type: T, listener: MappedEventListenerOrEventListenerObject<T> | null, options?: EventListenerOptions | boolean): void; | ||
} | ||
export interface Context extends Crank.Context { | ||
/** | ||
* A map of event type strings to Event subclasses. Can be extended via | ||
* TypeScript module augmentation to have strongly typed event listeners. | ||
*/ | ||
export interface EventMap extends Crank.EventMap { | ||
[type: string]: Event; | ||
} | ||
declare type MappedEventListener<T extends string> = (ev: EventMap[T]) => unknown; | ||
declare type MappedEventListenerOrEventListenerObject<T extends string> = MappedEventListener<T> | { | ||
handleEvent: MappedEventListener<T>; | ||
}; | ||
interface EventListenerRecord { | ||
type: string; | ||
listener: MappedEventListenerOrEventListenerObject<any>; | ||
callback: MappedEventListener<any>; | ||
options: AddEventListenerOptions; | ||
} | ||
declare global { | ||
@@ -528,0 +624,0 @@ module Crank { |
1206
crank.js
@@ -10,8 +10,10 @@ /// <reference types="./crank.d.ts" /> | ||
} | ||
function isIterable(value) { | ||
return value != null && typeof value[Symbol.iterator] === "function"; | ||
function arrayify(value) { | ||
return value == null | ||
? [] | ||
: typeof value !== "string" && | ||
typeof value[Symbol.iterator] === "function" | ||
? Array.from(value) | ||
: [value]; | ||
} | ||
function isNonStringIterable(value) { | ||
return typeof value !== "string" && isIterable(value); | ||
} | ||
function isIteratorLike(value) { | ||
@@ -25,61 +27,72 @@ return value != null && typeof value.next === "function"; | ||
* SPECIAL TAGS | ||
* | ||
* Crank provides a couple tags which have special meaning for the renderer. | ||
***/ | ||
/** | ||
* A special element tag for grouping multiple children within a parent. | ||
* A special tag for grouping multiple children within a parent. | ||
* | ||
* @remarks | ||
* All iterables which appear in the element tree are implicitly wrapped in fragments. The Fragment tag is just the empty string, and you can use the empty string in createElement calls/transpiler options to avoid having to reference the Fragment export directly. | ||
* All iterables which appear in the element tree are implicitly wrapped in a | ||
* fragment element. | ||
* | ||
* The tag is just the empty string, and you can use the empty string in | ||
* createElement calls or transpiler options to avoid having to reference this | ||
* export directly. | ||
*/ | ||
const Fragment = ""; | ||
// NOTE: We assert the following symbol tags to be any because typescript support for symbol tags in JSX does not exist yet. | ||
// NOTE: We assert the following symbol tags as any because typescript support | ||
// for symbol tags in JSX does not exist yet. | ||
// https://github.com/microsoft/TypeScript/issues/38367 | ||
/** | ||
* A special element tag for creating a new element subtree with a different root, passed via the root prop. | ||
* A special tag for rendering into a root node passed via a root prop. | ||
* | ||
* @remarks | ||
* This element tag is useful for creating element trees with multiple roots. Renderer.prototype.render will implicitly wrap the children which have been passed in in an implicit Portal element. | ||
* This tag is useful for creating element trees with multiple roots, for | ||
* things like modals or tooltips. | ||
* | ||
* Renderer.prototype.render will implicitly wrap the passed in element tree in | ||
* an implicit Portal element. | ||
*/ | ||
const Portal = Symbol.for("crank.Portal"); | ||
/** | ||
* A special element tag which copies whatever child appeared previously in the element’s position. | ||
* A special tag which preserves whatever was previously rendered in the | ||
* element’s position. | ||
* | ||
* @remarks | ||
* Copy elements are useful when you want to prevent a subtree from updating as a performance optimization. | ||
* Copy elements are useful for when you want to prevent a subtree from | ||
* rerendering as a performance optimization. Copy elements can also be keyed, | ||
* in which case the previously rendered keyed element will be preserved. | ||
*/ | ||
const Copy = Symbol.for("crank.Copy"); | ||
/** | ||
* A special element tag for injecting raw nodes into an element tree via its value prop. | ||
* A special element tag for injecting raw nodes or strings via a value prop. | ||
* | ||
* @remarks | ||
* If the value prop is a string, Renderer.prototype.parse will be called on the string and the element’s rendered value will be the result. | ||
* If the value prop is a string, Renderer.prototype.parse will be called on | ||
* the string and the result of that method will be inserted. | ||
*/ | ||
const Raw = Symbol.for("crank.Raw"); | ||
function narrow(child) { | ||
if (typeof child === "boolean" || child == null) { | ||
return undefined; | ||
} | ||
else if (typeof child === "string" || isElement(child)) { | ||
return child; | ||
} | ||
else { | ||
return child.toString(); | ||
} | ||
} | ||
const ElementSymbol = Symbol.for("crank.Element"); | ||
/*** ELEMENT FLAGS ***/ | ||
/** | ||
* A flag which is set when the component has been mounted. Used mainly to detect whether an element is being reused so that it can be cloned. | ||
* A flag which is set when the component has been mounted. Used mainly to | ||
* detect whether an element is being reused so that we clone it. | ||
*/ | ||
const Mounted = 1 << 0; | ||
const IsMounted = 1 << 0; | ||
/** | ||
* A flag which is set when the component has committed at least once. | ||
*/ | ||
const Committed = 1 << 1; | ||
// NOTE: To save on filesize, we mangle the internal properties of Crank classes by hand. These internal properties are prefixed with an underscore. Refer to their definitions to see their unabbreviated names. | ||
// NOTE: to maximize compatibility between Crank versions, starting with 0.2.0, the $$typeof property and the non-internal properties will not be changed, and any change to these properties will be considered a breaking change. This is to ensure maximum compatibility between components which use different Crank versions. | ||
const IsCommitted = 1 << 1; | ||
// NOTE: To save on filesize, we mangle the internal properties of Crank | ||
// classes by hand. These internal properties are prefixed with an underscore. | ||
// Refer to their definitions to see their unabbreviated names. | ||
// NOTE: to maximize compatibility between Crank versions, starting with 0.2.0, | ||
// any change to the $$typeof property or public properties will be considered | ||
// a breaking change. | ||
/** | ||
* Elements are the basic building blocks of Crank applications. They are JavaScript objects which are interpreted by special classes called renderers to produce and manage stateful nodes. | ||
* Elements are the basic building blocks of Crank applications. They are | ||
* JavaScript objects which are interpreted by special classes called renderers | ||
* to produce and manage stateful nodes. | ||
* | ||
* @typeparam TTag - the type of the tag of the element. | ||
* @typeparam TTag - The type of the tag of the element. | ||
* | ||
@@ -97,3 +110,4 @@ * @example | ||
* @remarks | ||
* Typically, you use the createElement function to create elements and not this class directly. | ||
* Typically, you use the createElement function to create elements rather than | ||
* instatiating this class directly. | ||
*/ | ||
@@ -111,3 +125,6 @@ class Element { | ||
this._ctx = undefined; | ||
// We don’t assign fallback (_fb), inflightPromise (_inf) or onNewValues (_onv) in the constructor to save on the shallow size of elements. This saves a couple bytes per element, especially when we aren’t rendering asynchronous components. This may or may not be a good idea. | ||
// NOTE: We don’t assign fallback (_fb), inflight (_inf) or onvalues (_onv) | ||
// in the constructor to save on the shallow size of elements. This saves a | ||
// couple bytes per element, especially when we aren’t rendering | ||
// asynchronous components. This may or may not be a good idea. | ||
} | ||
@@ -118,3 +135,13 @@ } | ||
} | ||
function createElement(tag, props, children) { | ||
/** | ||
* Creates an element with the specified tag, props and children. | ||
* | ||
* @remarks | ||
* This function is usually used as a transpilation target for JSX transpilers, | ||
* but it can also be called directly. It additionally extracts the crank-key | ||
* and crank-ref props so they aren’t accessible to renderer methods or | ||
* components, and assigns the children prop according to the arguments passed | ||
* to the function. | ||
*/ | ||
function createElement(tag, props, ...children) { | ||
let key; | ||
@@ -126,5 +153,6 @@ let ref; | ||
if (name === "crank-key") { | ||
// NOTE: We have to make sure we don’t assign null to the key because we don’t check for null keys in the diffing functions. | ||
if (props[name] != null) { | ||
key = props[name]; | ||
// NOTE: We have to make sure we don’t assign null to the key because | ||
// we don’t check for null keys in the diffing functions. | ||
if (props["crank-key"] != null) { | ||
key = props["crank-key"]; | ||
} | ||
@@ -134,3 +162,3 @@ } | ||
if (typeof props["crank-ref"] === "function") { | ||
ref = props[name]; | ||
ref = props["crank-ref"]; | ||
} | ||
@@ -143,13 +171,8 @@ } | ||
} | ||
let length = arguments.length; | ||
if (length > 3) { | ||
const children1 = []; | ||
while (length-- > 2) { | ||
children1[length - 2] = arguments[length]; | ||
} | ||
props1.children = children1; | ||
} | ||
else if (length > 2) { | ||
if (children.length > 1) { | ||
props1.children = children; | ||
} | ||
else if (children.length === 1) { | ||
props1.children = children[0]; | ||
} | ||
return new Element(tag, props1, key, ref); | ||
@@ -161,3 +184,5 @@ } | ||
* @remarks | ||
* Mainly used internally to make sure we don’t accidentally reuse elements in an element tree, because element have internal properties which are directly mutated by the renderer. | ||
* Mainly used internally to make sure we don’t accidentally reuse elements in | ||
* an element tree, because element have internal properties which are directly | ||
* mutated by the renderer. | ||
*/ | ||
@@ -170,8 +195,24 @@ function cloneElement(el) { | ||
} | ||
function narrow(value) { | ||
if (typeof value === "boolean" || value == null) { | ||
return undefined; | ||
} | ||
else if (typeof value === "string" || isElement(value)) { | ||
return value; | ||
} | ||
else if (typeof value[Symbol.iterator] === "function") { | ||
return createElement(Fragment, null, value); | ||
} | ||
return value.toString(); | ||
} | ||
/** | ||
* Takes an array of element values and normalizes the output as an array of nodes and strings. | ||
* Takes an array of element values and normalizes the output as an array of | ||
* nodes and strings. | ||
* | ||
* @returns Normalized array of nodes and/or strings. | ||
* | ||
* @remarks | ||
* Normalize will flatten only one level of nested arrays, because it is designed to be called once at each level of the tree. It will also concatenate adjacent strings and remove all undefined values. | ||
* Normalize will flatten only one level of nested arrays, because it is | ||
* designed to be called once at each level of the tree. It will also | ||
* concatenate adjacent strings and remove all undefined values. | ||
*/ | ||
@@ -187,3 +228,10 @@ function normalize(values) { | ||
} | ||
else if (Array.isArray(value)) { | ||
else if (!Array.isArray(value)) { | ||
if (buffer) { | ||
result.push(buffer); | ||
buffer = undefined; | ||
} | ||
result.push(value); | ||
} | ||
else { | ||
// We could use recursion here but it’s just easier to do it inline. | ||
@@ -205,10 +253,2 @@ for (let j = 0; j < value.length; j++) { | ||
} | ||
else { | ||
// value is of type TNode | ||
if (buffer) { | ||
result.push(buffer); | ||
buffer = undefined; | ||
} | ||
result.push(value); | ||
} | ||
} | ||
@@ -225,4 +265,7 @@ if (buffer) { | ||
function getValue(el) { | ||
if (typeof el._fb !== "undefined") { | ||
return getValue(el._fb); | ||
if (el._fb) { | ||
if (typeof el._fb === "object") { | ||
return getValue(el._fb); | ||
} | ||
return el._fb; | ||
} | ||
@@ -239,2 +282,3 @@ else if (el.tag === Portal) { | ||
* Walks an element’s children to find its child values. | ||
* | ||
* @returns A normalized array of nodes and strings. | ||
@@ -247,8 +291,5 @@ */ | ||
const child = children[i]; | ||
if (typeof child === "string") { | ||
values.push(child); | ||
if (child) { | ||
values.push(typeof child === "string" ? child : getValue(child)); | ||
} | ||
else if (typeof child !== "undefined") { | ||
values.push(getValue(child)); | ||
} | ||
} | ||
@@ -258,8 +299,11 @@ return normalize(values); | ||
/** | ||
* An abstract class which is subclassed to render to different target environments. This class is responsible for kicking off the rendering process, caching previous trees by root, and creating/mutating/disposing the nodes of the target environment. | ||
* An abstract class which is subclassed to render to different target | ||
* environments. This class is responsible for kicking off the rendering | ||
* process, caching previous trees by root, and creating, mutating and | ||
* disposing of nodes. | ||
* | ||
* @typeparam TNode - The type of the node for a specific rendering environment. | ||
* @typeparam TNode - The type of the node for a rendering environment. | ||
* @typeparam TScope - Data which is passed down the tree. | ||
* @typeparam TRoot - The type of the root for a specific rendering environment. | ||
* @typeparam TResult - The type of the exposed values. | ||
* @typeparam TRoot - The type of the root for a rendering environment. | ||
* @typeparam TResult - The type of exposed values. | ||
*/ | ||
@@ -273,7 +317,13 @@ class Renderer { | ||
* | ||
* @param children - An element tree. You can render null with a previously used root to delete the previously rendered element tree from the cache. | ||
* @param root - The node to be rendered into. The renderer will cache element trees per root. | ||
* @param ctx - An optional context that will be the ancestor context of all elements in the tree. Useful for connecting renderers which call each other so that events/provisions properly propagate. The context for a given root must be the same or an error will be thrown. | ||
* @param children - An element tree. You can render null with a previously | ||
* used root to delete the previously rendered element tree from the cache. | ||
* @param root - The node to be rendered into. The renderer will cache | ||
* element trees per root. | ||
* @param ctx - An optional context that will be the ancestor context of all | ||
* elements in the tree. Useful for connecting renderers which call each | ||
* other so that events/provisions properly propagate. The context for a | ||
* given root must be the same or an error will be thrown. | ||
* | ||
* @returns The read result of rendering the children, or a possible promise of the read result if the element tree renders asynchronously. | ||
* @returns The result of rendering the children, or a possible promise of | ||
* the result if the element tree renders asynchronously. | ||
*/ | ||
@@ -302,3 +352,4 @@ render(children, root, ctx) { | ||
const value = update(this, root, portal, ctx, undefined, portal); | ||
// NOTE: we return the read child values of the portal because portals themselves have no readable value. | ||
// NOTE: We return the child values of the portal because portal elements | ||
// themselves have no readable value. | ||
if (isPromiseLike(value)) { | ||
@@ -320,9 +371,16 @@ return value.then(() => { | ||
/** | ||
* Called when an element’s value is exposed via render, schedule, refresh, refs, or generator yield expressions. | ||
* Called when an element’s value is exposed via render, schedule, refresh, | ||
* refs, or generator yield expressions. | ||
* | ||
* @param value - The value of the element being read. Can be a node, a string, undefined, or an array of nodes and strings, depending on the element. | ||
* @returns Varies according to the specific renderer subclass. By default, it exposes the element’s value. | ||
* @param value - The value of the element being read. Can be a node, a | ||
* string, undefined, or an array of nodes and strings, depending on the | ||
* element. | ||
* | ||
* @returns Varies according to the specific renderer subclass. By default, | ||
* it exposes the element’s value. | ||
* | ||
* @remarks | ||
* This is useful for renderers which don’t want to expose their internal nodes. For instance, the HTML renderer will convert all internal nodes to strings. | ||
* This is useful for renderers which don’t want to expose their internal | ||
* nodes. For instance, the HTML renderer will convert all internal nodes to | ||
* strings. | ||
* | ||
@@ -337,3 +395,5 @@ */ | ||
* @remarks | ||
* Useful for passing data down the element tree. For instance, the DOM renderer uses this method to keep track of whether we’re in an SVG subtree. | ||
* Useful for passing data down the element tree. For instance, the DOM | ||
* renderer uses this method to keep track of whether we’re in an SVG | ||
* subtree. | ||
* | ||
@@ -343,6 +403,8 @@ * @param el - The host element. | ||
* | ||
* @returns The scope to be passed to create and scope for child host elements. | ||
* @returns The scope to be passed to create and scope for child host | ||
* elements. | ||
* | ||
* @remarks | ||
* This method sets the scope for child host elements, not the current host element. | ||
* This method sets the scope for child host elements, not the current host | ||
* element. | ||
*/ | ||
@@ -361,3 +423,6 @@ scope(el, scope) { | ||
* @remarks | ||
* Rather than returning text nodes for whatever environment we’re rendering to, we defer that step for Renderer.prototype.arrange. We do this so that adjacent strings can be concatenated and the actual element tree can be rendered in a normalized form. | ||
* Rather than returning text nodes for whatever environment we’re rendering | ||
* to, we defer that step for Renderer.prototype.arrange. We do this so that | ||
* adjacent strings can be concatenated and the actual element tree can be | ||
* rendered in a normalized form. | ||
*/ | ||
@@ -394,3 +459,2 @@ escape(text, _scope) { | ||
* @param node - The node associated with the host element. | ||
* @param scope - The current scope. | ||
* | ||
@@ -400,3 +464,4 @@ * @returns The return value is ignored. | ||
* @remarks | ||
* Used to mutate the node associated with an element when new props are passed. | ||
* Used to mutate the node associated with an element when new props are | ||
* passed. | ||
*/ | ||
@@ -408,3 +473,3 @@ patch(_el, _node) { | ||
/** | ||
* Called for each host element after its children have committed with the actual values of the children. | ||
* Called for each host element so that elements can be arranged into a tree. | ||
* | ||
@@ -418,13 +483,14 @@ * @param el - The host element. | ||
* @remarks | ||
* This method is also called by child components contexts as the last step of a refresh. | ||
* This method is also called by child components contexts as the last step | ||
* of a refresh. | ||
*/ | ||
arrange(_el, _parent, _children) { | ||
arrange(_el, _node, _children) { | ||
return; | ||
} | ||
// TODO: remove(): a method which is called to remove a child from a parent to optimize arrange | ||
// TODO: remove(): a method which is called to remove a child from a parent | ||
// to optimize arrange | ||
/** | ||
* Called for each host element when it is unmounted. | ||
* | ||
* @param tag - The tag of the host element. | ||
* @param props - The props of the host element. | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
@@ -449,4 +515,55 @@ * | ||
/*** PRIVATE RENDERER FUNCTIONS ***/ | ||
function diff(renderer, root, host, ctx, scope, oldChild, newChild) { | ||
let value; | ||
if (typeof oldChild === "object" && | ||
typeof newChild === "object" && | ||
oldChild.tag === newChild.tag) { | ||
// TODO: implement Raw element parse caching | ||
if (oldChild.tag === Portal) { | ||
if (oldChild.props.root !== newChild.props.root) { | ||
renderer.arrange(oldChild, oldChild.props.root, []); | ||
} | ||
} | ||
if (oldChild !== newChild) { | ||
oldChild.props = newChild.props; | ||
oldChild.ref = newChild.ref; | ||
newChild = oldChild; | ||
} | ||
value = update(renderer, root, host, ctx, scope, newChild); | ||
} | ||
else if (typeof newChild === "object") { | ||
if (newChild.tag === Copy) { | ||
if (typeof oldChild === "object") { | ||
value = oldChild._inf || getValue(oldChild); | ||
} | ||
else { | ||
value = oldChild; | ||
} | ||
if (typeof newChild.ref === "function") { | ||
if (isPromiseLike(value)) { | ||
value.then(newChild.ref).catch(NOOP); | ||
} | ||
else { | ||
newChild.ref(value); | ||
} | ||
} | ||
newChild = oldChild; | ||
} | ||
else { | ||
if (newChild._f & IsMounted) { | ||
newChild = cloneElement(newChild); | ||
} | ||
value = mount(renderer, root, host, ctx, scope, newChild); | ||
if (isPromiseLike(value)) { | ||
newChild._fb = oldChild; | ||
} | ||
} | ||
} | ||
else if (typeof newChild === "string") { | ||
value = renderer.escape(newChild, scope); | ||
} | ||
return [newChild, value]; | ||
} | ||
function mount(renderer, root, host, ctx, scope, el) { | ||
el._f |= Mounted; | ||
el._f |= IsMounted; | ||
if (typeof el.tag === "function") { | ||
@@ -466,22 +583,11 @@ el._ctx = new Context(renderer, root, host, ctx, scope, el); | ||
} | ||
// NOTE: The primary benefit of having a separate codepath for mounting is that it’s slightly faster because we don’t have to align and diff children against old children. But for singular child values, updateChild is sufficient. | ||
if (isNonStringIterable(el.props.children)) { | ||
return mountChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
} | ||
return updateChild(renderer, root, host, ctx, scope, el, el.props.children); | ||
return mountChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
} | ||
function mountChildren(renderer, root, host, ctx, scope, parent, children) { | ||
function mountChildren(renderer, root, host, ctx, scope, el, children) { | ||
const newChildren = arrayify(children); | ||
const values = []; | ||
const newChildren = Array.from(children); | ||
let async = false; | ||
let seen; | ||
for (let i = 0; i < newChildren.length; i++) { | ||
let value; | ||
let child = newChildren[i]; | ||
if (isNonStringIterable(child)) { | ||
child = createElement(Fragment, null, child); | ||
} | ||
else { | ||
child = narrow(child); | ||
} | ||
let child = narrow(newChildren[i]); | ||
if (typeof child === "object" && typeof child.key !== "undefined") { | ||
@@ -493,3 +599,2 @@ if (seen === undefined) { | ||
if (seen.has(child.key)) { | ||
// eslint-disable-next-line no-console | ||
console.error("Duplicate key", child.key); | ||
@@ -500,2 +605,3 @@ } | ||
} | ||
let value; | ||
[child, value] = diff(renderer, root, host, ctx, scope, undefined, // oldChild | ||
@@ -509,18 +615,26 @@ child); | ||
} | ||
parent._ch = unwrap(newChildren); | ||
let values1; | ||
el._ch = unwrap(newChildren); | ||
if (async) { | ||
values1 = Promise.all(values); | ||
let onvalues; | ||
const values1 = Promise.race([ | ||
Promise.all(values), | ||
new Promise((resolve) => (onvalues = resolve)), | ||
]); | ||
if (el._onv) { | ||
el._onv(values1); | ||
} | ||
el._onv = onvalues; | ||
el._inf = values1.then((values) => commit(renderer, scope, el, normalize(values))); | ||
return el._inf; | ||
} | ||
else { | ||
values1 = values; | ||
if (el._onv) { | ||
el._onv(values); | ||
el._onv = undefined; | ||
} | ||
return chase(renderer, host, ctx, scope, parent, values1); | ||
return commit(renderer, scope, el, normalize(values)); | ||
} | ||
function update(renderer, root, host, ctx, scope, el) { | ||
if (typeof el.tag === "function") { | ||
if (typeof el._ctx === "object") { | ||
return updateCtx(el._ctx); | ||
} | ||
return undefined; | ||
// el._ctx should probably never be undefined here | ||
return el._ctx ? updateCtx(el._ctx) : undefined; | ||
} | ||
@@ -537,31 +651,5 @@ else if (el.tag === Raw) { | ||
} | ||
if (isNonStringIterable(el.props.children)) { | ||
return updateChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
} | ||
else if (Array.isArray(el._ch)) { | ||
return updateChildren(renderer, root, host, ctx, scope, el, [ | ||
el.props.children, | ||
]); | ||
} | ||
return updateChild(renderer, root, host, ctx, scope, el, el.props.children); | ||
return updateChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
} | ||
function updateChild(renderer, root, host, ctx, scope, parent, child) { | ||
let oldChild = parent._ch; | ||
let newChild = narrow(child); | ||
if (typeof oldChild === "object" && | ||
typeof newChild === "object" && | ||
oldChild.key !== newChild.key) { | ||
oldChild = undefined; | ||
} | ||
let value; | ||
[newChild, value] = diff(renderer, root, host, ctx, scope, oldChild, newChild); | ||
if (typeof oldChild === "object" && oldChild !== newChild) { | ||
unmount(renderer, host, ctx, oldChild); | ||
} | ||
parent._ch = newChild; | ||
// TODO: allow single values to be passed to chase | ||
const values = isPromiseLike(value) ? value.then(wrap) : wrap(value); | ||
return chase(renderer, host, ctx, scope, parent, values); | ||
} | ||
function mapChildrenByKey(children) { | ||
function createChildrenByKey(children) { | ||
const childrenByKey = new Map(); | ||
@@ -576,9 +664,9 @@ for (let i = 0; i < children.length; i++) { | ||
} | ||
function updateChildren(renderer, root, host, ctx, scope, parent, children) { | ||
if (typeof parent._ch === "undefined") { | ||
return mountChildren(renderer, root, host, ctx, scope, parent, children); | ||
function updateChildren(renderer, root, host, ctx, scope, el, children) { | ||
if (typeof el._ch === "undefined") { | ||
return mountChildren(renderer, root, host, ctx, scope, el, children); | ||
} | ||
const oldChildren = wrap(el._ch); | ||
const newChildren = arrayify(children); | ||
const values = []; | ||
const oldChildren = wrap(parent._ch); | ||
const newChildren = Array.from(children); | ||
const graveyard = []; | ||
@@ -592,9 +680,3 @@ let i = 0; | ||
let oldChild = oldChildren[i]; | ||
let newChild = newChildren[j]; | ||
if (isNonStringIterable(newChild)) { | ||
newChild = createElement(Fragment, null, newChild); | ||
} | ||
else { | ||
newChild = narrow(newChild); | ||
} | ||
let newChild = narrow(newChildren[j]); | ||
// ALIGNMENT | ||
@@ -604,3 +686,2 @@ let oldKey = typeof oldChild === "object" ? oldChild.key : undefined; | ||
if (seen !== undefined && seen.has(newKey)) { | ||
// eslint-disable-next-line no-console | ||
console.error("Duplicate key", newKey); | ||
@@ -611,3 +692,3 @@ newKey = undefined; | ||
if (!childrenByKey) { | ||
childrenByKey = mapChildrenByKey(oldChildren.slice(i)); | ||
childrenByKey = createChildrenByKey(oldChildren.slice(i)); | ||
} | ||
@@ -651,3 +732,3 @@ if (newKey === undefined) { | ||
} | ||
parent._ch = unwrap(newChildren); | ||
el._ch = unwrap(newChildren); | ||
// cleanup | ||
@@ -664,84 +745,20 @@ for (; i < oldChildren.length; i++) { | ||
} | ||
let values1; | ||
if (async) { | ||
values1 = Promise.all(values).finally(() => graveyard.forEach((child) => unmount(renderer, host, ctx, child))); | ||
} | ||
else { | ||
values1 = values; | ||
graveyard.forEach((child) => unmount(renderer, host, ctx, child)); | ||
} | ||
return chase(renderer, host, ctx, scope, parent, values1); | ||
} | ||
function diff(renderer, root, host, ctx, scope, oldChild, newChild) { | ||
let value; | ||
if (typeof oldChild === "object" && | ||
typeof newChild === "object" && | ||
oldChild.tag === newChild.tag) { | ||
// TODO: implement Raw element parse caching | ||
if (oldChild.tag === Portal) { | ||
if (oldChild.props.root !== newChild.props.root) { | ||
renderer.arrange(oldChild, oldChild.props.root, []); | ||
} | ||
let values1 = Promise.all(values).finally(() => { | ||
graveyard.forEach((child) => unmount(renderer, host, ctx, child)); | ||
}); | ||
let onvalues; | ||
values1 = Promise.race([ | ||
values1, | ||
new Promise((resolve) => (onvalues = resolve)), | ||
]); | ||
if (el._onv) { | ||
el._onv(values1); | ||
} | ||
if (oldChild !== newChild) { | ||
oldChild.props = newChild.props; | ||
oldChild.ref = newChild.ref; | ||
newChild = oldChild; | ||
} | ||
value = update(renderer, root, host, ctx, scope, newChild); | ||
el._onv = onvalues; | ||
el._inf = values1.then((values) => commit(renderer, scope, el, normalize(values))); | ||
return el._inf; | ||
} | ||
else if (typeof newChild === "object") { | ||
if (newChild.tag === Copy) { | ||
if (typeof oldChild === "object") { | ||
value = oldChild._inf || getValue(oldChild); | ||
} | ||
else { | ||
value = oldChild; | ||
} | ||
if (typeof newChild.ref === "function") { | ||
if (isPromiseLike(value)) { | ||
value.then(newChild.ref).catch(NOOP); | ||
} | ||
else { | ||
newChild.ref(value); | ||
} | ||
} | ||
newChild = oldChild; | ||
} | ||
else { | ||
if (newChild._f & Mounted) { | ||
newChild = cloneElement(newChild); | ||
} | ||
value = mount(renderer, root, host, ctx, scope, newChild); | ||
if (typeof oldChild === "object" && isPromiseLike(value)) { | ||
newChild._fb = oldChild; | ||
} | ||
} | ||
} | ||
else if (typeof newChild === "string") { | ||
newChild = renderer.escape(newChild, scope); | ||
value = newChild; | ||
} | ||
return [newChild, value]; | ||
} | ||
/** | ||
* A function to race current child values with future child values. | ||
* | ||
* @remarks | ||
* When an element’s children update asynchronously, we race the resulting promise with the next update of the element’s children. By induction, this ensures that when any update to an element settles, all past updates to that same element will have settled as well. This prevents deadlocks and unnecessary awaiting when an element’s children have been cleared, for instance. | ||
*/ | ||
function chase(renderer, host, ctx, scope, el, values) { | ||
if (isPromiseLike(values)) { | ||
let onNewValues; | ||
const newValues = new Promise((resolve) => (onNewValues = resolve)); | ||
const valuesP = Promise.race([values, newValues]); | ||
if (typeof el._onv === "function") { | ||
el._onv(valuesP); | ||
} | ||
el._onv = onNewValues; | ||
const value = valuesP.then((values) => commit(renderer, scope, el, normalize(values))); | ||
el._inf = value; | ||
return value; | ||
} | ||
if (typeof el._onv === "function") { | ||
graveyard.forEach((child) => unmount(renderer, host, ctx, child)); | ||
if (el._onv) { | ||
el._onv(values); | ||
@@ -760,2 +777,5 @@ el._onv = undefined; | ||
else if (el.tag === Portal) { | ||
if (!(el._f & IsCommitted)) { | ||
el._f |= IsCommitted; | ||
} | ||
renderer.arrange(el, el.props.root, values); | ||
@@ -775,4 +795,5 @@ renderer.complete(el.props.root); | ||
else if (el.tag !== Fragment) { | ||
if (!(el._f & Committed)) { | ||
if (!(el._f & IsCommitted)) { | ||
el._n = renderer.create(el, scope); | ||
el._f |= IsCommitted; | ||
} | ||
@@ -783,10 +804,9 @@ renderer.patch(el, el._n); | ||
} | ||
el._f |= Committed; | ||
if (typeof el.ref === "function") { | ||
if (el.ref) { | ||
el.ref(renderer.read(value)); | ||
} | ||
if (typeof el._inf !== "undefined") { | ||
if (el._inf) { | ||
el._inf = undefined; | ||
} | ||
if (typeof el._fb !== "undefined") { | ||
if (el._fb) { | ||
el._fb = undefined; | ||
@@ -806,3 +826,3 @@ } | ||
renderer.arrange(host, host.props.root, []); | ||
renderer.complete(el.props.root); | ||
renderer.complete(host.props.root); | ||
} | ||
@@ -830,81 +850,39 @@ else if (el.tag !== Fragment) { | ||
} | ||
/*** EVENT UTILITIES ***/ | ||
// EVENT PHASE CONSTANTS (https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase) | ||
const NONE = 0; | ||
const CAPTURING_PHASE = 1; | ||
const AT_TARGET = 2; | ||
const BUBBLING_PHASE = 3; | ||
function normalizeOptions(options) { | ||
if (typeof options === "boolean") { | ||
return { capture: options }; | ||
} | ||
else if (options == null) { | ||
return {}; | ||
} | ||
else { | ||
return options; | ||
} | ||
} | ||
function isEventTarget(value) { | ||
return (value != null && | ||
typeof value.addEventListener === "function" && | ||
typeof value.removeEventListener === "function" && | ||
typeof value.dispatchEvent === "function"); | ||
} | ||
function setEventProperty(ev, key, value) { | ||
Object.defineProperty(ev, key, { value, writable: false, configurable: true }); | ||
} | ||
/** | ||
* A function to reconstruct an array of every listener given a context and a host element. | ||
* | ||
* @remarks | ||
* This function exploits the fact that contexts retain their nearest ancestor host element. We can determine all the contexts which are directly listening to an element by traversing up the context tree and checking that the host element passed in matches the context’s host property. | ||
*/ | ||
function getListeners(ctx, host) { | ||
let listeners; | ||
while (ctx !== undefined && ctx._ho === host) { | ||
if (typeof ctx._ls !== "undefined") { | ||
listeners = (listeners || []).concat(ctx._ls); | ||
} | ||
ctx = ctx._pa; | ||
} | ||
return listeners; | ||
} | ||
function clearEventListeners(ctx) { | ||
if (typeof ctx._ls !== "undefined" && ctx._ls.length > 0) { | ||
for (const value of getChildValues(ctx._el)) { | ||
if (isEventTarget(value)) { | ||
for (const record of ctx._ls) { | ||
value.removeEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
} | ||
ctx._ls = undefined; | ||
} | ||
} | ||
// CONTEXT FLAGS | ||
/** | ||
* A flag which is set when the component is being updated by the parent and cleared when the component has committed. Used to determine whether the nearest host ancestor needs to be rearranged. | ||
* A flag which is set when the component is being updated by the parent and | ||
* cleared when the component has committed. Used to determine whether the | ||
* nearest host ancestor needs to be rearranged. | ||
*/ | ||
const Updating = 1 << 0; | ||
const IsUpdating = 1 << 0; | ||
/** | ||
* A flag which is set when the component function is called or the component generator is resumed. This flags is used to ensure that a component which synchronously triggers a second update in the course of rendering does not cause an stack overflow or a generator error. | ||
* A flag which is set when the component function is called or the component | ||
* generator is resumed. This flags is used to ensure that a component which | ||
* synchronously triggers a second update in the course of rendering does not | ||
* cause an stack overflow or a generator error. | ||
*/ | ||
const Executing = 1 << 1; | ||
const IsExecuting = 1 << 1; | ||
/** | ||
* A flag used to make sure multiple values are not pulled from context prop iterators without a yield. | ||
* A flag used to make sure multiple values are not pulled from context prop | ||
* iterators without a yield. | ||
*/ | ||
const Iterating = 1 << 2; | ||
const IsIterating = 1 << 2; | ||
/** | ||
* A flag used by async generator components in conjunction with the onProps functions (_op) to mark whether new props can be pulled via the context iterator methods. | ||
* A flag used by async generator components in conjunction with the | ||
* onIsAvailable (_oa) callback to mark whether new props can be pulled via the | ||
* context async iterator. | ||
*/ | ||
const Available = 1 << 3; | ||
const IsAvailable = 1 << 3; | ||
/** | ||
* A flag which is set when generator components return. Set whenever an iterator returns an iteration with the done property set to true or throws. Done components will stick to their last rendered value and ignore further updates. | ||
* A flag which is set when generator components return. Set whenever an | ||
* iterator returns an iteration with the done property set to true or throws. | ||
* Done components will stick to their last rendered value and ignore further | ||
* updates. | ||
*/ | ||
const Done = 1 << 4; | ||
const IsDone = 1 << 4; | ||
/** | ||
* A flag which is set when the component is unmounted. Unmounted components are no longer in the element tree, and cannot run or refresh. | ||
* A flag which is set when the component is unmounted. Unmounted components | ||
* are no longer in the element tree, and cannot run or refresh. | ||
*/ | ||
const Unmounted = 1 << 5; | ||
const IsUnmounted = 1 << 5; | ||
/** | ||
@@ -919,5 +897,12 @@ * A flag which indicates that the component is a sync generator component. | ||
/** | ||
* A class which is instantiated and passed to every component as its this value. Contexts form a tree just like elements and all components in the element tree are connected via contexts. Components can use this tree to communicate data upwards via events and downwards via provisions. | ||
* @typeparam TProps - The expected shape of the props passed to the component. Used to strongly type the Context iterator methods. | ||
* @typeparam TResult - The readable element value type. It is used in places such as the return value of refresh and the argument passed to schedule/cleanup callbacks. | ||
* A class which is instantiated and passed to every component as its this | ||
* value. Contexts form a tree just like elements and all components in the | ||
* element tree are connected via contexts. Components can use this tree to | ||
* communicate data upwards via events and downwards via provisions. | ||
* | ||
* @typeparam TProps - The expected shape of the props passed to the component. | ||
* Used to strongly type the Context iterator methods. | ||
* @typeparam TResult - The readable element value type. It is used in places | ||
* such as the return value of refresh and the argument passed to | ||
* schedule/cleanup callbacks. | ||
*/ | ||
@@ -937,15 +922,2 @@ class Context { | ||
} | ||
consume(key) { | ||
for (let parent = this._pa; parent !== undefined; parent = parent._pa) { | ||
if (typeof parent._ps === "object" && parent._ps.has(key)) { | ||
return parent._ps.get(key); | ||
} | ||
} | ||
} | ||
provide(key, value) { | ||
if (typeof this._ps === "undefined") { | ||
this._ps = new Map(); | ||
} | ||
this._ps.set(key, value); | ||
} | ||
/** | ||
@@ -955,3 +927,5 @@ * The current props of the associated element. | ||
* @remarks | ||
* Typically, you should read props either via the first parameter of the component or via the context iterator methods. This property is mainly for plugins or utilities which wrap contexts. | ||
* Typically, you should read props either via the first parameter of the | ||
* component or via the context iterator methods. This property is mainly for | ||
* plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -965,3 +939,5 @@ get props() { | ||
* @remarks | ||
* Typically, you should read values via refs, generator yield expressions, or the refresh, schedule or cleanup methods. This property is mainly for plugins or utilities which wrap contexts. | ||
* Typically, you should read values via refs, generator yield expressions, | ||
* or the refresh, schedule or cleanup methods. This property is mainly for | ||
* plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -972,5 +948,4 @@ get value() { | ||
*[Symbol.iterator]() { | ||
const el = this._el; | ||
while (!(this._f & Unmounted)) { | ||
if (this._f & Iterating) { | ||
while (!(this._f & IsDone)) { | ||
if (this._f & IsIterating) { | ||
throw new Error("Context iterated twice without a yield"); | ||
@@ -981,10 +956,11 @@ } | ||
} | ||
this._f |= Iterating; | ||
yield el.props; | ||
this._f |= IsIterating; | ||
yield this._el.props; | ||
} | ||
} | ||
async *[Symbol.asyncIterator]() { | ||
const el = this._el; | ||
// We use a do while loop rather than a while loop to handle an edge case | ||
// where an async generator component is unmounted synchronously. | ||
do { | ||
if (this._f & Iterating) { | ||
if (this._f & IsIterating) { | ||
throw new Error("Context iterated twice without a yield"); | ||
@@ -995,14 +971,14 @@ } | ||
} | ||
this._f |= Iterating; | ||
if (this._f & Available) { | ||
this._f &= ~Available; | ||
yield el.props; | ||
this._f |= IsIterating; | ||
if (this._f & IsAvailable) { | ||
this._f &= ~IsAvailable; | ||
} | ||
else { | ||
const props = await new Promise((resolve) => (this._op = resolve)); | ||
if (!(this._f & Unmounted)) { | ||
yield props; | ||
await new Promise((resolve) => (this._oa = resolve)); | ||
if (this._f & IsDone) { | ||
break; | ||
} | ||
} | ||
} while (!(this._f & Unmounted)); | ||
yield this._el.props; | ||
} while (!(this._f & IsDone)); | ||
} | ||
@@ -1012,27 +988,29 @@ /** | ||
* | ||
* @returns The rendered value of the component or a promise of the rendered value if the component or its children execute asynchronously. | ||
* @returns The rendered value of the component or a promise of the rendered | ||
* value if the component or its children execute asynchronously. | ||
* | ||
* @remarks | ||
* The refresh method works a little differently for async generator components, in that it will resume the Context async iterator rather than resuming execution. This is because async generator components are perpetually resumed independent of updates/refresh. | ||
* The refresh method works a little differently for async generator | ||
* components, in that it will resume the Context async iterator rather than | ||
* resuming execution. This is because async generator components are | ||
* perpetually resumed independent of updates/refresh. | ||
*/ | ||
refresh() { | ||
if (this._f & Unmounted) { | ||
// eslint-disable-next-line no-console | ||
if (this._f & IsUnmounted) { | ||
console.error("Component is unmounted"); | ||
return this._re.read(undefined); | ||
} | ||
else if (this._f & Executing) { | ||
// eslint-disable-next-line no-console | ||
else if (this._f & IsExecuting) { | ||
console.error("Component is already executing"); | ||
return this._re.read(undefined); | ||
} | ||
this._f &= ~Updating; | ||
resume(this); | ||
return this._re.read(run(this)); | ||
resumeCtx(this); | ||
return this._re.read(runCtx(this)); | ||
} | ||
/** | ||
* Registers a callback which fires when the component commits. Will only fire once per callback and update. | ||
* Registers a callback which fires when the component commits. Will only | ||
* fire once per callback and update. | ||
*/ | ||
schedule(callback) { | ||
if (typeof this._ss === "undefined") { | ||
if (!this._ss) { | ||
this._ss = new Set(); | ||
@@ -1043,6 +1021,7 @@ } | ||
/** | ||
* Registers a callback which fires when the component unmounts. Will only fire once per callback. | ||
* Registers a callback which fires when the component unmounts. Will only | ||
* fire once per callback. | ||
*/ | ||
cleanup(callback) { | ||
if (typeof this._cs === "undefined") { | ||
if (!this._cs) { | ||
this._cs = new Set(); | ||
@@ -1052,2 +1031,15 @@ } | ||
} | ||
consume(key) { | ||
for (let parent = this._pa; parent !== undefined; parent = parent._pa) { | ||
if (parent._ps && parent._ps.has(key)) { | ||
return parent._ps.get(key); | ||
} | ||
} | ||
} | ||
provide(key, value) { | ||
if (!this._ps) { | ||
this._ps = new Map(); | ||
} | ||
this._ps.set(key, value); | ||
} | ||
addEventListener(type, listener, options) { | ||
@@ -1057,3 +1049,3 @@ if (listener == null) { | ||
} | ||
else if (typeof this._ls === "undefined") { | ||
else if (!this._ls) { | ||
this._ls = []; | ||
@@ -1073,3 +1065,3 @@ } | ||
record.callback = function () { | ||
if (typeof self._ls !== "undefined") { | ||
if (self._ls) { | ||
self._ls = self._ls.filter((record1) => record !== record1); | ||
@@ -1096,3 +1088,3 @@ if (self._ls.length === 0) { | ||
removeEventListener(type, listener, options) { | ||
if (listener == null || typeof this._ls === "undefined") { | ||
if (listener == null || !this._ls) { | ||
return; | ||
@@ -1109,2 +1101,5 @@ } | ||
this._ls.splice(i, 1); | ||
if (this._ls.length === 0) { | ||
this._ls = undefined; | ||
} | ||
for (const value of getChildValues(this._el)) { | ||
@@ -1115,5 +1110,2 @@ if (isEventTarget(value)) { | ||
} | ||
if (this._ls.length === 0) { | ||
this._ls = undefined; | ||
} | ||
} | ||
@@ -1125,35 +1117,36 @@ dispatchEvent(ev) { | ||
} | ||
let stopped = false; | ||
// We patch the stopImmediatePropagation method because ev.cancelBubble | ||
// only informs us if stopPropagation was called. | ||
let immediateCancelBubble = false; | ||
const stopImmediatePropagation = ev.stopImmediatePropagation; | ||
setEventProperty(ev, "stopImmediatePropagation", () => { | ||
stopped = true; | ||
immediateCancelBubble = true; | ||
return stopImmediatePropagation.call(ev); | ||
}); | ||
setEventProperty(ev, "target", this); | ||
setEventProperty(ev, "eventPhase", CAPTURING_PHASE); | ||
// The only possible errors in this block are errors thrown by listener | ||
// callbacks, and dispatchEvent will only log the error rather than | ||
// rethrowing it. We return true because the return value is overridden in | ||
// the finally block but TypeScript (justifiably) does not recognize the | ||
// unsafe return statement. | ||
try { | ||
setEventProperty(ev, "eventPhase", CAPTURING_PHASE); | ||
for (let i = path.length - 1; i >= 0; i--) { | ||
const et = path[i]; | ||
if (typeof et._ls !== "undefined") { | ||
if (et._ls) { | ||
setEventProperty(ev, "currentTarget", et); | ||
for (const record of et._ls) { | ||
if (record.type === ev.type && record.options.capture) { | ||
try { | ||
record.callback.call(this, ev); | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
catch (err) { | ||
// eslint-disable-next-line no-console | ||
console.error(err); | ||
} | ||
if (stopped) { | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
if (stopped || ev.cancelBubble) { | ||
return !ev.defaultPrevented; | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
} | ||
if (typeof this._ls !== "undefined") { | ||
if (this._ls) { | ||
setEventProperty(ev, "eventPhase", AT_TARGET); | ||
@@ -1163,16 +1156,10 @@ setEventProperty(ev, "currentTarget", this); | ||
if (record.type === ev.type) { | ||
try { | ||
record.callback.call(this, ev); | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
catch (err) { | ||
// eslint-disable-next-line no-console | ||
console.error(err); | ||
} | ||
if (stopped) { | ||
break; | ||
} | ||
} | ||
} | ||
if (stopped || ev.cancelBubble) { | ||
return !ev.defaultPrevented; | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
@@ -1183,29 +1170,27 @@ } | ||
for (const et of path) { | ||
if (typeof et._ls !== "undefined") { | ||
if (et._ls) { | ||
setEventProperty(ev, "currentTarget", et); | ||
for (const record of et._ls) { | ||
if (record.type === ev.type && !record.options.capture) { | ||
try { | ||
record.callback.call(this, ev); | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
catch (err) { | ||
// eslint-disable-next-line no-console | ||
console.error(err); | ||
} | ||
if (stopped) { | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
if (stopped || ev.cancelBubble) { | ||
return !ev.defaultPrevented; | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
return !ev.defaultPrevented; | ||
} | ||
catch (err) { | ||
console.error(err); | ||
} | ||
finally { | ||
setEventProperty(ev, "eventPhase", NONE); | ||
setEventProperty(ev, "currentTarget", null); | ||
// eslint-disable-next-line no-unsafe-finally | ||
return !ev.defaultPrevented; | ||
} | ||
@@ -1215,99 +1200,43 @@ } | ||
/*** PRIVATE CONTEXT FUNCTIONS ***/ | ||
/** | ||
* Called to make props available to the Context async iterator for async generator components. | ||
/* | ||
* NOTE: The functions stepCtx, advanceCtx and runCtx work together to | ||
* implement the async queueing behavior of components. The runCtx function | ||
* calls the stepCtx function, which returns two results in a tuple. The first | ||
* result, called the “block,” is a possible promise which represents the | ||
* duration for which the component is blocked from accepting new updates. The | ||
* second result, called the “value,” is the actual result of the update. The | ||
* runCtx function caches block/value from the stepCtx function on the context, | ||
* according to whether the component is currently blocked. The “inflight” | ||
* block/value properties are the currently executing update, and the | ||
* “enqueued” block/value properties represent an enqueued next stepCtx. | ||
* Enqueued steps are dequeued in a finally callback on the blocking promise. | ||
*/ | ||
function resume(ctx) { | ||
if (typeof ctx._op === "function") { | ||
ctx._op(ctx._el.props); | ||
ctx._op = undefined; | ||
} | ||
else { | ||
ctx._f |= Available; | ||
} | ||
} | ||
// NOTE: The functions run, step and advance work together to implement the async queueing behavior of components. The run function calls the step function, which returns two results in a tuple. The first result, called “block,” is a possible promise which represents the duration for which the component is blocked from accepting new updates. The second result, called “value,” is the actual result of the update. The run function caches block/value from the step function on the context, according to whether the component is currently blocked. The “inflight” block/value properties are the currently executing update, and the “enqueued” block/value properties represent an enqueued next step. Enqueued steps are dequeed in a finally callback on the inflight block. | ||
/** | ||
* Enqueues and executes the component associated with the context. | ||
*/ | ||
function run(ctx) { | ||
if (typeof ctx._ib === "undefined") { | ||
try { | ||
let [block, value] = step(ctx); | ||
if (isPromiseLike(block)) { | ||
ctx._ib = block | ||
.catch((err) => { | ||
if (!(ctx._f & Updating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}) | ||
.finally(() => advance(ctx)); | ||
} | ||
if (isPromiseLike(value)) { | ||
ctx._iv = value; | ||
ctx._el._inf = value; | ||
} | ||
return value; | ||
} | ||
catch (err) { | ||
if (!(ctx._f & Updating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
throw err; | ||
} | ||
} | ||
else if (ctx._f & IsAsyncGen) { | ||
return ctx._iv; | ||
} | ||
else if (typeof ctx._eb === "undefined") { | ||
let resolve; | ||
ctx._eb = ctx._ib | ||
.then(() => { | ||
try { | ||
const [block, value] = step(ctx); | ||
resolve(value); | ||
if (isPromiseLike(value)) { | ||
ctx._el._inf = value; | ||
} | ||
if (isPromiseLike(block)) { | ||
return block.catch((err) => { | ||
if (!(ctx._f & Updating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}); | ||
} | ||
} | ||
catch (err) { | ||
if (!(ctx._f & Updating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
} | ||
}) | ||
.finally(() => advance(ctx)); | ||
ctx._ev = new Promise((resolve1) => (resolve = resolve1)); | ||
} | ||
return ctx._ev; | ||
} | ||
/** | ||
* The step function is responsible for executing the component and handling all the different component types. | ||
* This function is responsible for executing the component and handling all | ||
* the different component types. | ||
* | ||
* @returns A tuple [block, value] | ||
* block - A possible promise which represents the duration during which the component is blocked from updating. | ||
* block - A possible promise which represents the duration during which the | ||
* component is blocked from updating. | ||
* value - A possible promise resolving to the rendered value of children. | ||
* | ||
* @remarks | ||
* Each component type will block/unblock according to the type of the component. | ||
* Sync function components never block and will transparently pass updates to children. | ||
* Async function components and async generator components block while executing itself, but will not block for async children. | ||
* Sync generator components block while any children are executing, because they are expected to only resume when they’ve actually rendered. Additionally, they have no mechanism for awaiting async children. | ||
* Each component type will block according to the type of the component. | ||
* Sync function components never block and will transparently pass updates to | ||
* children. | ||
* Async function components and async generator components block while | ||
* executing itself, but will not block for async children. | ||
* Sync generator components block while any children are executing, because | ||
* they are expected to only resume when they’ve actually rendered. | ||
* Additionally, they have no mechanism for awaiting async children. | ||
*/ | ||
function step(ctx) { | ||
function stepCtx(ctx) { | ||
const el = ctx._el; | ||
if (ctx._f & Done) { | ||
if (ctx._f & IsDone) { | ||
return [undefined, getValue(el)]; | ||
} | ||
let initial = false; | ||
const initial = !ctx._it; | ||
try { | ||
ctx._f |= Executing; | ||
if (typeof ctx._it === "undefined") { | ||
initial = true; | ||
ctx._f |= IsExecuting; | ||
if (initial) { | ||
clearEventListeners(ctx); | ||
@@ -1332,6 +1261,6 @@ const result = el.tag.call(ctx, el.props); | ||
finally { | ||
ctx._f &= ~Executing; | ||
ctx._f &= ~IsExecuting; | ||
} | ||
let oldValue; | ||
if (typeof ctx._el._inf === "object") { | ||
if (ctx._el._inf) { | ||
oldValue = ctx._el._inf.then((value) => ctx._re.read(value), () => ctx._re.read(undefined)); | ||
@@ -1347,11 +1276,11 @@ } | ||
try { | ||
ctx._f |= Executing; | ||
ctx._f |= IsExecuting; | ||
iteration = ctx._it.next(oldValue); | ||
} | ||
catch (err) { | ||
ctx._f |= Done; | ||
ctx._f |= IsDone; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~Executing; | ||
ctx._f &= ~IsExecuting; | ||
} | ||
@@ -1365,8 +1294,8 @@ if (isPromiseLike(iteration)) { | ||
const value = iteration.then((iteration) => { | ||
if (!(ctx._f & Iterating)) { | ||
ctx._f &= ~Available; | ||
if (!(ctx._f & IsIterating)) { | ||
ctx._f &= ~IsAvailable; | ||
} | ||
ctx._f &= ~Iterating; | ||
ctx._f &= ~IsIterating; | ||
if (iteration.done) { | ||
ctx._f |= Done; | ||
ctx._f |= IsDone; | ||
} | ||
@@ -1384,3 +1313,3 @@ try { | ||
}, (err) => { | ||
ctx._f |= Done; | ||
ctx._f |= IsDone; | ||
throw err; | ||
@@ -1394,5 +1323,5 @@ }); | ||
} | ||
ctx._f &= ~Iterating; | ||
ctx._f &= ~IsIterating; | ||
if (iteration.done) { | ||
ctx._f |= Done; | ||
ctx._f |= IsDone; | ||
} | ||
@@ -1418,3 +1347,3 @@ let value; | ||
*/ | ||
function advance(ctx) { | ||
function advanceCtx(ctx) { | ||
// _ib: inflightBlock | ||
@@ -1428,79 +1357,94 @@ // _iv: inflightValue | ||
ctx._ev = undefined; | ||
if (ctx._f & IsAsyncGen && !(ctx._f & Done)) { | ||
run(ctx); | ||
if (ctx._f & IsAsyncGen && !(ctx._f & IsDone)) { | ||
runCtx(ctx); | ||
} | ||
} | ||
// TODO: generator components which throw errors should be recoverable | ||
function handleChildError(ctx, err) { | ||
if (ctx._f & Done || | ||
typeof ctx._it !== "object" || | ||
typeof ctx._it.throw !== "function") { | ||
throw err; | ||
/** | ||
* Enqueues and executes the component associated with the context. | ||
*/ | ||
function runCtx(ctx) { | ||
if (!ctx._ib) { | ||
try { | ||
let [block, value] = stepCtx(ctx); | ||
if (isPromiseLike(block)) { | ||
ctx._ib = block | ||
.catch((err) => { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}) | ||
.finally(() => advanceCtx(ctx)); | ||
} | ||
if (isPromiseLike(value)) { | ||
ctx._iv = value; | ||
ctx._el._inf = value; | ||
} | ||
return value; | ||
} | ||
catch (err) { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
throw err; | ||
} | ||
} | ||
resume(ctx); | ||
let iteration; | ||
try { | ||
ctx._f |= Executing; | ||
iteration = ctx._it.throw(err); | ||
else if (ctx._f & IsAsyncGen) { | ||
return ctx._iv; | ||
} | ||
catch (err) { | ||
ctx._f |= Done; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~Executing; | ||
} | ||
if (isPromiseLike(iteration)) { | ||
return iteration.then((iteration) => { | ||
if (iteration.done) { | ||
ctx._f |= Done; | ||
else if (!ctx._eb) { | ||
let resolve; | ||
ctx._eb = ctx._ib | ||
.then(() => { | ||
try { | ||
const [block, value] = stepCtx(ctx); | ||
resolve(value); | ||
if (isPromiseLike(value)) { | ||
ctx._el._inf = value; | ||
} | ||
if (isPromiseLike(block)) { | ||
return block.catch((err) => { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}); | ||
} | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
}, (err) => { | ||
ctx._f |= Done; | ||
throw err; | ||
}); | ||
catch (err) { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
} | ||
}) | ||
.finally(() => advanceCtx(ctx)); | ||
ctx._ev = new Promise((resolve1) => (resolve = resolve1)); | ||
} | ||
if (iteration.done) { | ||
ctx._f |= Done; | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
return ctx._ev; | ||
} | ||
function propagateError(ctx, err) { | ||
if (ctx === undefined) { | ||
throw err; | ||
/** | ||
* Called to make props available to the Context async iterator for async | ||
* generator components. | ||
*/ | ||
function resumeCtx(ctx) { | ||
if (ctx._oa) { | ||
ctx._oa(); | ||
ctx._oa = undefined; | ||
} | ||
let result; | ||
try { | ||
result = handleChildError(ctx, err); | ||
else { | ||
ctx._f |= IsAvailable; | ||
} | ||
catch (err) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
if (isPromiseLike(result)) { | ||
return result.catch((err) => propagateError(ctx._pa, err)); | ||
} | ||
return result; | ||
} | ||
function updateCtx(ctx) { | ||
ctx._f |= Updating; | ||
resume(ctx); | ||
return run(ctx); | ||
ctx._f |= IsUpdating; | ||
resumeCtx(ctx); | ||
return runCtx(ctx); | ||
} | ||
function updateCtxChildren(ctx, children) { | ||
let child; | ||
if (isNonStringIterable(children)) { | ||
child = createElement(Fragment, null, children); | ||
} | ||
else { | ||
child = children; | ||
} | ||
return updateChild(ctx._re, ctx._rt, // root | ||
return updateChildren(ctx._re, ctx._rt, // root | ||
ctx._ho, // host | ||
ctx, ctx._sc, // scope | ||
ctx._el, // element | ||
child); | ||
narrow(children)); | ||
} | ||
function commitCtx(ctx, value) { | ||
if (ctx._f & Unmounted) { | ||
if (ctx._f & IsUnmounted) { | ||
return; | ||
@@ -1517,3 +1461,3 @@ } | ||
} | ||
if (!(ctx._f & Updating)) { | ||
if (!(ctx._f & IsUpdating)) { | ||
const listeners = getListeners(ctx._pa, ctx._ho); | ||
@@ -1530,5 +1474,5 @@ if (listeners !== undefined && listeners.length > 0) { | ||
} | ||
// TODO: we don’t need to call arrange if none of the nodes have changed or moved | ||
// TODO: avoid calling arrange if none of the nodes have changed or moved | ||
const host = ctx._ho; | ||
if (host._f & Committed) { | ||
if (host._f & IsCommitted) { | ||
ctx._re.arrange(host, host.tag === Portal ? host.props.root : host._n, getChildValues(host)); | ||
@@ -1538,5 +1482,7 @@ } | ||
} | ||
ctx._f &= ~Updating; | ||
if (typeof ctx._ss === "object" && ctx._ss.size > 0) { | ||
// NOTE: We have to clear the set of callbacks before calling them, because a callback which refreshes the component would otherwise cause a stack overflow. | ||
ctx._f &= ~IsUpdating; | ||
if (ctx._ss && ctx._ss.size > 0) { | ||
// NOTE: We have to clear the set of callbacks before calling them, because | ||
// a callback which refreshes the component would otherwise cause a stack | ||
// overflow. | ||
const callbacks = Array.from(ctx._ss); | ||
@@ -1552,5 +1498,5 @@ ctx._ss.clear(); | ||
function unmountCtx(ctx) { | ||
ctx._f |= Unmounted; | ||
ctx._f |= IsUnmounted; | ||
clearEventListeners(ctx); | ||
if (typeof ctx._cs === "object") { | ||
if (ctx._cs) { | ||
const value = ctx._re.read(getValue(ctx._el)); | ||
@@ -1562,13 +1508,13 @@ for (const cleanup of ctx._cs) { | ||
} | ||
if (!(ctx._f & Done)) { | ||
ctx._f |= Done; | ||
resume(ctx); | ||
if (typeof ctx._it === "object" && typeof ctx._it.return === "function") { | ||
if (!(ctx._f & IsDone)) { | ||
ctx._f |= IsDone; | ||
resumeCtx(ctx); | ||
if (ctx._it && typeof ctx._it.return === "function") { | ||
let iteration; | ||
try { | ||
ctx._f |= Executing; | ||
ctx._f |= IsExecuting; | ||
iteration = ctx._it.return(); | ||
} | ||
finally { | ||
ctx._f &= ~Executing; | ||
ctx._f &= ~IsExecuting; | ||
} | ||
@@ -1581,4 +1527,112 @@ if (isPromiseLike(iteration)) { | ||
} | ||
/*** EVENT TARGET UTILITIES ***/ | ||
// EVENT PHASE CONSTANTS | ||
// https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase | ||
const NONE = 0; | ||
const CAPTURING_PHASE = 1; | ||
const AT_TARGET = 2; | ||
const BUBBLING_PHASE = 3; | ||
function normalizeOptions(options) { | ||
if (typeof options === "boolean") { | ||
return { capture: options }; | ||
} | ||
else if (options == null) { | ||
return {}; | ||
} | ||
return options; | ||
} | ||
function isEventTarget(value) { | ||
return (value != null && | ||
typeof value.addEventListener === "function" && | ||
typeof value.removeEventListener === "function" && | ||
typeof value.dispatchEvent === "function"); | ||
} | ||
function setEventProperty(ev, key, value) { | ||
Object.defineProperty(ev, key, { value, writable: false, configurable: true }); | ||
} | ||
/** | ||
* A function to reconstruct an array of every listener given a context and a | ||
* host element. | ||
* | ||
* @remarks | ||
* This function exploits the fact that contexts retain their nearest ancestor | ||
* host element. We can determine all the contexts which are directly listening | ||
* to an element by traversing up the context tree and checking that the host | ||
* element passed in matches the context’s host property. | ||
*/ | ||
function getListeners(ctx, host) { | ||
let listeners; | ||
while (ctx !== undefined && ctx._ho === host) { | ||
if (typeof ctx._ls !== "undefined") { | ||
listeners = (listeners || []).concat(ctx._ls); | ||
} | ||
ctx = ctx._pa; | ||
} | ||
return listeners; | ||
} | ||
function clearEventListeners(ctx) { | ||
if (ctx._ls && ctx._ls.length > 0) { | ||
for (const value of getChildValues(ctx._el)) { | ||
if (isEventTarget(value)) { | ||
for (const { type, callback, options } of ctx._ls) { | ||
value.removeEventListener(type, callback, options); | ||
} | ||
} | ||
} | ||
ctx._ls = undefined; | ||
} | ||
} | ||
/*** ERROR HANDLING UTILITIES ***/ | ||
// TODO: generator components which throw errors should be recoverable | ||
function handleChildError(ctx, err) { | ||
if (ctx._f & IsDone || !ctx._it || typeof ctx._it.throw !== "function") { | ||
throw err; | ||
} | ||
resumeCtx(ctx); | ||
let iteration; | ||
try { | ||
ctx._f |= IsExecuting; | ||
iteration = ctx._it.throw(err); | ||
} | ||
catch (err) { | ||
ctx._f |= IsDone; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~IsExecuting; | ||
} | ||
if (isPromiseLike(iteration)) { | ||
return iteration.then((iteration) => { | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
}, (err) => { | ||
ctx._f |= IsDone; | ||
throw err; | ||
}); | ||
} | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
} | ||
function propagateError(ctx, err) { | ||
if (ctx === undefined) { | ||
throw err; | ||
} | ||
let result; | ||
try { | ||
result = handleChildError(ctx, err); | ||
} | ||
catch (err) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
if (isPromiseLike(result)) { | ||
return result.catch((err) => propagateError(ctx._pa, err)); | ||
} | ||
return result; | ||
} | ||
export { Context, Copy, Element, Fragment, Portal, Raw, Renderer, cloneElement, createElement, isElement }; | ||
//# sourceMappingURL=crank.js.map |
@@ -8,3 +8,3 @@ import { Children, Context, Element as CrankElement, ElementValue, Renderer } from "./crank.js"; | ||
patch(el: CrankElement<string | symbol>, node: Element): void; | ||
arrange(el: CrankElement<string | symbol>, parent: Node, children: Array<Node | string>): void; | ||
arrange(el: CrankElement<string | symbol>, node: Node, children: Array<Node | string>): void; | ||
} | ||
@@ -11,0 +11,0 @@ export declare const renderer: DOMRenderer; |
33
dom.js
@@ -110,2 +110,5 @@ /// <reference types="./dom.d.ts" /> | ||
} | ||
else if (typeof value === "function" || typeof value === "object") { | ||
node[name] = value; | ||
} | ||
else if (!forceAttribute && !isSVG && name in node) { | ||
@@ -127,14 +130,14 @@ node[name] = value; | ||
} | ||
arrange(el, parent, children) { | ||
arrange(el, node, children) { | ||
if (el.tag === Portal && | ||
(parent == null || typeof parent.nodeType !== "number")) { | ||
throw new TypeError(`Portal root is not a node. Received: ${JSON.stringify(parent && parent.toString())}`); | ||
(node == null || typeof node.nodeType !== "number")) { | ||
throw new TypeError(`Portal root is not a node. Received: ${JSON.stringify(node && node.toString())}`); | ||
} | ||
if (!("innerHTML" in el.props) && | ||
(children.length !== 0 || parent.__cranky)) { | ||
(children.length !== 0 || node.__cranky)) { | ||
if (children.length === 0) { | ||
parent.textContent = ""; | ||
node.textContent = ""; | ||
return; | ||
} | ||
let oldChild = parent.firstChild; | ||
let oldChild = node.firstChild; | ||
let i = 0; | ||
@@ -153,3 +156,3 @@ while (oldChild !== null && i < children.length) { | ||
else { | ||
parent.insertBefore(document.createTextNode(newChild), oldChild); | ||
node.insertBefore(document.createTextNode(newChild), oldChild); | ||
} | ||
@@ -160,7 +163,7 @@ i++; | ||
const nextSibling = oldChild.nextSibling; | ||
parent.removeChild(oldChild); | ||
node.removeChild(oldChild); | ||
oldChild = nextSibling; | ||
} | ||
else { | ||
parent.insertBefore(newChild, oldChild); | ||
node.insertBefore(newChild, oldChild); | ||
i++; | ||
@@ -170,3 +173,3 @@ // TODO: this is an optimization for the js frameworks benchmark swap rows, but we need to think a little more about other cases like prepending. | ||
const nextSibling = oldChild.nextSibling; | ||
parent.removeChild(oldChild); | ||
node.removeChild(oldChild); | ||
oldChild = nextSibling; | ||
@@ -178,3 +181,3 @@ } | ||
const nextSibling = oldChild.nextSibling; | ||
parent.removeChild(oldChild); | ||
node.removeChild(oldChild); | ||
oldChild = nextSibling; | ||
@@ -184,3 +187,3 @@ } | ||
const newChild = children[i]; | ||
parent.appendChild(typeof newChild === "string" | ||
node.appendChild(typeof newChild === "string" | ||
? document.createTextNode(newChild) | ||
@@ -190,6 +193,6 @@ : newChild); | ||
if (children.length > 0) { | ||
parent.__cranky = true; | ||
node.__cranky = true; | ||
} | ||
else if (parent.__cranky) { | ||
parent.__cranky = false; | ||
else if (node.__cranky) { | ||
node.__cranky = false; | ||
} | ||
@@ -196,0 +199,0 @@ } |
{ | ||
"name": "@bikeshaving/crank", | ||
"version": "0.3.5", | ||
"version": "0.3.6", | ||
"description": "Write JSX-driven components with functions, promises and generators.", | ||
@@ -5,0 +5,0 @@ "homepage": "https://crank.js.org", |
@@ -5,3 +5,6 @@ /** | ||
* @remarks | ||
* Elements whose tags are strings or symbols are called “host” or “intrinsic” elements, and their behavior is determined by the renderer, while elements whose tags are components are called “component” elements, and their behavior is determined by the execution of the component. | ||
* Elements whose tags are strings or symbols are called “host” or “intrinsic” | ||
* elements, and their behavior is determined by the renderer, while elements | ||
* whose tags are components are called “component” elements, and their | ||
* behavior is determined by the execution of the component. | ||
*/ | ||
@@ -17,9 +20,15 @@ export declare type Tag = string | symbol | Component; | ||
* SPECIAL TAGS | ||
* | ||
* Crank provides a couple tags which have special meaning for the renderer. | ||
***/ | ||
/** | ||
* A special element tag for grouping multiple children within a parent. | ||
* A special tag for grouping multiple children within a parent. | ||
* | ||
* @remarks | ||
* All iterables which appear in the element tree are implicitly wrapped in fragments. The Fragment tag is just the empty string, and you can use the empty string in createElement calls/transpiler options to avoid having to reference the Fragment export directly. | ||
* All iterables which appear in the element tree are implicitly wrapped in a | ||
* fragment element. | ||
* | ||
* The tag is just the empty string, and you can use the empty string in | ||
* createElement calls or transpiler options to avoid having to reference this | ||
* export directly. | ||
*/ | ||
@@ -29,6 +38,10 @@ export declare const Fragment = ""; | ||
/** | ||
* A special element tag for creating a new element subtree with a different root, passed via the root prop. | ||
* A special tag for rendering into a root node passed via a root prop. | ||
* | ||
* @remarks | ||
* This element tag is useful for creating element trees with multiple roots. Renderer.prototype.render will implicitly wrap the children which have been passed in in an implicit Portal element. | ||
* This tag is useful for creating element trees with multiple roots, for | ||
* things like modals or tooltips. | ||
* | ||
* Renderer.prototype.render will implicitly wrap the passed in element tree in | ||
* an implicit Portal element. | ||
*/ | ||
@@ -38,6 +51,9 @@ export declare const Portal: any; | ||
/** | ||
* A special element tag which copies whatever child appeared previously in the element’s position. | ||
* A special tag which preserves whatever was previously rendered in the | ||
* element’s position. | ||
* | ||
* @remarks | ||
* Copy elements are useful when you want to prevent a subtree from updating as a performance optimization. | ||
* Copy elements are useful for when you want to prevent a subtree from | ||
* rerendering as a performance optimization. Copy elements can also be keyed, | ||
* in which case the previously rendered keyed element will be preserved. | ||
*/ | ||
@@ -47,6 +63,7 @@ export declare const Copy: any; | ||
/** | ||
* A special element tag for injecting raw nodes into an element tree via its value prop. | ||
* A special element tag for injecting raw nodes or strings via a value prop. | ||
* | ||
* @remarks | ||
* If the value prop is a string, Renderer.prototype.parse will be called on the string and the element’s rendered value will be the result. | ||
* If the value prop is a string, Renderer.prototype.parse will be called on | ||
* the string and the result of that method will be inserted. | ||
*/ | ||
@@ -59,3 +76,5 @@ export declare const Raw: any; | ||
* @remarks | ||
* Arbitrary objects can also be safely rendered but they will be converted to a string using the toString method. We exclude them from this type to catch potential mistakes. | ||
* Arbitrary objects can also be safely rendered, but will be converted to a | ||
* string using the toString method. We exclude them from this type to catch | ||
* potential mistakes. | ||
*/ | ||
@@ -66,10 +85,7 @@ export declare type Child = Element | string | number | boolean | null | undefined; | ||
/** | ||
* Describes all valid values of an element tree, including arbitrarily nested iterables of such values. | ||
* Describes all valid values of an element tree, including arbitrarily nested | ||
* iterables of such values. | ||
*/ | ||
export declare type Children = Child | ChildIterable; | ||
/** | ||
* All values in the element tree are narrowed from the union in Child to NarrowedChild during rendering. This greatly simplifies element diffing. | ||
*/ | ||
declare type NarrowedChild = Element | string | undefined; | ||
/** | ||
* Represents all functions which can be used as a component. | ||
@@ -83,5 +99,7 @@ * | ||
/** | ||
* Elements are the basic building blocks of Crank applications. They are JavaScript objects which are interpreted by special classes called renderers to produce and manage stateful nodes. | ||
* Elements are the basic building blocks of Crank applications. They are | ||
* JavaScript objects which are interpreted by special classes called renderers | ||
* to produce and manage stateful nodes. | ||
* | ||
* @typeparam TTag - the type of the tag of the element. | ||
* @typeparam TTag - The type of the tag of the element. | ||
* | ||
@@ -99,3 +117,4 @@ * @example | ||
* @remarks | ||
* Typically, you use the createElement function to create elements and not this class directly. | ||
* Typically, you use the createElement function to create elements rather than | ||
* instatiating this class directly. | ||
*/ | ||
@@ -105,3 +124,4 @@ export declare class Element<TTag extends Tag = Tag> { | ||
* @internal | ||
* A unique symbol to identify elements as elements across versions and realms, and to protect against basic injection attacks. | ||
* A unique symbol to identify elements as elements across versions and | ||
* realms, and to protect against basic injection attacks. | ||
* https://overreacted.io/why-do-react-elements-have-typeof-property/ | ||
@@ -116,14 +136,14 @@ */ | ||
/** | ||
* The tag of the element. Can be a function, string or symbol depending on the kind of element. | ||
* The tag of the element. Can be a string, symbol or function. | ||
*/ | ||
tag: TTag; | ||
/** | ||
* An object containing the “properties” of an element. These correspond to the attributes of the element when using JSX syntax. | ||
* | ||
* @remarks | ||
* The props of an object are passed to most renderer host methods, and as the first argument to components. | ||
* An object containing the “properties” of an element. These correspond to | ||
* the “attributes” of an element when using JSX syntax. | ||
*/ | ||
props: TagProps<TTag>; | ||
/** | ||
* A value which uniquely identifies an element from its siblings so that it can be added/updated/moved/removed by the identity of the key rather than its position within the parent. | ||
* A value which uniquely identifies an element from its siblings so that it | ||
* can be added/updated/moved/removed by the identity of the key rather than | ||
* its position within the parent. | ||
* | ||
@@ -135,3 +155,3 @@ * @remarks | ||
/** | ||
* A callback which is called with the element’s value when the value is committed. | ||
* A callback which is called with the element’s result when it is committed. | ||
* | ||
@@ -141,3 +161,3 @@ * @remarks | ||
*/ | ||
ref: Function | undefined; | ||
ref: ((value: unknown) => unknown) | undefined; | ||
/** | ||
@@ -153,3 +173,4 @@ * @internal | ||
* @remarks | ||
* Set by Renderer.prototype.create when the component is mounted. This property will only be set for host elements. | ||
* Set by Renderer.prototype.create when the component is mounted. | ||
* This property will only be set for host elements. | ||
*/ | ||
@@ -162,3 +183,4 @@ _n: any; | ||
* @remarks | ||
* Created and assigned by the Renderer for component elements when it mounts the element tree. | ||
* Created and assigned by the Renderer for component elements when it mounts | ||
* the element tree. | ||
*/ | ||
@@ -171,11 +193,15 @@ _ctx: Context<TagProps<TTag>> | undefined; | ||
* @remarks | ||
* Until an element commits for the first time, we show any previously rendered values in its place. | ||
* Until an element commits for the first time, we show any previously | ||
* rendered values in its place. This is mainly important when the nearest | ||
* host is rearranged concurrently. | ||
*/ | ||
_fb: Element | undefined; | ||
_fb: NarrowedChild; | ||
/** | ||
* @internal | ||
* inflightPromise - The current async run of the element. | ||
* inflight - The current async run of the element. | ||
* | ||
* @remarks | ||
* This value is used to make sure Copy element refs and as the yield value of async generator components with async children. It is unset when the element is committed. | ||
* This value is used to make sure Copy element refs fire at the correct | ||
* time, and is also used as the yield value of async generator components | ||
* with async children. It is unset when the element is committed. | ||
*/ | ||
@@ -185,6 +211,7 @@ _inf: Promise<any> | undefined; | ||
* @internal | ||
* onNewValues - the resolve function of a promise which represents the next child result. See the chase function for more info. | ||
* onvalues - The resolve function of a promise which represents the next | ||
* children result. | ||
*/ | ||
_onv: Function | undefined; | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref: Function | undefined); | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref: ((value: unknown) => unknown) | undefined); | ||
} | ||
@@ -196,6 +223,9 @@ export declare function isElement(value: any): value is Element; | ||
* @remarks | ||
* This function is usually used as a transpilation target for JSX transpilers, but it can also be called directly. It additionally extracts the crank-key and crank-ref props so they aren’t accessible to renderer methods or components, and assigns the children prop according to the arguments passed to the function. | ||
* This function is usually used as a transpilation target for JSX transpilers, | ||
* but it can also be called directly. It additionally extracts the crank-key | ||
* and crank-ref props so they aren’t accessible to renderer methods or | ||
* components, and assigns the children prop according to the arguments passed | ||
* to the function. | ||
*/ | ||
export declare function createElement<TTag extends Tag>(tag: TTag, props?: TagProps<TTag> | null | undefined, ...children: Array<unknown>): Element<TTag>; | ||
export declare function createElement<TTag extends Tag>(tag: TTag, props?: TagProps<TTag> | null | undefined, children?: unknown): Element<TTag>; | ||
/** | ||
@@ -205,7 +235,14 @@ * Clones a given element. Will also shallow copy the props object. | ||
* @remarks | ||
* Mainly used internally to make sure we don’t accidentally reuse elements in an element tree, because element have internal properties which are directly mutated by the renderer. | ||
* Mainly used internally to make sure we don’t accidentally reuse elements in | ||
* an element tree, because element have internal properties which are directly | ||
* mutated by the renderer. | ||
*/ | ||
export declare function cloneElement<TTag extends Tag>(el: Element<TTag>): Element<TTag>; | ||
/*** ELEMENT VALUE UTILITIES ***/ | ||
/*** ELEMENT UTILITIES ***/ | ||
/** | ||
* All values in the element tree are narrowed from the union in Child to | ||
* NarrowedChild during rendering, to simplify element diffing. | ||
*/ | ||
declare type NarrowedChild = Element | string | undefined; | ||
/** | ||
* A helper type which repesents all the possible rendered values of an element. | ||
@@ -216,12 +253,24 @@ * | ||
* @remarks | ||
* When asking the question, what is the value of a specific element, the answer varies depending on the tag of the element. For host or Raw elements, the answer is simply the nodes (DOM nodes in the case of the DOM renderer) created for the element. For fragments, the values are usually an array of nodes. For portals, the value is undefined, because a Portal element’s root and children are opaque to its parent. For components, the value can be any of the above, because the value of a component is determined by its children. Rendered values can also be strings or arrays of nodes and strings, in the case of component or fragment elements with strings for children. All of these possible values are reflected in this utility type. | ||
* When asking the question, what is the value of a specific element, the | ||
* answer varies depending on its tag. For host or Raw elements, the answer is | ||
* simply the nodes created for the element. For fragments, the values are | ||
* usually an array of nodes. For portals, the value is undefined, because a | ||
* Portal element’s root and children are opaque to its parent. For components, | ||
* the value can be any of the above, because the value of a component is | ||
* determined by its immediate children. Rendered values can also be strings or | ||
* arrays of nodes and strings, in the case of component or fragment elements | ||
* with strings for children. All of these possible values are reflected in | ||
* this utility type. | ||
*/ | ||
export declare type ElementValue<TNode> = Array<TNode | string> | TNode | string | undefined; | ||
/** | ||
* An abstract class which is subclassed to render to different target environments. This class is responsible for kicking off the rendering process, caching previous trees by root, and creating/mutating/disposing the nodes of the target environment. | ||
* An abstract class which is subclassed to render to different target | ||
* environments. This class is responsible for kicking off the rendering | ||
* process, caching previous trees by root, and creating, mutating and | ||
* disposing of nodes. | ||
* | ||
* @typeparam TNode - The type of the node for a specific rendering environment. | ||
* @typeparam TNode - The type of the node for a rendering environment. | ||
* @typeparam TScope - Data which is passed down the tree. | ||
* @typeparam TRoot - The type of the root for a specific rendering environment. | ||
* @typeparam TResult - The type of the exposed values. | ||
* @typeparam TRoot - The type of the root for a rendering environment. | ||
* @typeparam TResult - The type of exposed values. | ||
*/ | ||
@@ -238,17 +287,30 @@ export declare class Renderer<TNode, TScope, TRoot = TNode, TResult = ElementValue<TNode>> { | ||
* | ||
* @param children - An element tree. You can render null with a previously used root to delete the previously rendered element tree from the cache. | ||
* @param root - The node to be rendered into. The renderer will cache element trees per root. | ||
* @param ctx - An optional context that will be the ancestor context of all elements in the tree. Useful for connecting renderers which call each other so that events/provisions properly propagate. The context for a given root must be the same or an error will be thrown. | ||
* @param children - An element tree. You can render null with a previously | ||
* used root to delete the previously rendered element tree from the cache. | ||
* @param root - The node to be rendered into. The renderer will cache | ||
* element trees per root. | ||
* @param ctx - An optional context that will be the ancestor context of all | ||
* elements in the tree. Useful for connecting renderers which call each | ||
* other so that events/provisions properly propagate. The context for a | ||
* given root must be the same or an error will be thrown. | ||
* | ||
* @returns The read result of rendering the children, or a possible promise of the read result if the element tree renders asynchronously. | ||
* @returns The result of rendering the children, or a possible promise of | ||
* the result if the element tree renders asynchronously. | ||
*/ | ||
render(children: Children, root?: TRoot | undefined, ctx?: Context | undefined): Promise<TResult> | TResult; | ||
/** | ||
* Called when an element’s value is exposed via render, schedule, refresh, refs, or generator yield expressions. | ||
* Called when an element’s value is exposed via render, schedule, refresh, | ||
* refs, or generator yield expressions. | ||
* | ||
* @param value - The value of the element being read. Can be a node, a string, undefined, or an array of nodes and strings, depending on the element. | ||
* @returns Varies according to the specific renderer subclass. By default, it exposes the element’s value. | ||
* @param value - The value of the element being read. Can be a node, a | ||
* string, undefined, or an array of nodes and strings, depending on the | ||
* element. | ||
* | ||
* @returns Varies according to the specific renderer subclass. By default, | ||
* it exposes the element’s value. | ||
* | ||
* @remarks | ||
* This is useful for renderers which don’t want to expose their internal nodes. For instance, the HTML renderer will convert all internal nodes to strings. | ||
* This is useful for renderers which don’t want to expose their internal | ||
* nodes. For instance, the HTML renderer will convert all internal nodes to | ||
* strings. | ||
* | ||
@@ -261,3 +323,5 @@ */ | ||
* @remarks | ||
* Useful for passing data down the element tree. For instance, the DOM renderer uses this method to keep track of whether we’re in an SVG subtree. | ||
* Useful for passing data down the element tree. For instance, the DOM | ||
* renderer uses this method to keep track of whether we’re in an SVG | ||
* subtree. | ||
* | ||
@@ -267,6 +331,8 @@ * @param el - The host element. | ||
* | ||
* @returns The scope to be passed to create and scope for child host elements. | ||
* @returns The scope to be passed to create and scope for child host | ||
* elements. | ||
* | ||
* @remarks | ||
* This method sets the scope for child host elements, not the current host element. | ||
* This method sets the scope for child host elements, not the current host | ||
* element. | ||
*/ | ||
@@ -283,3 +349,6 @@ scope(el: Element<string | symbol>, scope: TScope | undefined): TScope; | ||
* @remarks | ||
* Rather than returning text nodes for whatever environment we’re rendering to, we defer that step for Renderer.prototype.arrange. We do this so that adjacent strings can be concatenated and the actual element tree can be rendered in a normalized form. | ||
* Rather than returning text nodes for whatever environment we’re rendering | ||
* to, we defer that step for Renderer.prototype.arrange. We do this so that | ||
* adjacent strings can be concatenated and the actual element tree can be | ||
* rendered in a normalized form. | ||
*/ | ||
@@ -310,3 +379,2 @@ escape(text: string, _scope: TScope): string; | ||
* @param node - The node associated with the host element. | ||
* @param scope - The current scope. | ||
* | ||
@@ -316,7 +384,8 @@ * @returns The return value is ignored. | ||
* @remarks | ||
* Used to mutate the node associated with an element when new props are passed. | ||
* Used to mutate the node associated with an element when new props are | ||
* passed. | ||
*/ | ||
patch(_el: Element<string | symbol>, _node: TNode): unknown; | ||
/** | ||
* Called for each host element after its children have committed with the actual values of the children. | ||
* Called for each host element so that elements can be arranged into a tree. | ||
* | ||
@@ -330,10 +399,10 @@ * @param el - The host element. | ||
* @remarks | ||
* This method is also called by child components contexts as the last step of a refresh. | ||
* This method is also called by child components contexts as the last step | ||
* of a refresh. | ||
*/ | ||
arrange(_el: Element<string | symbol>, _parent: TNode | TRoot, _children: Array<TNode | string>): unknown; | ||
arrange(_el: Element<string | symbol>, _node: TNode | TRoot, _children: Array<TNode | string>): unknown; | ||
/** | ||
* Called for each host element when it is unmounted. | ||
* | ||
* @param tag - The tag of the host element. | ||
* @param props - The props of the host element. | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
@@ -354,27 +423,21 @@ * | ||
/** | ||
* A map of event type strings to Event subclasses. Can be extended via TypeScript module augmentation to have strongly typed event listeners. | ||
* An interface which can be extended to provide strongly typed provisions (see | ||
* Context.prototype.get and Context.prototype.set) | ||
*/ | ||
export interface EventMap extends Crank.EventMap { | ||
[type: string]: Event; | ||
export interface ProvisionMap extends Crank.ProvisionMap { | ||
} | ||
declare type MappedEventListener<T extends string> = (ev: EventMap[T]) => unknown; | ||
declare type MappedEventListenerOrEventListenerObject<T extends string> = MappedEventListener<T> | { | ||
handleEvent: MappedEventListener<T>; | ||
}; | ||
interface EventListenerRecord { | ||
type: string; | ||
listener: MappedEventListenerOrEventListenerObject<any>; | ||
callback: MappedEventListener<any>; | ||
options: AddEventListenerOptions; | ||
export interface Context extends Crank.Context { | ||
} | ||
/** | ||
* An interface which can be extended to provide strongly typed provisions (see Context.prototype.get and Context.prototype.set) | ||
* A class which is instantiated and passed to every component as its this | ||
* value. Contexts form a tree just like elements and all components in the | ||
* element tree are connected via contexts. Components can use this tree to | ||
* communicate data upwards via events and downwards via provisions. | ||
* | ||
* @typeparam TProps - The expected shape of the props passed to the component. | ||
* Used to strongly type the Context iterator methods. | ||
* @typeparam TResult - The readable element value type. It is used in places | ||
* such as the return value of refresh and the argument passed to | ||
* schedule/cleanup callbacks. | ||
*/ | ||
export interface ProvisionMap extends Crank.ProvisionMap { | ||
} | ||
/** | ||
* A class which is instantiated and passed to every component as its this value. Contexts form a tree just like elements and all components in the element tree are connected via contexts. Components can use this tree to communicate data upwards via events and downwards via provisions. | ||
* @typeparam TProps - The expected shape of the props passed to the component. Used to strongly type the Context iterator methods. | ||
* @typeparam TResult - The readable element value type. It is used in places such as the return value of refresh and the argument passed to schedule/cleanup callbacks. | ||
*/ | ||
export declare class Context<TProps = any, TResult = any> implements EventTarget { | ||
@@ -393,3 +456,3 @@ /** | ||
* @internal | ||
* root - The root node set by an ancestor’s Portal prop. | ||
* root - The root node as set by the nearest ancestor portal. | ||
*/ | ||
@@ -401,3 +464,5 @@ _rt: unknown; | ||
* @remarks | ||
* When refresh is called, the host element will be arranged as the last step of the commit, to make sure the parent’s children properly reflects the components’s children. | ||
* When refresh is called, the host element will be arranged as the last step | ||
* of the commit, to make sure the parent’s children properly reflects the | ||
* components’s children. | ||
*/ | ||
@@ -427,5 +492,7 @@ _ho: Element<string | symbol>; | ||
* @internal | ||
* onProps - A callback used in conjunction with the Available flag to implement the props async iterator. See the Symbol.asyncIterator method and the resume function. | ||
* onavailable - A callback used in conjunction with the IsAvailable flag to | ||
* implement the props async iterator. See the Symbol.asyncIterator method | ||
* and the resumeCtx function. | ||
*/ | ||
_op: ((props: any) => unknown) | undefined; | ||
_oa: (() => unknown) | undefined; | ||
/** | ||
@@ -453,3 +520,4 @@ * @internal | ||
* @internal | ||
* listeners - An array of event listeners added to the context via Context.prototype.addEventListener | ||
* listeners - An array of event listeners added to the context via | ||
* Context.prototype.addEventListener | ||
*/ | ||
@@ -459,3 +527,4 @@ _ls: Array<EventListenerRecord> | undefined; | ||
* @internal | ||
* provisions - A map of values which can be set via Context.prototype.set and read from child contexts via Context.prototype.get | ||
* provisions - A map of values which can be set via Context.prototype.set | ||
* and read from child contexts via Context.prototype.get | ||
*/ | ||
@@ -465,3 +534,4 @@ _ps: Map<unknown, unknown> | undefined; | ||
* @internal | ||
* schedules - a set of callbacks registered via Context.prototype.schedule, which fire when the component has committed. | ||
* schedules - a set of callbacks registered via Context.prototype.schedule, | ||
* which fire when the component has committed. | ||
*/ | ||
@@ -471,3 +541,4 @@ _ss: Set<(value: TResult) => unknown> | undefined; | ||
* @internal | ||
* cleanups - a set of callbacks registered via Context.prototype.cleanup, which fire when the component has unmounted. | ||
* cleanups - a set of callbacks registered via Context.prototype.cleanup, | ||
* which fire when the component has unmounted. | ||
*/ | ||
@@ -479,6 +550,2 @@ _cs: Set<(value: TResult) => unknown> | undefined; | ||
constructor(renderer: Renderer<unknown, unknown, unknown, TResult>, root: unknown, host: Element<string | symbol>, parent: Context<unknown, TResult> | undefined, scope: unknown, el: Element<Component>); | ||
consume<TKey extends keyof ProvisionMap>(key: TKey): ProvisionMap[TKey]; | ||
consume(key: unknown): any; | ||
provide<TKey extends keyof ProvisionMap>(key: TKey, value: ProvisionMap[TKey]): void; | ||
provide(key: unknown, value: any): void; | ||
/** | ||
@@ -488,3 +555,5 @@ * The current props of the associated element. | ||
* @remarks | ||
* Typically, you should read props either via the first parameter of the component or via the context iterator methods. This property is mainly for plugins or utilities which wrap contexts. | ||
* Typically, you should read props either via the first parameter of the | ||
* component or via the context iterator methods. This property is mainly for | ||
* plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -496,3 +565,5 @@ get props(): TProps; | ||
* @remarks | ||
* Typically, you should read values via refs, generator yield expressions, or the refresh, schedule or cleanup methods. This property is mainly for plugins or utilities which wrap contexts. | ||
* Typically, you should read values via refs, generator yield expressions, | ||
* or the refresh, schedule or cleanup methods. This property is mainly for | ||
* plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -505,16 +576,26 @@ get value(): TResult; | ||
* | ||
* @returns The rendered value of the component or a promise of the rendered value if the component or its children execute asynchronously. | ||
* @returns The rendered value of the component or a promise of the rendered | ||
* value if the component or its children execute asynchronously. | ||
* | ||
* @remarks | ||
* The refresh method works a little differently for async generator components, in that it will resume the Context async iterator rather than resuming execution. This is because async generator components are perpetually resumed independent of updates/refresh. | ||
* The refresh method works a little differently for async generator | ||
* components, in that it will resume the Context async iterator rather than | ||
* resuming execution. This is because async generator components are | ||
* perpetually resumed independent of updates/refresh. | ||
*/ | ||
refresh(): Promise<TResult> | TResult; | ||
/** | ||
* Registers a callback which fires when the component commits. Will only fire once per callback and update. | ||
* Registers a callback which fires when the component commits. Will only | ||
* fire once per callback and update. | ||
*/ | ||
schedule(callback: (value: TResult) => unknown): void; | ||
/** | ||
* Registers a callback which fires when the component unmounts. Will only fire once per callback. | ||
* Registers a callback which fires when the component unmounts. Will only | ||
* fire once per callback. | ||
*/ | ||
cleanup(callback: (value: TResult) => unknown): void; | ||
consume<TKey extends keyof ProvisionMap>(key: TKey): ProvisionMap[TKey]; | ||
consume(key: unknown): any; | ||
provide<TKey extends keyof ProvisionMap>(key: TKey, value: ProvisionMap[TKey]): void; | ||
provide(key: unknown, value: any): void; | ||
addEventListener<T extends string>(type: T, listener: MappedEventListenerOrEventListenerObject<T> | null, options?: boolean | AddEventListenerOptions): void; | ||
@@ -524,4 +605,19 @@ removeEventListener<T extends string>(type: T, listener: MappedEventListenerOrEventListenerObject<T> | null, options?: EventListenerOptions | boolean): void; | ||
} | ||
export interface Context extends Crank.Context { | ||
/** | ||
* A map of event type strings to Event subclasses. Can be extended via | ||
* TypeScript module augmentation to have strongly typed event listeners. | ||
*/ | ||
export interface EventMap extends Crank.EventMap { | ||
[type: string]: Event; | ||
} | ||
declare type MappedEventListener<T extends string> = (ev: EventMap[T]) => unknown; | ||
declare type MappedEventListenerOrEventListenerObject<T extends string> = MappedEventListener<T> | { | ||
handleEvent: MappedEventListener<T>; | ||
}; | ||
interface EventListenerRecord { | ||
type: string; | ||
listener: MappedEventListenerOrEventListenerObject<any>; | ||
callback: MappedEventListener<any>; | ||
options: AddEventListenerOptions; | ||
} | ||
declare global { | ||
@@ -528,0 +624,0 @@ module Crank { |
@@ -8,3 +8,3 @@ import { Children, Context, Element as CrankElement, ElementValue, Renderer } from "./crank"; | ||
patch(el: CrankElement<string | symbol>, node: Element): void; | ||
arrange(el: CrankElement<string | symbol>, parent: Node, children: Array<Node | string>): void; | ||
arrange(el: CrankElement<string | symbol>, node: Node, children: Array<Node | string>): void; | ||
} | ||
@@ -11,0 +11,0 @@ export declare const renderer: DOMRenderer; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
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
7627
291256