@bikeshaving/crank
Advanced tools
Comparing version 0.3.11 to 0.4.0-beta.1
# Changelog | ||
## [0.4.0] - 2021-08-19 | ||
### Added | ||
- The `Context.prototype.flush()` method has been added. It behaves similarly to `Context.prototype.schedule()`, with the exception that it runs after a component’s children is definitely in the DOM. This is important for things like focusing after render. See #180 for motivation. | ||
- The `crank-skip` prop has been added as an alternative to `<Copy />` elements. See #173 for motivation and `src/__tests__/static.tsx` for examples. | ||
### Changed | ||
- Properties and styles which are missing will now be removed. | ||
Crank 0.3 tried to implement uncontrolled properties by saying missing props were uncontrolled. For instance, `<div class="class" />` rerendered as `<div />` would preserve the class property even though it was removed. Starting in 0.4, missing props and styles will be removed from DOM elements between renders. | ||
- Crank will now log a console error when `undefined` is yielded or returned from a component. To squash these warnings, yield or return `null` instead. | ||
- The internal Renderer API has been overhauled yet again. | ||
- All internal methods which are implemented by the various renderers (`create()`, `patch()`, `arrange()`) have been removed from the base renderer class. Instead, you will now have to pass in these methods via a call to `super()` in the constructor. See `src/dom.ts` or `src/html.ts` for examples. | ||
- The `complete()` method has been renamed to `flush()`. | ||
- `patch()` now runs per prop as opposed to passing all props. | ||
- `patch()` now runs in a post-order call of the tree (yet again). | ||
- The signatures of all of the methods have been changed, mainly to avoid passing elements into the renderer, and allow for previous values to be inspected and used. | ||
- The default type for `TagProps` is now `Record<string, unknown>` as opposed to `unknown`. | ||
- Crank will no longer attempt to reuse or modify elements. Motivated by #198. | ||
- Internal context properties have been hidden using a symbol. | ||
### Fixed | ||
- Assigning to `boolean` properties with strings like `spellcheck="true"` will | ||
now work as expected. See #175 for motivation. | ||
## [0.3.11] - 2021-05-11 | ||
@@ -3,0 +23,0 @@ ### Fixed |
/** | ||
* A type which represents all valid values for an element tag. | ||
* | ||
* 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 functions are called “component” elements, and their | ||
* behavior is determined by the execution of the component function. | ||
*/ | ||
@@ -15,3 +10,3 @@ export declare type Tag = string | symbol | Component; | ||
*/ | ||
export declare type TagProps<TTag extends Tag> = TTag extends string ? JSX.IntrinsicElements[TTag] : TTag extends Component<infer TProps> ? TProps : unknown; | ||
export declare type TagProps<TTag extends Tag> = TTag extends string ? JSX.IntrinsicElements[TTag] : TTag extends Component<infer TProps> ? TProps : Record<string, unknown>; | ||
/*** | ||
@@ -93,26 +88,10 @@ * SPECIAL TAGS | ||
*/ | ||
export declare type Component<TProps = any> = (this: Context<TProps>, props: TProps) => Children | PromiseLike<Children> | Iterator<Children, Children | void, any> | AsyncIterator<Children, Children | void, any>; | ||
export declare type Component<TProps extends Record<string, unknown> = any> = (this: Context<TProps>, props: TProps) => Children | PromiseLike<Children> | Iterator<Children, Children | void, any> | AsyncIterator<Children, Children | void, any>; | ||
/** | ||
* A type to keep track of keys. Any value can be a key, though null and | ||
* undefined are ignored. | ||
*/ | ||
declare type Key = unknown; | ||
declare const ElementSymbol: unique symbol; | ||
/** | ||
* 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. | ||
* | ||
* @template {Tag} [TTag=Tag] - The type of the tag of the element. | ||
* | ||
* @example | ||
* // specific element types | ||
* let div: Element<"div">; | ||
* let portal: Element<Portal>; | ||
* let myEl: Element<MyComponent>; | ||
* | ||
* // general element types | ||
* let host: Element<string | symbol>; | ||
* let component: Element<Component>; | ||
* | ||
* Typically, you use a helper function like createElement to create elements | ||
* rather than instatiating this class directly. | ||
*/ | ||
export declare class Element<TTag extends Tag = Tag> { | ||
export interface Element<TTag extends Tag = Tag> { | ||
/** | ||
@@ -150,55 +129,27 @@ * @internal | ||
ref: ((value: unknown) => unknown) | undefined; | ||
/** | ||
* @internal | ||
* flags - A bitmask. See ELEMENT FLAGS. | ||
*/ | ||
_f: number; | ||
/** | ||
* @internal | ||
* children - The rendered children of the element. | ||
*/ | ||
_ch: Array<NarrowedChild> | NarrowedChild; | ||
/** | ||
* @internal | ||
* node - The node or context associated with the element. | ||
* | ||
* For host elements, this property is set to the return value of | ||
* Renderer.prototype.create when the component is mounted, i.e. DOM nodes | ||
* for the DOM renderer. | ||
* | ||
* For component elements, this property is set to a Context instance | ||
* (Context<TagProps<TTag>>). | ||
* | ||
* We assign both of these to the same property because they are mutually | ||
* exclusive. We use any because the Element type has no knowledge of | ||
* renderer nodes. | ||
*/ | ||
_n: any; | ||
/** | ||
* @internal | ||
* fallback - The element which this element is replacing. | ||
* | ||
* If an element renders asynchronously, we show any previously rendered | ||
* values in its place until it has committed for the first time. This | ||
* property is set to the previously rendered child. | ||
*/ | ||
_fb: NarrowedChild; | ||
/** | ||
* @internal | ||
* inflightChildren - The current async run of the element’s children. | ||
* | ||
* This property is used to make sure Copy element refs fire at the correct | ||
* time, and is also used to create yield values for async generator | ||
* components with async children. It is unset when the element is committed. | ||
*/ | ||
_ic: Promise<any> | undefined; | ||
/** | ||
* @internal | ||
* onvalue(s) - This property is set to the resolve function of a promise | ||
* which represents the next children, so that renderings can be raced. | ||
*/ | ||
_ov: Function | undefined; | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref: ((value: unknown) => unknown) | undefined); | ||
get hadChildren(): boolean; | ||
static_: boolean | undefined; | ||
} | ||
/** | ||
* 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. | ||
* | ||
* @template {Tag} [TTag=Tag] - The type of the tag of the element. | ||
* | ||
* @example | ||
* // specific element types | ||
* let div: Element<"div">; | ||
* let portal: Element<Portal>; | ||
* let myEl: Element<MyComponent>; | ||
* | ||
* // general element types | ||
* let host: Element<string | symbol>; | ||
* let component: Element<Component>; | ||
* | ||
* Typically, you use a helper function like createElement to create elements | ||
* rather than instatiating this class directly. | ||
*/ | ||
export declare class Element<TTag extends Tag = Tag> { | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref?: ((value: unknown) => unknown) | undefined, static_?: boolean | undefined); | ||
} | ||
export declare function isElement(value: any): value is Element; | ||
@@ -217,17 +168,8 @@ /** | ||
* Clones a given element, shallowly copying the props object. | ||
* | ||
* Used internally to make sure we don’t accidentally reuse elements when | ||
* rendering. | ||
*/ | ||
export declare function cloneElement<TTag extends Tag>(el: Element<TTag>): Element<TTag>; | ||
/*** 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. | ||
* A helper type which repesents all possible rendered values of an element. | ||
* | ||
* @template TNode - The node type for the element assigned by the renderer. | ||
* @template TNode - The node type for the element provided by the renderer. | ||
* | ||
@@ -253,37 +195,18 @@ * When asking the question, what is the “value” of a specific element, the | ||
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 and | ||
* disposing of nodes. | ||
* | ||
* @template TNode - The type of the node for a rendering environment. | ||
* @template TScope - Data which is passed down the tree. | ||
* @template TRoot - The type of the root for a rendering environment. | ||
* @template TResult - The type of exposed values. | ||
*/ | ||
export declare class Renderer<TNode, TScope, TRoot = TNode, TResult = ElementValue<TNode>> { | ||
declare type RetainerChild<TNode> = Retainer<TNode> | string | undefined; | ||
declare class Retainer<TNode> { | ||
el: Element; | ||
ctx: ContextInternals<TNode> | undefined; | ||
children: Array<RetainerChild<TNode>> | RetainerChild<TNode>; | ||
value: TNode | string | undefined; | ||
cached: ElementValue<TNode>; | ||
fallback: RetainerChild<TNode>; | ||
inflight: Promise<ElementValue<TNode>> | undefined; | ||
onCommit: Function | undefined; | ||
constructor(el: Element); | ||
} | ||
export interface RendererImpl<TNode, TScope, TRoot extends TNode = TNode, TResult = ElementValue<TNode>> { | ||
scope<TTag extends string | symbol>(scope: TScope | undefined, tag: TTag, props: TagProps<TTag>): TScope | undefined; | ||
create<TTag extends string | symbol>(tag: TTag, props: TagProps<TTag>, scope: TScope | undefined): TNode; | ||
/** | ||
* @internal | ||
* A weakmap which stores element trees by root. | ||
*/ | ||
_cache: WeakMap<object, Element<Portal>>; | ||
constructor(); | ||
/** | ||
* Renders an element tree into a specific root. | ||
* | ||
* @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 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 rendered value is exposed via render, schedule, | ||
@@ -305,19 +228,2 @@ * refresh, refs, or generator yield expressions. | ||
/** | ||
* Called in a preorder traversal for each host element. | ||
* | ||
* 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. | ||
* | ||
* @param el - The host element. | ||
* @param scope - The current scope. | ||
* | ||
* @returns The scope to be passed to create and scope for child host | ||
* elements. | ||
* | ||
* This method sets the scope for child host elements, not the current host | ||
* element. | ||
*/ | ||
scope(_el: Element<string | symbol>, scope: TScope | undefined): TScope; | ||
/** | ||
* Called for each string in an element tree. | ||
@@ -335,3 +241,3 @@ * | ||
*/ | ||
escape(text: string, _scope: TScope): string; | ||
escape(text: string, scope: TScope | undefined): string; | ||
/** | ||
@@ -345,54 +251,42 @@ * Called for each Raw element whose value prop is a string. | ||
*/ | ||
parse(text: string, _scope: TScope): TNode | string; | ||
parse(text: string, scope: TScope | undefined): TNode | string; | ||
patch<TTag extends string | symbol, TName extends string>(tag: TTag, node: TNode, name: TName, value: TagProps<TTag>[TName], oldValue: TagProps<TTag>[TName] | undefined, scope: TScope): unknown; | ||
arrange<TTag extends string | symbol>(tag: TTag, node: TNode, props: TagProps<TTag>, children: Array<TNode | string>, oldProps: TagProps<TTag> | undefined, oldChildren: Array<TNode | string> | undefined): unknown; | ||
dispose<TTag extends string | symbol>(tag: TTag, node: TNode, props: TagProps<TTag>): unknown; | ||
flush(root: TRoot): unknown; | ||
} | ||
/** | ||
* An abstract class which is subclassed to render to different target | ||
* environments. This class is responsible for kicking off the rendering | ||
* process and caching previous trees by root. | ||
* | ||
* @template TNode - The type of the node for a rendering environment. | ||
* @template TScope - Data which is passed down the tree. | ||
* @template TRoot - The type of the root for a rendering environment. | ||
* @template TResult - The type of exposed values. | ||
*/ | ||
export declare class Renderer<TNode extends object = object, TScope = unknown, TRoot extends TNode = TNode, TResult = ElementValue<TNode>> { | ||
/** | ||
* Called for each host element when it is committed for the first time. | ||
* | ||
* @param el - The host element. | ||
* @param scope - The current scope. | ||
* | ||
* @returns A “node” which determines the value of the host element. | ||
* @internal | ||
* A weakmap which stores element trees by root. | ||
*/ | ||
create(_el: Element<string | symbol>, _scope: TScope): TNode; | ||
cache: WeakMap<object, Retainer<TNode>>; | ||
impl: RendererImpl<TNode, TScope, TRoot, TResult>; | ||
constructor(impl: Partial<RendererImpl<TNode, TScope, TRoot, TResult>>); | ||
/** | ||
* Called for each host element when it is committed. | ||
* Renders an element tree into a specific root. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* @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 different renderers so that | ||
* events/provisions properly propagate. The context for a given root must be | ||
* the same or an error will be thrown. | ||
* | ||
* @returns The return value is ignored. | ||
* | ||
* Used to mutate the node associated with an element when new props are | ||
* passed. | ||
* @returns The result of rendering the children, or a possible promise of | ||
* the result if the element tree renders asynchronously. | ||
*/ | ||
patch(_el: Element<string | symbol>, _node: TNode): unknown; | ||
/** | ||
* Called for each host element so that elements can be arranged into a tree. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* @param children - An array of nodes and strings from child elements. | ||
* | ||
* @returns The return value is ignored. | ||
* | ||
* This method is also called by child components contexts as the last step | ||
* of a refresh. | ||
*/ | ||
arrange(_el: Element<string | symbol>, _node: TNode | TRoot, _children: Array<TNode | string>): unknown; | ||
/** | ||
* Called for each host element when it is unmounted. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* | ||
* @returns The return value is ignored. | ||
*/ | ||
dispose(_el: Element<string | symbol>, _node: TNode): unknown; | ||
/** | ||
* Called at the end of the rendering process for each root of the tree. | ||
* | ||
* @param root - The root prop passed to portals or the render method. | ||
* | ||
* @returns The return value is ignored. | ||
*/ | ||
complete(_root: TRoot): unknown; | ||
render(children: Children, root?: TRoot | undefined, bridge?: Context | undefined): Promise<TResult> | TResult; | ||
} | ||
@@ -408,32 +302,23 @@ export interface Context extends Crank.Context { | ||
/** | ||
* 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. | ||
* | ||
* @template [TProps=*] - The expected shape of the props passed to the | ||
* component. Used to strongly type the Context iterator methods. | ||
* @template [TResult=*] - The readable element value type. It is used in | ||
* places such as the return value of refresh and the argument passed to | ||
* schedule and cleanup callbacks. | ||
* @internal | ||
*/ | ||
export declare class Context<TProps = any, TResult = any> implements EventTarget { | ||
declare class ContextInternals<TNode = unknown, TScope = unknown, TRoot extends TNode = TNode, TResult = unknown> { | ||
/** | ||
* @internal | ||
* flags - A bitmask. See CONTEXT FLAGS above. | ||
*/ | ||
_f: number; | ||
f: number; | ||
/** | ||
* @internal | ||
* facade - The actual object passed as this to components. | ||
*/ | ||
facade: Context<unknown, TResult>; | ||
/** | ||
* renderer - The renderer which created this context. | ||
*/ | ||
_re: Renderer<unknown, unknown, unknown, TResult>; | ||
renderer: RendererImpl<TNode, TScope, TRoot, TResult>; | ||
/** | ||
* @internal | ||
* root - The root node as set by the nearest ancestor portal. | ||
*/ | ||
_rt: unknown; | ||
root: TRoot | undefined; | ||
/** | ||
* @internal | ||
* host - The nearest ancestor host element. | ||
* host - The nearest host or portal retainer. | ||
* | ||
@@ -444,56 +329,63 @@ * When refresh is called, the host element will be arranged as the last step | ||
*/ | ||
_ho: Element<string | symbol>; | ||
host: Retainer<TNode>; | ||
/** | ||
* @internal | ||
* parent - The parent context. | ||
*/ | ||
_pa: Context<unknown, TResult> | undefined; | ||
parent: ContextInternals<TNode, TScope, TRoot, TResult> | undefined; | ||
/** | ||
* @internal | ||
* scope - The value of the scope at the point of element’s creation. | ||
*/ | ||
_sc: unknown; | ||
scope: TScope | undefined; | ||
/** | ||
* @internal | ||
* el - The associated component element. | ||
* retainer - The internal node associated with this context. | ||
*/ | ||
_el: Element<Component>; | ||
ret: Retainer<TNode>; | ||
/** | ||
* @internal | ||
* iterator - The iterator returned by the component function. | ||
*/ | ||
_it: Iterator<Children, Children | void, unknown> | AsyncIterator<Children, Children | void, unknown> | undefined; | ||
iterator: Iterator<Children, Children | void, unknown> | AsyncIterator<Children, Children | void, unknown> | undefined; | ||
/*** async properties ***/ | ||
/** | ||
* @internal | ||
* 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. | ||
*/ | ||
_oa: Function | undefined; | ||
/** | ||
* @internal | ||
* inflightBlock | ||
*/ | ||
_ib: Promise<unknown> | undefined; | ||
inflightBlock: Promise<unknown> | undefined; | ||
/** | ||
* @internal | ||
* inflightValue | ||
*/ | ||
_iv: Promise<ElementValue<any>> | undefined; | ||
inflightValue: Promise<ElementValue<TNode>> | undefined; | ||
/** | ||
* @internal | ||
* enqueuedBlock | ||
*/ | ||
_eb: Promise<unknown> | undefined; | ||
enqueuedBlock: Promise<unknown> | undefined; | ||
/** | ||
* @internal | ||
* enqueuedValue | ||
*/ | ||
_ev: Promise<ElementValue<any>> | undefined; | ||
enqueuedValue: Promise<ElementValue<TNode>> | undefined; | ||
/** | ||
* onavailable - A callback used in conjunction with the IsAvailable flag to | ||
* implement the props async iterator. See the Symbol.asyncIterator method | ||
* and the resumeCtxIterator function. | ||
*/ | ||
onAvailable: Function | undefined; | ||
constructor(renderer: RendererImpl<TNode, TScope, TRoot, TResult>, root: TRoot | undefined, host: Retainer<TNode>, parent: ContextInternals<TNode, TScope, TRoot, TResult> | undefined, scope: TScope | undefined, ret: Retainer<TNode>); | ||
} | ||
export declare const ContextInternalsSymbol: unique symbol; | ||
/** | ||
* 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. | ||
* | ||
* @template [TProps=*] - The expected shape of the props passed to the | ||
* component. Used to strongly type the Context iterator methods. | ||
* @template [TResult=*] - The readable element value type. It is used in | ||
* places such as the return value of refresh and the argument passed to | ||
* schedule and cleanup callbacks. | ||
*/ | ||
export declare class Context<TProps = any, TResult = any> implements EventTarget { | ||
/** | ||
* @internal | ||
* Contexts should never be instantiated directly. | ||
*/ | ||
constructor(renderer: Renderer<unknown, unknown, unknown, TResult>, root: unknown, host: Element<string | symbol>, parent: Context<unknown, TResult> | undefined, scope: unknown, el: Element<Component>); | ||
[ContextInternalsSymbol]: ContextInternals<unknown, unknown, unknown, TResult>; | ||
constructor(internals: ContextInternals<unknown, unknown, unknown, TResult>); | ||
/** | ||
@@ -511,4 +403,4 @@ * The current props of the associated element. | ||
* 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. | ||
* or the refresh, schedule, cleanup, or flush methods. This property is | ||
* mainly for plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -537,2 +429,7 @@ get value(): TResult; | ||
/** | ||
* Registers a callback which fires when the component’s children are | ||
* rendered into the root. Will only fire once per callback and render. | ||
*/ | ||
flush(callback: (value: TResult) => unknown): void; | ||
/** | ||
* Registers a callback which fires when the component unmounts. Will only | ||
@@ -539,0 +436,0 @@ * fire once per callback. |
1762
cjs/crank.js
@@ -6,2 +6,3 @@ 'use strict'; | ||
const NOOP = () => { }; | ||
const IDENTITY = (value) => value; | ||
function wrap(value) { | ||
@@ -81,25 +82,3 @@ return value === undefined ? [] : Array.isArray(value) ? value : [value]; | ||
const ElementSymbol = Symbol.for("crank.Element"); | ||
/*** ELEMENT FLAGS ***/ | ||
/** | ||
* A flag which is set when the element is mounted, used to detect whether an | ||
* element is being reused so that we clone it rather than accidentally | ||
* overwriting its state. | ||
* | ||
* Changing this flag value would likely be a breaking changes in terms of | ||
* interop between elements and renderers of different versions of Crank. | ||
* | ||
* TODO: Consider deleting this flag because we’re not using it anymore. | ||
*/ | ||
const IsInUse = 1 << 0; | ||
/** | ||
* A flag which tracks whether the element has previously rendered children, | ||
* used to clear elements which no longer render children in the next render. | ||
* We may deprecate this behavior and make elements without explicit children | ||
* uncontrolled. | ||
*/ | ||
const HadChildren = 1 << 1; | ||
// 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. | ||
/** | ||
* Elements are the basic building blocks of Crank applications. They are | ||
@@ -125,4 +104,3 @@ * JavaScript objects which are interpreted by special classes called renderers | ||
class Element { | ||
constructor(tag, props, key, ref) { | ||
this._f = 0; | ||
constructor(tag, props, key, ref, static_) { | ||
this.tag = tag; | ||
@@ -132,11 +110,4 @@ this.props = props; | ||
this.ref = ref; | ||
this._ch = undefined; | ||
this._n = undefined; | ||
this._fb = undefined; | ||
this._ic = undefined; | ||
this._ov = undefined; | ||
this.static_ = static_; | ||
} | ||
get hadChildren() { | ||
return (this._f & HadChildren) !== 0; | ||
} | ||
} | ||
@@ -159,2 +130,3 @@ Element.prototype.$$typeof = ElementSymbol; | ||
let ref; | ||
let static_ = false; | ||
const props1 = {}; | ||
@@ -176,2 +148,5 @@ if (props != null) { | ||
break; | ||
case "crank-static": | ||
static_ = !!props["crank-static"]; | ||
break; | ||
default: | ||
@@ -188,9 +163,6 @@ props1[name] = props[name]; | ||
} | ||
return new Element(tag, props1, key, ref); | ||
return new Element(tag, props1, key, ref, static_); | ||
} | ||
/** | ||
* Clones a given element, shallowly copying the props object. | ||
* | ||
* Used internally to make sure we don’t accidentally reuse elements when | ||
* rendering. | ||
*/ | ||
@@ -264,2 +236,14 @@ function cloneElement(el) { | ||
} | ||
class Retainer { | ||
constructor(el) { | ||
this.el = el; | ||
this.value = undefined; | ||
this.ctx = undefined; | ||
this.children = undefined; | ||
this.cached = undefined; | ||
this.fallback = undefined; | ||
this.inflight = undefined; | ||
this.onCommit = undefined; | ||
} | ||
} | ||
/** | ||
@@ -270,29 +254,17 @@ * Finds the value of the element according to its type. | ||
*/ | ||
function getValue(el) { | ||
if (typeof el._fb !== "undefined") { | ||
return typeof el._fb === "object" ? getValue(el._fb) : el._fb; | ||
function getValue(ret) { | ||
if (typeof ret.fallback !== "undefined") { | ||
return typeof ret.fallback === "object" | ||
? getValue(ret.fallback) | ||
: ret.fallback; | ||
} | ||
else if (el.tag === Portal) { | ||
return undefined; | ||
else if (ret.el.tag === Portal) { | ||
return; | ||
} | ||
else if (typeof el.tag !== "function" && el.tag !== Fragment) { | ||
return el._n; | ||
else if (typeof ret.el.tag !== "function" && ret.el.tag !== Fragment) { | ||
return ret.value; | ||
} | ||
return unwrap(getChildValues(el)); | ||
return unwrap(getChildValues(ret)); | ||
} | ||
/** | ||
* This function is only used to make sure <Copy /> elements wait for the | ||
* current run of async elements, but it’s somewhat complex so I put it here. | ||
*/ | ||
function getInflightValue(el) { | ||
const ctx = typeof el.tag === "function" ? el._n : undefined; | ||
if (ctx && ctx._f & IsUpdating && ctx._iv) { | ||
return ctx._iv; // inflightValue | ||
} | ||
else if (el._ic) { | ||
return el._ic; // inflightChildren | ||
} | ||
return getValue(el); | ||
} | ||
/** | ||
* Walks an element’s children to find its child values. | ||
@@ -302,5 +274,8 @@ * | ||
*/ | ||
function getChildValues(el) { | ||
function getChildValues(ret) { | ||
if (ret.cached) { | ||
return wrap(ret.cached); | ||
} | ||
const values = []; | ||
const children = wrap(el._ch); | ||
const children = wrap(ret.children); | ||
for (let i = 0; i < children.length; i++) { | ||
@@ -312,9 +287,26 @@ const child = children[i]; | ||
} | ||
return normalize(values); | ||
const values1 = normalize(values); | ||
const tag = ret.el.tag; | ||
if (typeof tag === "function" || (tag !== Fragment && tag !== Raw)) { | ||
ret.cached = unwrap(values1); | ||
} | ||
return values1; | ||
} | ||
const defaultRendererImpl = { | ||
create() { | ||
throw new Error("Not implemented"); | ||
}, | ||
scope: IDENTITY, | ||
read: IDENTITY, | ||
escape: IDENTITY, | ||
parse: IDENTITY, | ||
patch: NOOP, | ||
arrange: NOOP, | ||
dispose: NOOP, | ||
flush: NOOP, | ||
}; | ||
/** | ||
* 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. | ||
* process and caching previous trees by root. | ||
* | ||
@@ -327,4 +319,8 @@ * @template TNode - The type of the node for a rendering environment. | ||
class Renderer { | ||
constructor() { | ||
this._cache = new WeakMap(); | ||
constructor(impl) { | ||
this.cache = new WeakMap(); | ||
this.impl = { | ||
...defaultRendererImpl, | ||
...impl, | ||
}; | ||
} | ||
@@ -339,5 +335,5 @@ /** | ||
* @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. | ||
* elements in the tree. Useful for connecting different renderers so that | ||
* events/provisions properly propagate. The context for a given root must be | ||
* the same or an error will be thrown. | ||
* | ||
@@ -347,343 +343,177 @@ * @returns The result of rendering the children, or a possible promise of | ||
*/ | ||
render(children, root, ctx) { | ||
let portal; | ||
render(children, root, bridge) { | ||
let ret; | ||
const ctx = bridge && bridge[ContextInternalsSymbol]; | ||
if (typeof root === "object" && root !== null) { | ||
portal = this._cache.get(root); | ||
ret = this.cache.get(root); | ||
} | ||
if (portal === undefined) { | ||
portal = createElement(Portal, { children, root }); | ||
portal._n = ctx; | ||
let oldProps; | ||
if (ret === undefined) { | ||
ret = new Retainer(createElement(Portal, { children, root })); | ||
ret.value = root; | ||
ret.ctx = ctx; | ||
if (typeof root === "object" && root !== null && children != null) { | ||
this._cache.set(root, portal); | ||
this.cache.set(root, ret); | ||
} | ||
} | ||
else if (ret.ctx !== ctx) { | ||
throw new Error("Context mismatch"); | ||
} | ||
else { | ||
if (portal._n !== ctx) { | ||
throw new Error("Context mismatch"); | ||
} | ||
portal.props = { children, root }; | ||
oldProps = ret.el.props; | ||
ret.el = createElement(Portal, { children, root }); | ||
if (typeof root === "object" && root !== null && children == null) { | ||
this._cache.delete(root); | ||
this.cache.delete(root); | ||
} | ||
} | ||
const value = update(this, root, portal, ctx, undefined, portal); | ||
const scope = this.impl.scope(undefined, Portal, ret.el.props); | ||
const childValues = diffChildren(this.impl, root, ret, ctx, scope, ret, children); | ||
// We return the child values of the portal because portal elements | ||
// themselves have no readable value. | ||
if (isPromiseLike(value)) { | ||
return value.then(() => { | ||
const result = this.read(unwrap(getChildValues(portal))); | ||
if (root == null) { | ||
unmount(this, portal, undefined, portal); | ||
} | ||
return result; | ||
}); | ||
if (isPromiseLike(childValues)) { | ||
return childValues.then((childValues) => commitRootRender(this.impl, root, ctx, ret, childValues, oldProps)); | ||
} | ||
const result = this.read(unwrap(getChildValues(portal))); | ||
if (root == null) { | ||
unmount(this, portal, undefined, portal); | ||
} | ||
return result; | ||
return commitRootRender(this.impl, root, ctx, ret, childValues, oldProps); | ||
} | ||
/** | ||
* Called when an element’s rendered 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. | ||
* | ||
* 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. | ||
*/ | ||
read(value) { | ||
return value; | ||
} | ||
/** | ||
* Called in a preorder traversal for each host element. | ||
* | ||
* 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. | ||
* | ||
* @param el - The host element. | ||
* @param scope - The current scope. | ||
* | ||
* @returns The scope to be passed to create and scope for child host | ||
* elements. | ||
* | ||
* This method sets the scope for child host elements, not the current host | ||
* element. | ||
*/ | ||
scope(_el, scope) { | ||
return scope; | ||
} | ||
/** | ||
* Called for each string in an element tree. | ||
* | ||
* @param text - The string child. | ||
* @param scope - The current scope. | ||
* | ||
* @returns The escaped string. | ||
* | ||
* 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. | ||
*/ | ||
escape(text, _scope) { | ||
return text; | ||
} | ||
/** | ||
* Called for each Raw element whose value prop is a string. | ||
* | ||
* @param text - The string child. | ||
* @param scope - The current scope. | ||
* | ||
* @returns The parsed node or string. | ||
*/ | ||
parse(text, _scope) { | ||
return text; | ||
} | ||
/** | ||
* Called for each host element when it is committed for the first time. | ||
* | ||
* @param el - The host element. | ||
* @param scope - The current scope. | ||
* | ||
* @returns A “node” which determines the value of the host element. | ||
*/ | ||
create(_el, _scope) { | ||
throw new Error("Not implemented"); | ||
} | ||
/** | ||
* Called for each host element when it is committed. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* | ||
* @returns The return value is ignored. | ||
* | ||
* Used to mutate the node associated with an element when new props are | ||
* passed. | ||
*/ | ||
patch(_el, _node) { | ||
return; | ||
} | ||
// TODO: pass hints into arrange about where the dirty children start and end | ||
/** | ||
* Called for each host element so that elements can be arranged into a tree. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* @param children - An array of nodes and strings from child elements. | ||
* | ||
* @returns The return value is ignored. | ||
* | ||
* This method is also called by child components contexts as the last step | ||
* of a refresh. | ||
*/ | ||
arrange(_el, _node, _children) { | ||
return; | ||
} | ||
// 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 el - The host element. | ||
* @param node - The node associated with the host element. | ||
* | ||
* @returns The return value is ignored. | ||
*/ | ||
dispose(_el, _node) { | ||
return; | ||
} | ||
/** | ||
* Called at the end of the rendering process for each root of the tree. | ||
* | ||
* @param root - The root prop passed to portals or the render method. | ||
* | ||
* @returns The return value is ignored. | ||
*/ | ||
complete(_root) { | ||
return; | ||
} | ||
} | ||
/*** PRIVATE RENDERER FUNCTIONS ***/ | ||
function mount(renderer, root, host, ctx, scope, el) { | ||
el._f |= IsInUse; | ||
if (typeof el.tag === "function") { | ||
el._n = new Context(renderer, root, host, ctx, scope, el); | ||
return updateCtx(el._n); | ||
function commitRootRender(renderer, root, ctx, ret, childValues, oldProps) { | ||
// element is a host or portal element | ||
if (root !== undefined) { | ||
renderer.arrange(Portal, root, ret.el.props, childValues, oldProps, wrap(ret.cached)); | ||
flush(renderer, root); | ||
} | ||
else if (el.tag === Raw) { | ||
return commit(renderer, scope, el, []); | ||
ret.cached = unwrap(childValues); | ||
if (root == null) { | ||
unmount(renderer, ret, ctx, ret); | ||
} | ||
else if (el.tag !== Fragment) { | ||
if (el.tag === Portal) { | ||
root = el.props.root; | ||
} | ||
else { | ||
el._n = renderer.create(el, scope); | ||
renderer.patch(el, el._n); | ||
} | ||
host = el; | ||
scope = renderer.scope(host, scope); | ||
} | ||
return updateChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
return renderer.read(ret.cached); | ||
} | ||
function update(renderer, root, host, ctx, scope, el) { | ||
if (typeof el.tag === "function") { | ||
return updateCtx(el._n); | ||
} | ||
else if (el.tag === Raw) { | ||
return commit(renderer, scope, el, []); | ||
} | ||
else if (el.tag !== Fragment) { | ||
if (el.tag === Portal) { | ||
root = el.props.root; | ||
} | ||
else { | ||
renderer.patch(el, el._n); | ||
} | ||
host = el; | ||
scope = renderer.scope(host, scope); | ||
} | ||
return updateChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
} | ||
function createChildrenByKey(children) { | ||
const childrenByKey = new Map(); | ||
for (let i = 0; i < children.length; i++) { | ||
const child = children[i]; | ||
if (typeof child === "object" && typeof child.key !== "undefined") { | ||
childrenByKey.set(child.key, child); | ||
} | ||
} | ||
return childrenByKey; | ||
} | ||
function updateChildren(renderer, root, host, ctx, scope, el, children) { | ||
const oldChildren = wrap(el._ch); | ||
function diffChildren(renderer, root, host, ctx, scope, parent, children) { | ||
const oldRetained = wrap(parent.children); | ||
const newRetained = []; | ||
const newChildren = arrayify(children); | ||
const newChildren1 = []; | ||
const values = []; | ||
let graveyard; | ||
let childrenByKey; | ||
let seenKeys; | ||
let childrenByKey; | ||
let isAsync = false; | ||
let i = 0; | ||
for (let j = 0, il = oldChildren.length, jl = newChildren.length; j < jl; j++) { | ||
let oldChild = i >= il ? undefined : oldChildren[i]; | ||
let newChild = narrow(newChildren[j]); | ||
// ALIGNMENT | ||
let oldKey = typeof oldChild === "object" ? oldChild.key : undefined; | ||
let newKey = typeof newChild === "object" ? newChild.key : undefined; | ||
if (newKey !== undefined && seenKeys && seenKeys.has(newKey)) { | ||
console.error("Duplicate key", newKey); | ||
newKey = undefined; | ||
} | ||
if (oldKey === newKey) { | ||
if (childrenByKey !== undefined && newKey !== undefined) { | ||
childrenByKey.delete(newKey); | ||
let oi = 0, oldLength = oldRetained.length; | ||
for (let ni = 0, newLength = newChildren.length; ni < newLength; ni++) { | ||
// We make sure we don’t access indices out of bounds to prevent | ||
// deoptimizations. | ||
let ret = oi >= oldLength ? undefined : oldRetained[oi]; | ||
let child = narrow(newChildren[ni]); | ||
{ | ||
// Aligning new children with old retainers | ||
let oldKey = typeof ret === "object" ? ret.el.key : undefined; | ||
let newKey = typeof child === "object" ? child.key : undefined; | ||
if (newKey !== undefined && seenKeys && seenKeys.has(newKey)) { | ||
console.error("Duplicate key", newKey); | ||
newKey = undefined; | ||
} | ||
i++; | ||
} | ||
else { | ||
if (!childrenByKey) { | ||
childrenByKey = createChildrenByKey(oldChildren.slice(i)); | ||
} | ||
if (newKey === undefined) { | ||
while (oldChild !== undefined && oldKey !== undefined) { | ||
i++; | ||
oldChild = oldChildren[i]; | ||
oldKey = typeof oldChild === "object" ? oldChild.key : undefined; | ||
if (oldKey === newKey) { | ||
if (childrenByKey !== undefined && newKey !== undefined) { | ||
childrenByKey.delete(newKey); | ||
} | ||
i++; | ||
oi++; | ||
} | ||
else { | ||
oldChild = childrenByKey.get(newKey); | ||
if (oldChild !== undefined) { | ||
childrenByKey.delete(newKey); | ||
childrenByKey = childrenByKey || createChildrenByKey(oldRetained, oi); | ||
if (newKey === undefined) { | ||
while (ret !== undefined && oldKey !== undefined) { | ||
oi++; | ||
ret = oldRetained[oi]; | ||
oldKey = typeof ret === "object" ? ret.el.key : undefined; | ||
} | ||
oi++; | ||
} | ||
if (!seenKeys) { | ||
seenKeys = new Set(); | ||
else { | ||
ret = childrenByKey.get(newKey); | ||
if (ret !== undefined) { | ||
childrenByKey.delete(newKey); | ||
} | ||
(seenKeys = seenKeys || new Set()).add(newKey); | ||
} | ||
seenKeys.add(newKey); | ||
} | ||
} | ||
// UPDATING | ||
// Updating | ||
let value; | ||
if (typeof oldChild === "object" && | ||
typeof newChild === "object" && | ||
oldChild.tag === newChild.tag) { | ||
if (oldChild.tag === Portal && | ||
oldChild.props.root !== newChild.props.root) { | ||
renderer.arrange(oldChild, oldChild.props.root, []); | ||
renderer.complete(oldChild.props.root); | ||
if (typeof child === "object") { | ||
if (typeof ret === "object" && child.static_) { | ||
ret.el = child; | ||
value = getInflightValue(ret); | ||
} | ||
// TODO: implement Raw element parse caching | ||
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) { | ||
value = | ||
typeof oldChild === "object" | ||
? getInflightValue(oldChild) | ||
: oldChild; | ||
if (typeof newChild.ref === "function") { | ||
if (isPromiseLike(value)) { | ||
value.then(newChild.ref).catch(NOOP); | ||
else if (child.tag === Copy) { | ||
value = getInflightValue(ret); | ||
} | ||
else { | ||
let oldProps; | ||
if (typeof ret === "object" && ret.el.tag === child.tag) { | ||
oldProps = ret.el.props; | ||
ret.el = child; | ||
} | ||
else { | ||
if (typeof ret === "object") { | ||
(graveyard = graveyard || []).push(ret); | ||
} | ||
else { | ||
newChild.ref(value); | ||
} | ||
const fallback = ret; | ||
ret = new Retainer(child); | ||
ret.fallback = fallback; | ||
} | ||
newChild = oldChild; | ||
if (child.tag === Raw) { | ||
value = updateRaw(renderer, ret, scope, oldProps); | ||
} | ||
else if (child.tag === Fragment) { | ||
value = updateFragment(renderer, root, host, ctx, scope, ret); | ||
} | ||
else if (typeof child.tag === "function") { | ||
value = updateComponent(renderer, root, host, ctx, scope, ret, oldProps); | ||
} | ||
else { | ||
value = updateHost(renderer, root, ctx, scope, ret, oldProps); | ||
} | ||
} | ||
else { | ||
newChild = new Element(newChild.tag, newChild.props, newChild.key, newChild.ref); | ||
value = mount(renderer, root, host, ctx, scope, newChild); | ||
if (isPromiseLike(value)) { | ||
newChild._fb = oldChild; | ||
const ref = child.ref; | ||
if (isPromiseLike(value)) { | ||
isAsync = true; | ||
if (typeof ref === "function") { | ||
value = value.then((value) => { | ||
ref(renderer.read(value)); | ||
return value; | ||
}); | ||
} | ||
} | ||
else if (typeof ref === "function") { | ||
ref(renderer.read(value)); | ||
} | ||
} | ||
else if (typeof newChild === "string") { | ||
newChild = value = renderer.escape(newChild, scope); | ||
} | ||
newChildren1[j] = newChild; | ||
values[j] = value; | ||
isAsync = isAsync || isPromiseLike(value); | ||
if (typeof oldChild === "object" && oldChild !== newChild) { | ||
if (!graveyard) { | ||
graveyard = []; | ||
else { | ||
// child is a string or undefined | ||
if (typeof ret === "object") { | ||
(graveyard = graveyard || []).push(ret); | ||
} | ||
graveyard.push(oldChild); | ||
if (typeof child === "string") { | ||
value = ret = renderer.escape(child, scope); | ||
} | ||
else { | ||
ret = undefined; | ||
} | ||
} | ||
values[ni] = value; | ||
newRetained[ni] = ret; | ||
} | ||
el._ch = unwrap(newChildren1); | ||
// cleanup | ||
for (; i < oldChildren.length; i++) { | ||
const oldChild = oldChildren[i]; | ||
if (typeof oldChild === "object" && typeof oldChild.key === "undefined") { | ||
if (!graveyard) { | ||
graveyard = []; | ||
} | ||
graveyard.push(oldChild); | ||
// cleanup remaining retainers | ||
for (; oi < oldLength; oi++) { | ||
const ret = oldRetained[oi]; | ||
if (typeof ret === "object" && typeof ret.el.key === "undefined") { | ||
(graveyard = graveyard || []).push(ret); | ||
} | ||
} | ||
if (childrenByKey !== undefined && childrenByKey.size > 0) { | ||
if (!graveyard) { | ||
graveyard = []; | ||
} | ||
graveyard.push(...childrenByKey.values()); | ||
(graveyard = graveyard || []).push(...childrenByKey.values()); | ||
} | ||
parent.children = unwrap(newRetained); | ||
if (isAsync) { | ||
let values1 = Promise.all(values).finally(() => { | ||
let childValues1 = Promise.all(values).finally(() => { | ||
if (graveyard) { | ||
@@ -695,13 +525,15 @@ for (let i = 0; i < graveyard.length; i++) { | ||
}); | ||
let onvalues; | ||
values1 = Promise.race([ | ||
values1, | ||
new Promise((resolve) => (onvalues = resolve)), | ||
let onChildValues; | ||
childValues1 = Promise.race([ | ||
childValues1, | ||
new Promise((resolve) => (onChildValues = resolve)), | ||
]); | ||
if (el._ov) { | ||
el._ov(values1); | ||
if (parent.onCommit) { | ||
parent.onCommit(childValues1); | ||
} | ||
el._ov = onvalues; | ||
const children = (el._ic = values1.then((values) => commit(renderer, scope, el, normalize(values)))); | ||
return children; | ||
parent.onCommit = onChildValues; | ||
return childValues1.then((childValues) => { | ||
parent.inflight = parent.fallback = undefined; | ||
return normalize(childValues); | ||
}); | ||
} | ||
@@ -713,75 +545,157 @@ if (graveyard) { | ||
} | ||
if (el._ov) { | ||
el._ov(values); | ||
el._ov = undefined; | ||
if (parent.onCommit) { | ||
parent.onCommit(values); | ||
parent.onCommit = undefined; | ||
} | ||
return commit(renderer, scope, el, normalize(values)); | ||
parent.inflight = parent.fallback = undefined; | ||
// We can assert there are no promises in the array because isAsync is false | ||
return normalize(values); | ||
} | ||
function commit(renderer, scope, el, values) { | ||
if (el._ic) { | ||
el._ic = undefined; | ||
function createChildrenByKey(children, offset) { | ||
const childrenByKey = new Map(); | ||
for (let i = offset; i < children.length; i++) { | ||
const child = children[i]; | ||
if (typeof child === "object" && typeof child.el.key !== "undefined") { | ||
childrenByKey.set(child.el.key, child); | ||
} | ||
} | ||
// Need to handle (_fb) fallback being the empty string. | ||
if (typeof el._fb !== "undefined") { | ||
el._fb = undefined; | ||
return childrenByKey; | ||
} | ||
function getInflightValue(child) { | ||
if (typeof child !== "object") { | ||
return child; | ||
} | ||
let value; | ||
if (typeof el.tag === "function") { | ||
value = commitCtx(el._n, values); | ||
const ctx = typeof child.el.tag === "function" ? child.ctx : undefined; | ||
if (ctx && ctx.f & IsUpdating && ctx.inflightValue) { | ||
return ctx.inflightValue; | ||
} | ||
else if (el.tag === Raw) { | ||
if (typeof el.props.value === "string") { | ||
el._n = renderer.parse(el.props.value, scope); | ||
else if (child.inflight) { | ||
return child.inflight; | ||
} | ||
return getValue(child); | ||
} | ||
function updateRaw(renderer, ret, scope, oldProps) { | ||
const props = ret.el.props; | ||
if (typeof props.value === "string") { | ||
if (!oldProps || oldProps.value !== props.value) { | ||
ret.value = renderer.parse(props.value, scope); | ||
} | ||
else { | ||
el._n = el.props.value; | ||
} | ||
else { | ||
ret.value = props.value; | ||
} | ||
return ret.value; | ||
} | ||
function updateFragment(renderer, root, host, ctx, scope, ret) { | ||
const childValues = diffChildren(renderer, root, host, ctx, scope, ret, ret.el.props.children); | ||
if (isPromiseLike(childValues)) { | ||
ret.inflight = childValues.then((childValues) => unwrap(childValues)); | ||
return ret.inflight; | ||
} | ||
return unwrap(childValues); | ||
} | ||
function updateHost(renderer, root, ctx, scope, ret, oldProps) { | ||
const el = ret.el; | ||
const tag = el.tag; | ||
if (el.tag === Portal) { | ||
root = ret.value = el.props.root; | ||
} | ||
else if (!oldProps) { | ||
// We use the truthiness of oldProps to determine if this the first render. | ||
ret.value = renderer.create(tag, el.props, scope); | ||
} | ||
scope = renderer.scope(scope, tag, el.props); | ||
const childValues = diffChildren(renderer, root, ret, ctx, scope, ret, ret.el.props.children); | ||
if (isPromiseLike(childValues)) { | ||
ret.inflight = childValues.then((childValues) => commitHost(renderer, scope, ret, childValues, oldProps)); | ||
return ret.inflight; | ||
} | ||
return commitHost(renderer, scope, ret, childValues, oldProps); | ||
} | ||
function commitHost(renderer, scope, ret, childValues, oldProps) { | ||
const tag = ret.el.tag; | ||
const value = ret.value; | ||
let props = ret.el.props; | ||
let copied; | ||
if (tag !== Portal) { | ||
for (const propName in { ...oldProps, ...props }) { | ||
const propValue = props[propName]; | ||
if (propValue === Copy) { | ||
(copied = copied || new Set()).add(propName); | ||
} | ||
else if (propName !== "children") { | ||
renderer.patch(tag, value, propName, propValue, oldProps && oldProps[propName], scope); | ||
} | ||
} | ||
value = el._n; | ||
} | ||
else if (el.tag === Fragment) { | ||
value = unwrap(values); | ||
if (copied) { | ||
props = { ...ret.el.props }; | ||
for (const name of copied) { | ||
props[name] = oldProps && oldProps[name]; | ||
} | ||
ret.el = new Element(tag, props, ret.el.key, ret.el.ref); | ||
} | ||
else { | ||
if (el.tag === Portal) { | ||
renderer.arrange(el, el.props.root, values); | ||
renderer.complete(el.props.root); | ||
renderer.arrange(tag, value, props, childValues, oldProps, wrap(ret.cached)); | ||
ret.cached = unwrap(childValues); | ||
if (tag === Portal) { | ||
flush(renderer, ret.value); | ||
return; | ||
} | ||
return value; | ||
} | ||
function flush(renderer, root, initiator) { | ||
renderer.flush(root); | ||
if (typeof root !== "object" || root === null) { | ||
return; | ||
} | ||
const flushMap = flushMaps.get(root); | ||
if (flushMap) { | ||
if (initiator) { | ||
const flushMap1 = new Map(); | ||
for (let [ctx, callbacks] of flushMap) { | ||
if (!ctxContains(initiator, ctx)) { | ||
flushMap.delete(ctx); | ||
flushMap1.set(ctx, callbacks); | ||
} | ||
} | ||
if (flushMap1.size) { | ||
flushMaps.set(root, flushMap1); | ||
} | ||
else { | ||
flushMaps.delete(root); | ||
} | ||
} | ||
else { | ||
renderer.arrange(el, el._n, values); | ||
flushMaps.delete(root); | ||
} | ||
value = el._n; | ||
if (values.length) { | ||
el._f |= HadChildren; | ||
for (const [ctx, callbacks] of flushMap) { | ||
const value = renderer.read(getValue(ctx.ret)); | ||
for (const callback of callbacks) { | ||
callback(value); | ||
} | ||
} | ||
else { | ||
el._f &= ~HadChildren; | ||
} | ||
} | ||
if (el.ref) { | ||
el.ref(renderer.read(value)); | ||
} | ||
return value; | ||
} | ||
function unmount(renderer, host, ctx, el) { | ||
if (typeof el.tag === "function") { | ||
unmountCtx(el._n); | ||
ctx = el._n; | ||
function unmount(renderer, host, ctx, ret) { | ||
if (typeof ret.el.tag === "function") { | ||
ctx = ret.ctx; | ||
unmountComponent(ctx); | ||
} | ||
else if (el.tag === Portal) { | ||
host = el; | ||
renderer.arrange(host, host.props.root, []); | ||
renderer.complete(host.props.root); | ||
else if (ret.el.tag === Portal) { | ||
host = ret; | ||
renderer.arrange(Portal, host.value, host.el.props, [], host.el.props, wrap(host.cached)); | ||
flush(renderer, host.value); | ||
} | ||
else if (el.tag !== Fragment) { | ||
if (isEventTarget(el._n)) { | ||
const listeners = getListeners(ctx, host); | ||
for (let i = 0; i < listeners.length; i++) { | ||
const record = listeners[i]; | ||
el._n.removeEventListener(record.type, record.callback, record.options); | ||
else if (ret.el.tag !== Fragment) { | ||
if (isEventTarget(ret.value)) { | ||
const records = getListenerRecords(ctx, host); | ||
for (let i = 0; i < records.length; i++) { | ||
const record = records[i]; | ||
ret.value.removeEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
host = el; | ||
renderer.dispose(host, host._n); | ||
renderer.dispose(ret.el.tag, ret.value, ret.el.props); | ||
host = ret; | ||
} | ||
const children = wrap(el._ch); | ||
const children = wrap(ret.children); | ||
for (let i = 0; i < children.length; i++) { | ||
@@ -794,8 +708,7 @@ const child = children[i]; | ||
} | ||
// TODO: Now that we have element flags again, we should probably merge these flags. | ||
/*** 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. | ||
* cleared when the component has committed. Used to determine things like | ||
* whether the nearest host ancestor needs to be rearranged. | ||
*/ | ||
@@ -819,3 +732,3 @@ const IsUpdating = 1 << 0; | ||
* context async iterator. See the Symbol.asyncIterator method and the | ||
* resumeCtx function. | ||
* resumeCtxIterator function. | ||
*/ | ||
@@ -825,31 +738,65 @@ const IsAvailable = 1 << 3; | ||
* A flag which is set when a generator components returns, i.e. the done | ||
* property on the generator is set to true or throws. Done components will | ||
* stick to their last rendered value and ignore further updates. | ||
* property on the iteration is set to true. Generator components will stick to | ||
* their last rendered value and ignore further updates. | ||
*/ | ||
const IsDone = 1 << 4; | ||
/** | ||
* A flag which is set when a generator component errors. | ||
* | ||
* NOTE: This is mainly used to prevent some false positives in component | ||
* yields or returns undefined warnings. The reason we’re using this versus | ||
* IsUnmounted is a very troubling jest test (cascades sync generator parent | ||
* and sync generator child) where synchronous code causes a stack overflow | ||
* error in a non-deterministic way. Deeply disturbing stuff. | ||
*/ | ||
const IsErrored = 1 << 5; | ||
/** | ||
* A flag which is set when the component is unmounted. Unmounted components | ||
* are no longer in the element tree and cannot refresh or rerender. | ||
*/ | ||
const IsUnmounted = 1 << 5; | ||
const IsUnmounted = 1 << 6; | ||
/** | ||
* A flag which indicates that the component is a sync generator component. | ||
*/ | ||
const IsSyncGen = 1 << 6; | ||
const IsSyncGen = 1 << 7; | ||
/** | ||
* A flag which indicates that the component is an async generator component. | ||
*/ | ||
const IsAsyncGen = 1 << 7; | ||
const IsAsyncGen = 1 << 8; | ||
/** | ||
* A flag which is set while schedule callbacks are called. | ||
*/ | ||
const IsScheduling = 1 << 8; | ||
const IsScheduling = 1 << 9; | ||
/** | ||
* A flag which is set when a schedule callback calls refresh. | ||
*/ | ||
const IsSchedulingRefresh = 1 << 9; | ||
const IsSchedulingRefresh = 1 << 10; | ||
const provisionMaps = new WeakMap(); | ||
const scheduleMap = new WeakMap(); | ||
const cleanupMap = new WeakMap(); | ||
// keys are roots | ||
const flushMaps = new WeakMap(); | ||
/** | ||
* @internal | ||
*/ | ||
class ContextInternals { | ||
constructor(renderer, root, host, parent, scope, ret) { | ||
this.f = 0; | ||
this.facade = new Context(this); | ||
this.renderer = renderer; | ||
this.root = root; | ||
this.host = host; | ||
this.parent = parent; | ||
this.scope = scope; | ||
this.ret = ret; | ||
this.iterator = undefined; | ||
this.inflightBlock = undefined; | ||
this.inflightValue = undefined; | ||
this.enqueuedBlock = undefined; | ||
this.enqueuedValue = undefined; | ||
this.onAvailable = undefined; | ||
} | ||
} | ||
const ContextInternalsSymbol = Symbol.for("Crank.ContextInternals"); | ||
/** | ||
* A class which is instantiated and passed to every component as its this | ||
@@ -867,20 +814,4 @@ * value. Contexts form a tree just like elements and all components in the | ||
class Context { | ||
/** | ||
* @internal | ||
* Contexts should never be instantiated directly. | ||
*/ | ||
constructor(renderer, root, host, parent, scope, el) { | ||
this._f = 0; | ||
this._re = renderer; | ||
this._rt = root; | ||
this._ho = host; | ||
this._pa = parent; | ||
this._sc = scope; | ||
this._el = el; | ||
this._it = undefined; | ||
this._oa = undefined; | ||
this._ib = undefined; | ||
this._iv = undefined; | ||
this._eb = undefined; | ||
this._ev = undefined; | ||
constructor(internals) { | ||
this[ContextInternalsSymbol] = internals; | ||
} | ||
@@ -895,4 +826,5 @@ /** | ||
get props() { | ||
return this._el.props; | ||
return this[ContextInternalsSymbol].ret.el.props; | ||
} | ||
// TODO: Should we rename this??? | ||
/** | ||
@@ -902,18 +834,19 @@ * The current value of the associated element. | ||
* 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. | ||
* or the refresh, schedule, cleanup, or flush methods. This property is | ||
* mainly for plugins or utilities which wrap contexts. | ||
*/ | ||
get value() { | ||
return this._re.read(getValue(this._el)); | ||
return this[ContextInternalsSymbol].renderer.read(getValue(this[ContextInternalsSymbol].ret)); | ||
} | ||
*[Symbol.iterator]() { | ||
while (!(this._f & IsDone)) { | ||
if (this._f & IsIterating) { | ||
const internals = this[ContextInternalsSymbol]; | ||
while (!(internals.f & IsDone)) { | ||
if (internals.f & IsIterating) { | ||
throw new Error("Context iterated twice without a yield"); | ||
} | ||
else if (this._f & IsAsyncGen) { | ||
else if (internals.f & IsAsyncGen) { | ||
throw new Error("Use for await…of in async generator components"); | ||
} | ||
this._f |= IsIterating; | ||
yield this._el.props; | ||
internals.f |= IsIterating; | ||
yield internals.ret.el.props; | ||
} | ||
@@ -923,22 +856,24 @@ } | ||
// We use a do while loop rather than a while loop to handle an edge case | ||
// where an async generator component is unmounted synchronously. | ||
// where an async generator component is unmounted synchronously and | ||
// therefore “done” before it starts iterating over the context. | ||
const internals = this[ContextInternalsSymbol]; | ||
do { | ||
if (this._f & IsIterating) { | ||
if (internals.f & IsIterating) { | ||
throw new Error("Context iterated twice without a yield"); | ||
} | ||
else if (this._f & IsSyncGen) { | ||
else if (internals.f & IsSyncGen) { | ||
throw new Error("Use for…of in sync generator components"); | ||
} | ||
this._f |= IsIterating; | ||
if (this._f & IsAvailable) { | ||
this._f &= ~IsAvailable; | ||
internals.f |= IsIterating; | ||
if (internals.f & IsAvailable) { | ||
internals.f &= ~IsAvailable; | ||
} | ||
else { | ||
await new Promise((resolve) => (this._oa = resolve)); | ||
if (this._f & IsDone) { | ||
await new Promise((resolve) => (internals.onAvailable = resolve)); | ||
if (internals.f & IsDone) { | ||
break; | ||
} | ||
} | ||
yield this._el.props; | ||
} while (!(this._f & IsDone)); | ||
yield internals.ret.el.props; | ||
} while (!(internals.f & IsDone)); | ||
} | ||
@@ -958,12 +893,17 @@ /** | ||
refresh() { | ||
if (this._f & IsUnmounted) { | ||
const internals = this[ContextInternalsSymbol]; | ||
if (internals.f & IsUnmounted) { | ||
console.error("Component is unmounted"); | ||
return this._re.read(undefined); | ||
return internals.renderer.read(undefined); | ||
} | ||
else if (this._f & IsExecuting) { | ||
else if (internals.f & IsExecuting) { | ||
console.error("Component is already executing"); | ||
return this._re.read(undefined); | ||
return internals.renderer.read(undefined); | ||
} | ||
resumeCtx(this); | ||
return this._re.read(runCtx(this)); | ||
resumeCtxIterator(internals); | ||
const value = runComponent(internals); | ||
if (isPromiseLike(value)) { | ||
return value.then((value) => internals.renderer.read(value)); | ||
} | ||
return internals.renderer.read(value); | ||
} | ||
@@ -975,6 +915,7 @@ /** | ||
schedule(callback) { | ||
let callbacks = scheduleMap.get(this); | ||
const internals = this[ContextInternalsSymbol]; | ||
let callbacks = scheduleMap.get(internals); | ||
if (!callbacks) { | ||
callbacks = new Set(); | ||
scheduleMap.set(this, callbacks); | ||
scheduleMap.set(internals, callbacks); | ||
} | ||
@@ -984,2 +925,23 @@ callbacks.add(callback); | ||
/** | ||
* Registers a callback which fires when the component’s children are | ||
* rendered into the root. Will only fire once per callback and render. | ||
*/ | ||
flush(callback) { | ||
const internals = this[ContextInternalsSymbol]; | ||
if (typeof internals.root !== "object" || internals.root === null) { | ||
return; | ||
} | ||
let flushMap = flushMaps.get(internals.root); | ||
if (!flushMap) { | ||
flushMap = new Map(); | ||
flushMaps.set(internals.root, flushMap); | ||
} | ||
let callbacks = flushMap.get(internals); | ||
if (!callbacks) { | ||
callbacks = new Set(); | ||
flushMap.set(internals, callbacks); | ||
} | ||
callbacks.add(callback); | ||
} | ||
/** | ||
* Registers a callback which fires when the component unmounts. Will only | ||
@@ -989,6 +951,7 @@ * fire once per callback. | ||
cleanup(callback) { | ||
let callbacks = cleanupMap.get(this); | ||
const internals = this[ContextInternalsSymbol]; | ||
let callbacks = cleanupMap.get(internals); | ||
if (!callbacks) { | ||
callbacks = new Set(); | ||
cleanupMap.set(this, callbacks); | ||
cleanupMap.set(internals, callbacks); | ||
} | ||
@@ -998,3 +961,3 @@ callbacks.add(callback); | ||
consume(key) { | ||
for (let parent = this._pa; parent !== undefined; parent = parent._pa) { | ||
for (let parent = this[ContextInternalsSymbol].parent; parent !== undefined; parent = parent.parent) { | ||
const provisions = provisionMaps.get(parent); | ||
@@ -1007,6 +970,7 @@ if (provisions && provisions.has(key)) { | ||
provide(key, value) { | ||
let provisions = provisionMaps.get(this); | ||
const internals = this[ContextInternalsSymbol]; | ||
let provisions = provisionMaps.get(internals); | ||
if (!provisions) { | ||
provisions = new Map(); | ||
provisionMaps.set(this, provisions); | ||
provisionMaps.set(internals, provisions); | ||
} | ||
@@ -1016,161 +980,201 @@ provisions.set(key, value); | ||
addEventListener(type, listener, options) { | ||
let listeners; | ||
if (listener == null) { | ||
return; | ||
return addEventListener(this[ContextInternalsSymbol], type, listener, options); | ||
} | ||
removeEventListener(type, listener, options) { | ||
return removeEventListener(this[ContextInternalsSymbol], type, listener, options); | ||
} | ||
dispatchEvent(ev) { | ||
return dispatchEvent(this[ContextInternalsSymbol], ev); | ||
} | ||
} | ||
/*** PRIVATE CONTEXT FUNCTIONS ***/ | ||
function ctxContains(parent, child) { | ||
for (let current = child; current !== undefined; current = current.parent) { | ||
if (current === parent) { | ||
return true; | ||
} | ||
else { | ||
const listeners1 = listenersMap.get(this); | ||
if (listeners1) { | ||
listeners = listeners1; | ||
} | ||
return false; | ||
} | ||
function updateComponent(renderer, root, host, parent, scope, ret, oldProps) { | ||
let ctx; | ||
if (oldProps) { | ||
ctx = ret.ctx; | ||
} | ||
else { | ||
ctx = ret.ctx = new ContextInternals(renderer, root, host, parent, scope, ret); | ||
} | ||
ctx.f |= IsUpdating; | ||
resumeCtxIterator(ctx); | ||
return runComponent(ctx); | ||
} | ||
function updateComponentChildren(ctx, children) { | ||
if (ctx.f & IsUnmounted || ctx.f & IsErrored) { | ||
return; | ||
} | ||
else if (children === undefined) { | ||
console.error("A component has returned or yielded undefined. If this was intentional, return or yield null instead."); | ||
} | ||
const childValues = diffChildren(ctx.renderer, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children)); | ||
if (isPromiseLike(childValues)) { | ||
ctx.ret.inflight = childValues.then((childValues) => commitComponent(ctx, childValues)); | ||
return ctx.ret.inflight; | ||
} | ||
return commitComponent(ctx, childValues); | ||
} | ||
function commitComponent(ctx, values) { | ||
if (ctx.f & IsUnmounted) { | ||
return; | ||
} | ||
const listeners = listenersMap.get(ctx); | ||
if (listeners && listeners.length) { | ||
for (let i = 0; i < values.length; i++) { | ||
const value = values[i]; | ||
if (isEventTarget(value)) { | ||
for (let j = 0; j < listeners.length; j++) { | ||
const record = listeners[j]; | ||
value.addEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
else { | ||
listeners = []; | ||
listenersMap.set(this, listeners); | ||
} | ||
} | ||
options = normalizeOptions(options); | ||
let callback; | ||
if (typeof listener === "object") { | ||
callback = () => listener.handleEvent.apply(listener, arguments); | ||
} | ||
else { | ||
callback = listener; | ||
} | ||
const record = { type, callback, listener, options }; | ||
if (options.once) { | ||
record.callback = function () { | ||
const i = listeners.indexOf(record); | ||
if (i !== -1) { | ||
listeners.splice(i, 1); | ||
} | ||
const oldValues = wrap(ctx.ret.cached); | ||
let value = (ctx.ret.cached = unwrap(values)); | ||
if (ctx.f & IsScheduling) { | ||
ctx.f |= IsSchedulingRefresh; | ||
} | ||
else if (!(ctx.f & IsUpdating)) { | ||
// If we’re not updating the component, which happens when components are | ||
// refreshed, or when async generator components iterate, we have to do a | ||
// little bit housekeeping when a component’s child values have changed. | ||
if (!valuesEqual(oldValues, values)) { | ||
const records = getListenerRecords(ctx.parent, ctx.host); | ||
if (records.length) { | ||
for (let i = 0; i < values.length; i++) { | ||
const value = values[i]; | ||
if (isEventTarget(value)) { | ||
for (let j = 0; j < records.length; j++) { | ||
const record = records[j]; | ||
value.addEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
} | ||
return callback.apply(this, arguments); | ||
}; | ||
} | ||
if (listeners.some((record1) => record.type === record1.type && | ||
record.listener === record1.listener && | ||
!record.options.capture === !record1.options.capture)) { | ||
return; | ||
} | ||
listeners.push(record); | ||
for (const value of getChildValues(this._el)) { | ||
if (isEventTarget(value)) { | ||
value.addEventListener(record.type, record.callback, record.options); | ||
} | ||
// rearranging the nearest ancestor host element | ||
const host = ctx.host; | ||
const oldHostValues = wrap(host.cached); | ||
invalidate(ctx, host); | ||
const hostValues = getChildValues(host); | ||
ctx.renderer.arrange(host.el.tag, host.value, host.el.props, hostValues, | ||
// props and oldProps are the same because the host isn’t updated. | ||
host.el.props, oldHostValues); | ||
} | ||
flush(ctx.renderer, ctx.root, ctx); | ||
} | ||
removeEventListener(type, listener, options) { | ||
const listeners = listenersMap.get(this); | ||
if (listener == null || listeners == null) { | ||
return; | ||
const callbacks = scheduleMap.get(ctx); | ||
if (callbacks) { | ||
scheduleMap.delete(ctx); | ||
ctx.f |= IsScheduling; | ||
const value1 = ctx.renderer.read(value); | ||
for (const callback of callbacks) { | ||
callback(value1); | ||
} | ||
const options1 = normalizeOptions(options); | ||
const i = listeners.findIndex((record) => record.type === type && | ||
record.listener === listener && | ||
!record.options.capture === !options1.capture); | ||
if (i === -1) { | ||
return; | ||
ctx.f &= ~IsScheduling; | ||
// Handles an edge case where refresh() is called during a schedule(). | ||
if (ctx.f & IsSchedulingRefresh) { | ||
ctx.f &= ~IsSchedulingRefresh; | ||
value = getValue(ctx.ret); | ||
} | ||
const record = listeners[i]; | ||
listeners.splice(i, 1); | ||
for (const value of getChildValues(this._el)) { | ||
if (isEventTarget(value)) { | ||
value.removeEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
ctx.f &= ~IsUpdating; | ||
return value; | ||
} | ||
function invalidate(ctx, host) { | ||
for (let parent = ctx.parent; parent !== undefined && parent.host === host; parent = parent.parent) { | ||
parent.ret.cached = undefined; | ||
} | ||
host.cached = undefined; | ||
} | ||
function valuesEqual(values1, values2) { | ||
if (values1.length !== values2.length) { | ||
return false; | ||
} | ||
for (let i = 0; i < values1.length; i++) { | ||
const value1 = values1[i]; | ||
const value2 = values2[i]; | ||
if (value1 !== value2) { | ||
return false; | ||
} | ||
} | ||
dispatchEvent(ev) { | ||
const path = []; | ||
for (let parent = this._pa; parent !== undefined; parent = parent._pa) { | ||
path.push(parent); | ||
} | ||
// We patch the stopImmediatePropagation method because ev.cancelBubble | ||
// only informs us if stopPropagation was called and there are no | ||
// properties which inform us if stopImmediatePropagation was called. | ||
let immediateCancelBubble = false; | ||
const stopImmediatePropagation = ev.stopImmediatePropagation; | ||
setEventProperty(ev, "stopImmediatePropagation", () => { | ||
immediateCancelBubble = true; | ||
return stopImmediatePropagation.call(ev); | ||
}); | ||
setEventProperty(ev, "target", this); | ||
// The only possible errors in this block are errors thrown by callbacks, | ||
// and dispatchEvent will only log these errors rather than throwing | ||
// them. Therefore, we place all code in a try block, log errors in the | ||
// catch block, and use an unsafe return statement in the finally block. | ||
// | ||
// Each early return within the try block returns true because while the | ||
// return value is overridden in the finally block, TypeScript | ||
// (justifiably) does not recognize the unsafe return statement. | ||
return true; | ||
} | ||
/** | ||
* Enqueues and executes the component associated with the context. | ||
* | ||
* The functions stepComponent and runComponent work together | ||
* to implement the async queueing behavior of components. The runComponent | ||
* function calls the stepComponent 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 runComponent function caches block/value from the | ||
* stepComponent function on the context, according to whether the component | ||
* blocks. The “inflight” block/value properties are the currently executing | ||
* update, and the “enqueued” block/value properties represent an enqueued next | ||
* stepComponent. Enqueued steps are dequeued every time the current block | ||
* promise settles. | ||
*/ | ||
function runComponent(ctx) { | ||
if (!ctx.inflightBlock) { | ||
try { | ||
setEventProperty(ev, "eventPhase", CAPTURING_PHASE); | ||
for (let i = path.length - 1; i >= 0; i--) { | ||
const target = path[i]; | ||
const listeners = listenersMap.get(target); | ||
if (listeners) { | ||
setEventProperty(ev, "currentTarget", target); | ||
for (const record of listeners) { | ||
if (record.type === ev.type && record.options.capture) { | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
} | ||
const [block, value] = stepComponent(ctx); | ||
if (block) { | ||
ctx.inflightBlock = block | ||
.catch((err) => { | ||
if (!(ctx.f & IsUpdating)) { | ||
return propagateError(ctx.parent, err); | ||
} | ||
} | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
}) | ||
.finally(() => advanceComponent(ctx)); | ||
// stepComponent will only return a block if the value is asynchronous | ||
ctx.inflightValue = value; | ||
} | ||
{ | ||
const listeners = listenersMap.get(this); | ||
if (listeners) { | ||
setEventProperty(ev, "eventPhase", AT_TARGET); | ||
setEventProperty(ev, "currentTarget", this); | ||
for (const record of listeners) { | ||
if (record.type === ev.type) { | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
return value; | ||
} | ||
catch (err) { | ||
if (!(ctx.f & IsUpdating)) { | ||
return propagateError(ctx.parent, err); | ||
} | ||
throw err; | ||
} | ||
} | ||
else if (ctx.f & IsAsyncGen) { | ||
return ctx.inflightValue; | ||
} | ||
else if (!ctx.enqueuedBlock) { | ||
let resolve; | ||
ctx.enqueuedBlock = ctx.inflightBlock | ||
.then(() => { | ||
try { | ||
const [block, value] = stepComponent(ctx); | ||
resolve(value); | ||
if (block) { | ||
return block.catch((err) => { | ||
if (!(ctx.f & IsUpdating)) { | ||
return propagateError(ctx.parent, err); | ||
} | ||
} | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
}); | ||
} | ||
} | ||
if (ev.bubbles) { | ||
setEventProperty(ev, "eventPhase", BUBBLING_PHASE); | ||
for (let i = 0; i < path.length; i++) { | ||
const target = path[i]; | ||
const listeners = listenersMap.get(target); | ||
if (listeners) { | ||
setEventProperty(ev, "currentTarget", target); | ||
for (const record of listeners) { | ||
if (record.type === ev.type && !record.options.capture) { | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
catch (err) { | ||
if (!(ctx.f & IsUpdating)) { | ||
return propagateError(ctx.parent, err); | ||
} | ||
} | ||
} | ||
catch (err) { | ||
console.error(err); | ||
} | ||
finally { | ||
setEventProperty(ev, "eventPhase", NONE); | ||
setEventProperty(ev, "currentTarget", null); | ||
// eslint-disable-next-line no-unsafe-finally | ||
return !ev.defaultPrevented; | ||
} | ||
}) | ||
.finally(() => advanceComponent(ctx)); | ||
ctx.enqueuedValue = new Promise((resolve1) => (resolve = resolve1)); | ||
} | ||
return ctx.enqueuedValue; | ||
} | ||
/*** PRIVATE CONTEXT FUNCTIONS ***/ | ||
/** | ||
@@ -1193,35 +1197,39 @@ * This function is responsible for executing the component and handling all | ||
*/ | ||
function stepCtx(ctx) { | ||
const el = ctx._el; | ||
if (ctx._f & IsDone) { | ||
return [undefined, getValue(el)]; | ||
function stepComponent(ctx) { | ||
const ret = ctx.ret; | ||
if (ctx.f & IsDone) { | ||
return [undefined, getValue(ret)]; | ||
} | ||
const initial = !ctx._it; | ||
const initial = !ctx.iterator; | ||
if (initial) { | ||
ctx.f |= IsExecuting; | ||
clearEventListeners(ctx); | ||
let result; | ||
try { | ||
ctx._f |= IsExecuting; | ||
clearEventListeners(ctx); | ||
const result = el.tag.call(ctx, el.props); | ||
if (isIteratorLike(result)) { | ||
ctx._it = result; | ||
} | ||
else if (isPromiseLike(result)) { | ||
// async function component | ||
const result1 = result instanceof Promise ? result : Promise.resolve(result); | ||
const value = result1.then((result) => updateCtxChildren(ctx, result)); | ||
return [result1, value]; | ||
} | ||
else { | ||
// sync function component | ||
return [undefined, updateCtxChildren(ctx, result)]; | ||
} | ||
result = ret.el.tag.call(ctx.facade, ret.el.props); | ||
} | ||
catch (err) { | ||
ctx.f |= IsErrored; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~IsExecuting; | ||
ctx.f &= ~IsExecuting; | ||
} | ||
if (isIteratorLike(result)) { | ||
ctx.iterator = result; | ||
} | ||
else if (isPromiseLike(result)) { | ||
// async function component | ||
const result1 = result instanceof Promise ? result : Promise.resolve(result); | ||
const value = result1.then((result) => updateComponentChildren(ctx, result), (err) => { | ||
ctx.f |= IsErrored; | ||
throw err; | ||
}); | ||
return [result1, value]; | ||
} | ||
else { | ||
// sync function component | ||
return [undefined, updateComponentChildren(ctx, result)]; | ||
} | ||
} | ||
// The value passed back into the generator as the argument to the next | ||
// method is a promise if an async generator component has async children. | ||
// Sync generator components only resume when their children have fulfilled | ||
// so ctx._el._ic (the element’s inflight children) will never be defined. | ||
let oldValue; | ||
@@ -1232,19 +1240,23 @@ if (initial) { | ||
} | ||
else if (ctx._el._ic) { | ||
oldValue = ctx._el._ic.then(ctx._re.read, () => ctx._re.read(undefined)); | ||
else if (ctx.ret.inflight) { | ||
// The value passed back into the generator as the argument to the next | ||
// method is a promise if an async generator component has async children. | ||
// Sync generator components only resume when their children have fulfilled | ||
// so the element’s inflight child values will never be defined. | ||
oldValue = ctx.ret.inflight.then((value) => ctx.renderer.read(value), () => ctx.renderer.read(undefined)); | ||
} | ||
else { | ||
oldValue = ctx._re.read(getValue(el)); | ||
oldValue = ctx.renderer.read(getValue(ret)); | ||
} | ||
let iteration; | ||
ctx.f |= IsExecuting; | ||
try { | ||
ctx._f |= IsExecuting; | ||
iteration = ctx._it.next(oldValue); | ||
iteration = ctx.iterator.next(oldValue); | ||
} | ||
catch (err) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone | IsErrored; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~IsExecuting; | ||
ctx.f &= ~IsExecuting; | ||
} | ||
@@ -1254,14 +1266,14 @@ if (isPromiseLike(iteration)) { | ||
if (initial) { | ||
ctx._f |= IsAsyncGen; | ||
ctx.f |= IsAsyncGen; | ||
} | ||
const value = iteration.then((iteration) => { | ||
if (!(ctx._f & IsIterating)) { | ||
ctx._f &= ~IsAvailable; | ||
if (!(ctx.f & IsIterating)) { | ||
ctx.f &= ~IsAvailable; | ||
} | ||
ctx._f &= ~IsIterating; | ||
ctx.f &= ~IsIterating; | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone; | ||
} | ||
try { | ||
const value = updateCtxChildren(ctx, iteration.value); | ||
const value = updateComponentChildren(ctx, iteration.value); | ||
if (isPromiseLike(value)) { | ||
@@ -1276,3 +1288,3 @@ return value.catch((err) => handleChildError(ctx, err)); | ||
}, (err) => { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone | IsErrored; | ||
throw err; | ||
@@ -1284,11 +1296,11 @@ }); | ||
if (initial) { | ||
ctx._f |= IsSyncGen; | ||
ctx.f |= IsSyncGen; | ||
} | ||
ctx._f &= ~IsIterating; | ||
ctx.f &= ~IsIterating; | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone; | ||
} | ||
let value; | ||
try { | ||
value = updateCtxChildren(ctx, iteration.value); | ||
value = updateComponentChildren(ctx, iteration.value); | ||
if (isPromiseLike(value)) { | ||
@@ -1309,221 +1321,225 @@ value = value.catch((err) => handleChildError(ctx, err)); | ||
*/ | ||
function advanceCtx(ctx) { | ||
// _ib - inflightBlock | ||
// _iv - inflightValue | ||
// _eb - enqueuedBlock | ||
// _ev - enqueuedValue | ||
ctx._ib = ctx._eb; | ||
ctx._iv = ctx._ev; | ||
ctx._eb = undefined; | ||
ctx._ev = undefined; | ||
if (ctx._f & IsAsyncGen && !(ctx._f & IsDone)) { | ||
runCtx(ctx); | ||
function advanceComponent(ctx) { | ||
ctx.inflightBlock = ctx.enqueuedBlock; | ||
ctx.inflightValue = ctx.enqueuedValue; | ||
ctx.enqueuedBlock = undefined; | ||
ctx.enqueuedValue = undefined; | ||
if (ctx.f & IsAsyncGen && !(ctx.f & IsDone) && !(ctx.f & IsUnmounted)) { | ||
runComponent(ctx); | ||
} | ||
} | ||
/** | ||
* Enqueues and executes the component associated with the context. | ||
* | ||
* 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 blocks. 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 every time | ||
* the current block promise settles. | ||
*/ | ||
function runCtx(ctx) { | ||
if (!ctx._ib) { | ||
try { | ||
const [block, value] = stepCtx(ctx); | ||
if (block) { | ||
ctx._ib = block | ||
.catch((err) => { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}) | ||
.finally(() => advanceCtx(ctx)); | ||
// stepCtx will only return a block if the value is asynchronous | ||
ctx._iv = value; | ||
} | ||
return value; | ||
} | ||
catch (err) { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
throw err; | ||
} | ||
} | ||
else if (ctx._f & IsAsyncGen) { | ||
return ctx._iv; | ||
} | ||
else if (!ctx._eb) { | ||
let resolve; | ||
ctx._eb = ctx._ib | ||
.then(() => { | ||
try { | ||
const [block, value] = stepCtx(ctx); | ||
resolve(value); | ||
if (block) { | ||
return block.catch((err) => { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}); | ||
} | ||
} | ||
catch (err) { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
} | ||
}) | ||
.finally(() => advanceCtx(ctx)); | ||
ctx._ev = new Promise((resolve1) => (resolve = resolve1)); | ||
} | ||
return ctx._ev; | ||
} | ||
/** | ||
* Called to make props available to the props async iterator for async | ||
* generator components. | ||
*/ | ||
function resumeCtx(ctx) { | ||
if (ctx._oa) { | ||
ctx._oa(); | ||
ctx._oa = undefined; | ||
function resumeCtxIterator(ctx) { | ||
if (ctx.onAvailable) { | ||
ctx.onAvailable(); | ||
ctx.onAvailable = undefined; | ||
} | ||
else { | ||
ctx._f |= IsAvailable; | ||
ctx.f |= IsAvailable; | ||
} | ||
} | ||
function updateCtx(ctx) { | ||
ctx._f |= IsUpdating; | ||
resumeCtx(ctx); | ||
return runCtx(ctx); | ||
} | ||
function updateCtxChildren(ctx, children) { | ||
return updateChildren(ctx._re, ctx._rt, ctx._ho, ctx, ctx._sc, ctx._el, narrow(children)); | ||
} | ||
function commitCtx(ctx, values) { | ||
if (ctx._f & IsUnmounted) { | ||
return; | ||
// TODO: async unmounting | ||
function unmountComponent(ctx) { | ||
ctx.f |= IsUnmounted; | ||
clearEventListeners(ctx); | ||
const callbacks = cleanupMap.get(ctx); | ||
if (callbacks) { | ||
cleanupMap.delete(ctx); | ||
const value = ctx.renderer.read(getValue(ctx.ret)); | ||
for (const callback of callbacks) { | ||
callback(value); | ||
} | ||
} | ||
const listeners = listenersMap.get(ctx); | ||
if (listeners && listeners.length) { | ||
for (let i = 0; i < values.length; i++) { | ||
const value = values[i]; | ||
if (isEventTarget(value)) { | ||
for (let j = 0; j < listeners.length; j++) { | ||
const record = listeners[j]; | ||
value.addEventListener(record.type, record.callback, record.options); | ||
if (!(ctx.f & IsDone)) { | ||
ctx.f |= IsDone; | ||
resumeCtxIterator(ctx); | ||
if (ctx.iterator && typeof ctx.iterator.return === "function") { | ||
ctx.f |= IsExecuting; | ||
try { | ||
const iteration = ctx.iterator.return(); | ||
if (isPromiseLike(iteration)) { | ||
iteration.catch((err) => propagateError(ctx.parent, err)); | ||
} | ||
} | ||
finally { | ||
ctx.f &= ~IsExecuting; | ||
} | ||
} | ||
} | ||
if (ctx._f & IsScheduling) { | ||
ctx._f |= IsSchedulingRefresh; | ||
} | ||
/*** 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; | ||
const listenersMap = new WeakMap(); | ||
function addEventListener(ctx, type, listener, options) { | ||
let listeners; | ||
if (listener == null) { | ||
return; | ||
} | ||
else if (!(ctx._f & IsUpdating)) { | ||
// Rearrange the host. | ||
const listeners = getListeners(ctx._pa, ctx._ho); | ||
if (listeners.length) { | ||
for (let i = 0; i < values.length; i++) { | ||
const value = values[i]; | ||
if (isEventTarget(value)) { | ||
for (let j = 0; j < listeners.length; j++) { | ||
const record = listeners[j]; | ||
value.addEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
} | ||
else { | ||
const listeners1 = listenersMap.get(ctx); | ||
if (listeners1) { | ||
listeners = listeners1; | ||
} | ||
const host = ctx._ho; | ||
const hostValues = getChildValues(host); | ||
ctx._re.arrange(host, host.tag === Portal ? host.props.root : host._n, hostValues); | ||
if (hostValues.length) { | ||
host._f |= HadChildren; | ||
} | ||
else { | ||
host._f &= ~HadChildren; | ||
listeners = []; | ||
listenersMap.set(ctx, listeners); | ||
} | ||
ctx._re.complete(ctx._rt); | ||
} | ||
let value = unwrap(values); | ||
const callbacks = scheduleMap.get(ctx); | ||
if (callbacks && callbacks.size) { | ||
const callbacks1 = Array.from(callbacks); | ||
// We must clear the set of callbacks before calling them, because a | ||
// callback which refreshes the component would otherwise cause a stack | ||
// overflow. | ||
callbacks.clear(); | ||
const value1 = ctx._re.read(value); | ||
ctx._f |= IsScheduling; | ||
for (const callback of callbacks1) { | ||
try { | ||
callback(value1); | ||
options = normalizeListenerOptions(options); | ||
let callback; | ||
if (typeof listener === "object") { | ||
callback = () => listener.handleEvent.apply(listener, arguments); | ||
} | ||
else { | ||
callback = listener; | ||
} | ||
const record = { type, callback, listener, options }; | ||
if (options.once) { | ||
record.callback = function () { | ||
const i = listeners.indexOf(record); | ||
if (i !== -1) { | ||
listeners.splice(i, 1); | ||
} | ||
catch (err) { | ||
// TODO: handle schedule callback errors in a better way. | ||
console.error(err); | ||
} | ||
return callback.apply(this, arguments); | ||
}; | ||
} | ||
if (listeners.some((record1) => record.type === record1.type && | ||
record.listener === record1.listener && | ||
!record.options.capture === !record1.options.capture)) { | ||
return; | ||
} | ||
listeners.push(record); | ||
// TODO: is it possible to separate out the EventTarget delegation logic | ||
for (const value of getChildValues(ctx.ret)) { | ||
if (isEventTarget(value)) { | ||
value.addEventListener(record.type, record.callback, record.options); | ||
} | ||
ctx._f &= ~IsScheduling; | ||
if (ctx._f & IsSchedulingRefresh) { | ||
ctx._f &= ~IsSchedulingRefresh; | ||
value = getValue(ctx._el); | ||
} | ||
} | ||
function removeEventListener(ctx, type, listener, options) { | ||
const listeners = listenersMap.get(ctx); | ||
if (listener == null || listeners == null) { | ||
return; | ||
} | ||
const options1 = normalizeListenerOptions(options); | ||
const i = listeners.findIndex((record) => record.type === type && | ||
record.listener === listener && | ||
!record.options.capture === !options1.capture); | ||
if (i === -1) { | ||
return; | ||
} | ||
const record = listeners[i]; | ||
listeners.splice(i, 1); | ||
// TODO: is it possible to separate out the EventTarget delegation logic | ||
for (const value of getChildValues(ctx.ret)) { | ||
if (isEventTarget(value)) { | ||
value.removeEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
ctx._f &= ~IsUpdating; | ||
return value; | ||
} | ||
// TODO: async unmounting | ||
function unmountCtx(ctx) { | ||
ctx._f |= IsUnmounted; | ||
clearEventListeners(ctx); | ||
const callbacks = cleanupMap.get(ctx); | ||
if (callbacks && callbacks.size) { | ||
const callbacks1 = Array.from(callbacks); | ||
callbacks.clear(); | ||
const value = ctx._re.read(getValue(ctx._el)); | ||
for (const callback of callbacks1) { | ||
try { | ||
callback(value); | ||
function dispatchEvent(ctx, ev) { | ||
const path = []; | ||
for (let parent = ctx.parent; parent !== undefined; parent = parent.parent) { | ||
path.push(parent); | ||
} | ||
// We patch the stopImmediatePropagation method because ev.cancelBubble | ||
// only informs us if stopPropagation was called and there are no | ||
// properties which inform us if stopImmediatePropagation was called. | ||
let immediateCancelBubble = false; | ||
const stopImmediatePropagation = ev.stopImmediatePropagation; | ||
setEventProperty(ev, "stopImmediatePropagation", () => { | ||
immediateCancelBubble = true; | ||
return stopImmediatePropagation.call(ev); | ||
}); | ||
setEventProperty(ev, "target", ctx.facade); | ||
// The only possible errors in this block are errors thrown by callbacks, | ||
// and dispatchEvent will only log these errors rather than throwing | ||
// them. Therefore, we place all code in a try block, log errors in the | ||
// catch block, and use an unsafe return statement in the finally block. | ||
// | ||
// Each early return within the try block returns true because while the | ||
// return value is overridden in the finally block, TypeScript | ||
// (justifiably) does not recognize the unsafe return statement. | ||
// | ||
// TODO: Run all callbacks even if one of them errors | ||
try { | ||
setEventProperty(ev, "eventPhase", CAPTURING_PHASE); | ||
for (let i = path.length - 1; i >= 0; i--) { | ||
const target = path[i]; | ||
const listeners = listenersMap.get(target); | ||
if (listeners) { | ||
setEventProperty(ev, "currentTarget", target.facade); | ||
for (const record of listeners) { | ||
if (record.type === ev.type && record.options.capture) { | ||
record.callback.call(target.facade, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
catch (err) { | ||
// TODO: handle cleanup callback errors in a better way. | ||
console.error(err); | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
if (!(ctx._f & IsDone)) { | ||
ctx._f |= IsDone; | ||
resumeCtx(ctx); | ||
if (ctx._it && typeof ctx._it.return === "function") { | ||
try { | ||
ctx._f |= IsExecuting; | ||
const iteration = ctx._it.return(); | ||
if (isPromiseLike(iteration)) { | ||
iteration.catch((err) => propagateError(ctx._pa, err)); | ||
{ | ||
const listeners = listenersMap.get(ctx); | ||
if (listeners) { | ||
setEventProperty(ev, "eventPhase", AT_TARGET); | ||
setEventProperty(ev, "currentTarget", ctx.facade); | ||
for (const record of listeners) { | ||
if (record.type === ev.type) { | ||
record.callback.call(ctx.facade, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
} | ||
finally { | ||
ctx._f &= ~IsExecuting; | ||
} | ||
if (ev.bubbles) { | ||
setEventProperty(ev, "eventPhase", BUBBLING_PHASE); | ||
for (let i = 0; i < path.length; i++) { | ||
const target = path[i]; | ||
const listeners = listenersMap.get(target); | ||
if (listeners) { | ||
setEventProperty(ev, "currentTarget", target.facade); | ||
for (const record of listeners) { | ||
if (record.type === ev.type && !record.options.capture) { | ||
record.callback.call(target.facade, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
catch (err) { | ||
// TODO: Use setTimeout to rethrow the error. | ||
console.error(err); | ||
} | ||
finally { | ||
setEventProperty(ev, "eventPhase", NONE); | ||
setEventProperty(ev, "currentTarget", null); | ||
// eslint-disable-next-line no-unsafe-finally | ||
return !ev.defaultPrevented; | ||
} | ||
} | ||
/*** 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; | ||
const listenersMap = new WeakMap(); | ||
function normalizeOptions(options) { | ||
function normalizeListenerOptions(options) { | ||
if (typeof options === "boolean") { | ||
@@ -1546,2 +1562,4 @@ return { capture: options }; | ||
} | ||
// TODO: Maybe we can pass in the current context directly, rather than | ||
// starting from the parent? | ||
/** | ||
@@ -1555,9 +1573,6 @@ * A function to reconstruct an array of every listener given a context and a | ||
* element passed in matches the parent context’s host element. | ||
* | ||
* TODO: Maybe we can pass in the current context directly, rather than | ||
* starting from the parent? | ||
*/ | ||
function getListeners(ctx, host) { | ||
function getListenerRecords(ctx, ret) { | ||
let listeners = []; | ||
while (ctx !== undefined && ctx._ho === host) { | ||
while (ctx !== undefined && ctx.host === ret) { | ||
const listeners1 = listenersMap.get(ctx); | ||
@@ -1567,3 +1582,3 @@ if (listeners1) { | ||
} | ||
ctx = ctx._pa; | ||
ctx = ctx.parent; | ||
} | ||
@@ -1575,3 +1590,3 @@ return listeners; | ||
if (listeners && listeners.length) { | ||
for (const value of getChildValues(ctx._el)) { | ||
for (const value of getChildValues(ctx.ret)) { | ||
if (isEventTarget(value)) { | ||
@@ -1589,17 +1604,19 @@ for (const record of listeners) { | ||
function handleChildError(ctx, err) { | ||
if (ctx._f & IsDone || !ctx._it || typeof ctx._it.throw !== "function") { | ||
if (ctx.f & IsDone || | ||
!ctx.iterator || | ||
typeof ctx.iterator.throw !== "function") { | ||
throw err; | ||
} | ||
resumeCtx(ctx); | ||
resumeCtxIterator(ctx); | ||
let iteration; | ||
try { | ||
ctx._f |= IsExecuting; | ||
iteration = ctx._it.throw(err); | ||
ctx.f |= IsExecuting; | ||
iteration = ctx.iterator.throw(err); | ||
} | ||
catch (err) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone | IsErrored; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~IsExecuting; | ||
ctx.f &= ~IsExecuting; | ||
} | ||
@@ -1609,7 +1626,7 @@ if (isPromiseLike(iteration)) { | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone; | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
return updateComponentChildren(ctx, iteration.value); | ||
}, (err) => { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone | IsErrored; | ||
throw err; | ||
@@ -1619,5 +1636,5 @@ }); | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone; | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
return updateComponentChildren(ctx, iteration.value); | ||
} | ||
@@ -1633,6 +1650,6 @@ function propagateError(ctx, err) { | ||
catch (err) { | ||
return propagateError(ctx._pa, err); | ||
return propagateError(ctx.parent, err); | ||
} | ||
if (isPromiseLike(result)) { | ||
return result.catch((err) => propagateError(ctx._pa, err)); | ||
return result.catch((err) => propagateError(ctx.parent, err)); | ||
} | ||
@@ -1643,2 +1660,3 @@ return result; | ||
exports.Context = Context; | ||
exports.ContextInternalsSymbol = ContextInternalsSymbol; | ||
exports.Copy = Copy; | ||
@@ -1645,0 +1663,0 @@ exports.Element = Element; |
@@ -1,9 +0,5 @@ | ||
import { Children, Context, Element as CrankElement, ElementValue, Renderer } from "./crank"; | ||
export declare class DOMRenderer extends Renderer<Node, string | undefined> { | ||
import { Children, Context, ElementValue, Renderer } from "./crank"; | ||
export declare class DOMRenderer extends Renderer<Node, string> { | ||
constructor(); | ||
render(children: Children, root: Node, ctx?: Context): Promise<ElementValue<Node>> | ElementValue<Node>; | ||
parse(text: string): DocumentFragment; | ||
scope(el: CrankElement<string | symbol>, scope: string | undefined): string | undefined; | ||
create(el: CrankElement<string | symbol>, ns: string | undefined): Node; | ||
patch(el: CrankElement<string | symbol>, node: Element): void; | ||
arrange(el: CrankElement<string | symbol>, node: Node, children: Array<Node | string>): void; | ||
} | ||
@@ -10,0 +6,0 @@ export declare const renderer: DOMRenderer; |
204
cjs/dom.js
@@ -8,9 +8,3 @@ 'use strict'; | ||
const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; | ||
class DOMRenderer extends crank.Renderer { | ||
render(children, root, ctx) { | ||
if (root == null || typeof root.nodeType !== "number") { | ||
throw new TypeError(`Render root is not a node. Received: ${JSON.stringify(root && root.toString())}`); | ||
} | ||
return super.render(children, root, ctx); | ||
} | ||
const impl = { | ||
parse(text) { | ||
@@ -29,5 +23,6 @@ if (typeof document.createRange === "function") { | ||
} | ||
} | ||
scope(el, scope) { | ||
switch (el.tag) { | ||
}, | ||
scope(scope, tag) { | ||
// TODO: Should we handle xmlns??? | ||
switch (tag) { | ||
case crank.Portal: | ||
@@ -41,107 +36,111 @@ case "foreignObject": | ||
} | ||
} | ||
create(el, ns) { | ||
if (typeof el.tag !== "string") { | ||
throw new Error(`Unknown tag: ${el.tag.toString()}`); | ||
}, | ||
create(tag, _props, ns) { | ||
if (typeof tag !== "string") { | ||
throw new Error(`Unknown tag: ${tag.toString()}`); | ||
} | ||
else if (el.tag === "svg") { | ||
else if (tag.toLowerCase() === "svg") { | ||
ns = SVG_NAMESPACE; | ||
} | ||
return ns | ||
? document.createElementNS(ns, el.tag) | ||
: document.createElement(el.tag); | ||
} | ||
patch(el, node) { | ||
const isSVG = node.namespaceURI === SVG_NAMESPACE; | ||
for (let name in el.props) { | ||
let forceAttribute = false; | ||
const value = el.props[name]; | ||
switch (name) { | ||
case "children": | ||
break; | ||
case "style": { | ||
const style = node.style; | ||
if (style == null) { | ||
node.setAttribute("style", value); | ||
return ns ? document.createElementNS(ns, tag) : document.createElement(tag); | ||
}, | ||
patch(_tag, | ||
// TODO: Why does this assignment work? | ||
node, name, | ||
// TODO: Stricter typings? | ||
value, oldValue, scope) { | ||
const isSVG = scope === SVG_NAMESPACE; | ||
switch (name) { | ||
case "style": { | ||
const style = node.style; | ||
if (style == null) { | ||
node.setAttribute("style", value); | ||
} | ||
else if (value == null || value === false) { | ||
node.removeAttribute("style"); | ||
} | ||
else if (value === true) { | ||
node.setAttribute("style", ""); | ||
} | ||
else if (typeof value === "string") { | ||
if (style.cssText !== value) { | ||
style.cssText = value; | ||
} | ||
else { | ||
if (value == null) { | ||
node.removeAttribute("style"); | ||
} | ||
else { | ||
if (typeof oldValue === "string") { | ||
style.cssText = ""; | ||
} | ||
for (const styleName in { ...oldValue, ...value }) { | ||
const styleValue = value && value[styleName]; | ||
if (styleValue == null) { | ||
style.removeProperty(styleName); | ||
} | ||
else if (typeof value === "string") { | ||
if (style.cssText !== value) { | ||
style.cssText = value; | ||
} | ||
else if (style.getPropertyValue(styleName) !== styleValue) { | ||
style.setProperty(styleName, styleValue); | ||
} | ||
else { | ||
for (const styleName in value) { | ||
const styleValue = value && value[styleName]; | ||
if (styleValue == null) { | ||
style.removeProperty(styleName); | ||
} | ||
else if (style.getPropertyValue(styleName) !== styleValue) { | ||
style.setProperty(styleName, styleValue); | ||
} | ||
} | ||
} | ||
} | ||
break; | ||
} | ||
case "class": | ||
case "className": | ||
if (value === true) { | ||
node.setAttribute("class", ""); | ||
break; | ||
} | ||
case "class": | ||
case "className": | ||
if (value === true) { | ||
node.setAttribute("class", ""); | ||
} | ||
else if (value == null) { | ||
node.removeAttribute("class"); | ||
} | ||
else if (!isSVG) { | ||
if (node.className !== value) { | ||
node["className"] = value; | ||
} | ||
else if (!value) { | ||
node.removeAttribute("class"); | ||
} | ||
else if (!isSVG) { | ||
if (node.className !== value) { | ||
node["className"] = value; | ||
} | ||
} | ||
else if (node.getAttribute("class") !== value) { | ||
node.setAttribute("class", value); | ||
} | ||
break; | ||
// Gleaned from: | ||
// https://github.com/preactjs/preact/blob/05e5d2c0d2d92c5478eeffdbd96681c96500d29f/src/diff/props.js#L111-L117 | ||
// TODO: figure out why we use setAttribute for each of these | ||
case "form": | ||
case "list": | ||
case "type": | ||
case "size": | ||
forceAttribute = true; | ||
// fallthrough | ||
default: { | ||
if (value == null) { | ||
node.removeAttribute(name); | ||
} | ||
else if (typeof value === "function" || | ||
typeof value === "object" || | ||
(!forceAttribute && !isSVG && name in node)) { | ||
} | ||
else if (node.getAttribute("class") !== value) { | ||
node.setAttribute("class", value); | ||
} | ||
break; | ||
default: { | ||
if (name in node && | ||
// boolean properties will coerce strings, but sometimes they map to | ||
// enumerated attributes, where truthy strings ("false", "no") map to | ||
// falsy properties, so we use attributes in this case. | ||
!(typeof value === "string" && | ||
typeof node[name] === "boolean")) { | ||
try { | ||
if (node[name] !== value) { | ||
node[name] = value; | ||
} | ||
return; | ||
} | ||
else if (value === true) { | ||
node.setAttribute(name, ""); | ||
catch (err) { | ||
// some properties are readonly so we fallback to setting them as | ||
// attributes | ||
} | ||
else if (value === false) { | ||
node.removeAttribute(name); | ||
} | ||
else if (node.getAttribute(name) !== value) { | ||
node.setAttribute(name, value); | ||
} | ||
} | ||
if (value === true) { | ||
value = ""; | ||
} | ||
else if (value == null || value === false) { | ||
node.removeAttribute(name); | ||
return; | ||
} | ||
if (node.getAttribute(name) !== value) { | ||
node.setAttribute(name, value); | ||
} | ||
} | ||
} | ||
} | ||
arrange(el, node, children) { | ||
if (el.tag === crank.Portal && | ||
(node == null || typeof node.nodeType !== "number")) { | ||
}, | ||
arrange(tag, node, props, children, _oldProps, oldChildren) { | ||
if (tag === crank.Portal && (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" in el.props || el.hadChildren)) { | ||
if (!("innerHTML" in props) && | ||
// We don’t want to update elements without explicit children (<div/>), | ||
// because these elements sometimes have child nodes added via raw | ||
// DOM manipulations. | ||
// However, if an element has previously rendered children, we clear the | ||
// them because it would be surprising not to clear Crank managed | ||
// children, even if the new element does not have explicit children. | ||
("children" in props || (oldChildren && oldChildren.length))) { | ||
if (children.length === 0) { | ||
@@ -187,2 +186,3 @@ node.textContent = ""; | ||
} | ||
// remove excess DOM nodes | ||
while (oldChild !== null) { | ||
@@ -193,2 +193,3 @@ const nextSibling = oldChild.nextSibling; | ||
} | ||
// append excess children | ||
for (; i < children.length; i++) { | ||
@@ -202,3 +203,14 @@ const newChild = children[i]; | ||
} | ||
}, | ||
}; | ||
class DOMRenderer extends crank.Renderer { | ||
constructor() { | ||
super(impl); | ||
} | ||
render(children, root, ctx) { | ||
if (root == null || typeof root.nodeType !== "number") { | ||
throw new TypeError(`Render root is not a node. Received: ${JSON.stringify(root && root.toString())}`); | ||
} | ||
return super.render(children, root, ctx); | ||
} | ||
} | ||
@@ -205,0 +217,0 @@ const renderer = new DOMRenderer(); |
@@ -1,10 +0,7 @@ | ||
import { Element, ElementValue, Renderer } from "./crank"; | ||
import { Renderer } from "./crank"; | ||
interface Node { | ||
value: string; | ||
} | ||
export declare class HTMLRenderer extends Renderer<Node | string, undefined, unknown, string> { | ||
create(): Node; | ||
escape(text: string): string; | ||
read(value: ElementValue<Node>): string; | ||
arrange(el: Element<string | symbol>, node: Node, children: Array<Node | string>): void; | ||
export declare class HTMLRenderer extends Renderer<Node, undefined, any, string> { | ||
constructor(); | ||
} | ||
@@ -11,0 +8,0 @@ export declare const renderer: HTMLRenderer; |
@@ -88,9 +88,9 @@ 'use strict'; | ||
} | ||
class HTMLRenderer extends crank.Renderer { | ||
const impl = { | ||
create() { | ||
return { value: "" }; | ||
} | ||
}, | ||
escape(text) { | ||
return escape(text); | ||
} | ||
}, | ||
read(value) { | ||
@@ -109,22 +109,27 @@ if (Array.isArray(value)) { | ||
} | ||
} | ||
arrange(el, node, children) { | ||
if (el.tag === crank.Portal) { | ||
}, | ||
arrange(tag, node, props, children) { | ||
if (tag === crank.Portal) { | ||
return; | ||
} | ||
else if (typeof el.tag !== "string") { | ||
throw new Error(`Unknown tag: ${el.tag.toString()}`); | ||
else if (typeof tag !== "string") { | ||
throw new Error(`Unknown tag: ${tag.toString()}`); | ||
} | ||
const attrs = printAttrs(el.props); | ||
const open = `<${el.tag}${attrs.length ? " " : ""}${attrs}>`; | ||
const attrs = printAttrs(props); | ||
const open = `<${tag}${attrs.length ? " " : ""}${attrs}>`; | ||
let result; | ||
if (voidTags.has(el.tag)) { | ||
if (voidTags.has(tag)) { | ||
result = open; | ||
} | ||
else { | ||
const close = `</${el.tag}>`; | ||
const contents = "innerHTML" in el.props ? el.props["innerHTML"] : join(children); | ||
const close = `</${tag}>`; | ||
const contents = "innerHTML" in props ? props["innerHTML"] : join(children); | ||
result = `${open}${contents}${close}`; | ||
} | ||
node.value = result; | ||
}, | ||
}; | ||
class HTMLRenderer extends crank.Renderer { | ||
constructor() { | ||
super(impl); | ||
} | ||
@@ -131,0 +136,0 @@ } |
@@ -10,2 +10,3 @@ 'use strict'; | ||
exports.Context = crank.Context; | ||
exports.ContextInternalsSymbol = crank.ContextInternalsSymbol; | ||
exports.Copy = crank.Copy; | ||
@@ -12,0 +13,0 @@ exports.Element = crank.Element; |
371
crank.d.ts
/** | ||
* A type which represents all valid values for an element tag. | ||
* | ||
* 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 functions are called “component” elements, and their | ||
* behavior is determined by the execution of the component function. | ||
*/ | ||
@@ -15,3 +10,3 @@ export declare type Tag = string | symbol | Component; | ||
*/ | ||
export declare type TagProps<TTag extends Tag> = TTag extends string ? JSX.IntrinsicElements[TTag] : TTag extends Component<infer TProps> ? TProps : unknown; | ||
export declare type TagProps<TTag extends Tag> = TTag extends string ? JSX.IntrinsicElements[TTag] : TTag extends Component<infer TProps> ? TProps : Record<string, unknown>; | ||
/*** | ||
@@ -93,26 +88,10 @@ * SPECIAL TAGS | ||
*/ | ||
export declare type Component<TProps = any> = (this: Context<TProps>, props: TProps) => Children | PromiseLike<Children> | Iterator<Children, Children | void, any> | AsyncIterator<Children, Children | void, any>; | ||
export declare type Component<TProps extends Record<string, unknown> = any> = (this: Context<TProps>, props: TProps) => Children | PromiseLike<Children> | Iterator<Children, Children | void, any> | AsyncIterator<Children, Children | void, any>; | ||
/** | ||
* A type to keep track of keys. Any value can be a key, though null and | ||
* undefined are ignored. | ||
*/ | ||
declare type Key = unknown; | ||
declare const ElementSymbol: unique symbol; | ||
/** | ||
* 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. | ||
* | ||
* @template {Tag} [TTag=Tag] - The type of the tag of the element. | ||
* | ||
* @example | ||
* // specific element types | ||
* let div: Element<"div">; | ||
* let portal: Element<Portal>; | ||
* let myEl: Element<MyComponent>; | ||
* | ||
* // general element types | ||
* let host: Element<string | symbol>; | ||
* let component: Element<Component>; | ||
* | ||
* Typically, you use a helper function like createElement to create elements | ||
* rather than instatiating this class directly. | ||
*/ | ||
export declare class Element<TTag extends Tag = Tag> { | ||
export interface Element<TTag extends Tag = Tag> { | ||
/** | ||
@@ -150,55 +129,27 @@ * @internal | ||
ref: ((value: unknown) => unknown) | undefined; | ||
/** | ||
* @internal | ||
* flags - A bitmask. See ELEMENT FLAGS. | ||
*/ | ||
_f: number; | ||
/** | ||
* @internal | ||
* children - The rendered children of the element. | ||
*/ | ||
_ch: Array<NarrowedChild> | NarrowedChild; | ||
/** | ||
* @internal | ||
* node - The node or context associated with the element. | ||
* | ||
* For host elements, this property is set to the return value of | ||
* Renderer.prototype.create when the component is mounted, i.e. DOM nodes | ||
* for the DOM renderer. | ||
* | ||
* For component elements, this property is set to a Context instance | ||
* (Context<TagProps<TTag>>). | ||
* | ||
* We assign both of these to the same property because they are mutually | ||
* exclusive. We use any because the Element type has no knowledge of | ||
* renderer nodes. | ||
*/ | ||
_n: any; | ||
/** | ||
* @internal | ||
* fallback - The element which this element is replacing. | ||
* | ||
* If an element renders asynchronously, we show any previously rendered | ||
* values in its place until it has committed for the first time. This | ||
* property is set to the previously rendered child. | ||
*/ | ||
_fb: NarrowedChild; | ||
/** | ||
* @internal | ||
* inflightChildren - The current async run of the element’s children. | ||
* | ||
* This property is used to make sure Copy element refs fire at the correct | ||
* time, and is also used to create yield values for async generator | ||
* components with async children. It is unset when the element is committed. | ||
*/ | ||
_ic: Promise<any> | undefined; | ||
/** | ||
* @internal | ||
* onvalue(s) - This property is set to the resolve function of a promise | ||
* which represents the next children, so that renderings can be raced. | ||
*/ | ||
_ov: Function | undefined; | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref: ((value: unknown) => unknown) | undefined); | ||
get hadChildren(): boolean; | ||
static_: boolean | undefined; | ||
} | ||
/** | ||
* 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. | ||
* | ||
* @template {Tag} [TTag=Tag] - The type of the tag of the element. | ||
* | ||
* @example | ||
* // specific element types | ||
* let div: Element<"div">; | ||
* let portal: Element<Portal>; | ||
* let myEl: Element<MyComponent>; | ||
* | ||
* // general element types | ||
* let host: Element<string | symbol>; | ||
* let component: Element<Component>; | ||
* | ||
* Typically, you use a helper function like createElement to create elements | ||
* rather than instatiating this class directly. | ||
*/ | ||
export declare class Element<TTag extends Tag = Tag> { | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref?: ((value: unknown) => unknown) | undefined, static_?: boolean | undefined); | ||
} | ||
export declare function isElement(value: any): value is Element; | ||
@@ -217,17 +168,8 @@ /** | ||
* Clones a given element, shallowly copying the props object. | ||
* | ||
* Used internally to make sure we don’t accidentally reuse elements when | ||
* rendering. | ||
*/ | ||
export declare function cloneElement<TTag extends Tag>(el: Element<TTag>): Element<TTag>; | ||
/*** 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. | ||
* A helper type which repesents all possible rendered values of an element. | ||
* | ||
* @template TNode - The node type for the element assigned by the renderer. | ||
* @template TNode - The node type for the element provided by the renderer. | ||
* | ||
@@ -253,37 +195,18 @@ * When asking the question, what is the “value” of a specific element, the | ||
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 and | ||
* disposing of nodes. | ||
* | ||
* @template TNode - The type of the node for a rendering environment. | ||
* @template TScope - Data which is passed down the tree. | ||
* @template TRoot - The type of the root for a rendering environment. | ||
* @template TResult - The type of exposed values. | ||
*/ | ||
export declare class Renderer<TNode, TScope, TRoot = TNode, TResult = ElementValue<TNode>> { | ||
declare type RetainerChild<TNode> = Retainer<TNode> | string | undefined; | ||
declare class Retainer<TNode> { | ||
el: Element; | ||
ctx: ContextInternals<TNode> | undefined; | ||
children: Array<RetainerChild<TNode>> | RetainerChild<TNode>; | ||
value: TNode | string | undefined; | ||
cached: ElementValue<TNode>; | ||
fallback: RetainerChild<TNode>; | ||
inflight: Promise<ElementValue<TNode>> | undefined; | ||
onCommit: Function | undefined; | ||
constructor(el: Element); | ||
} | ||
export interface RendererImpl<TNode, TScope, TRoot extends TNode = TNode, TResult = ElementValue<TNode>> { | ||
scope<TTag extends string | symbol>(scope: TScope | undefined, tag: TTag, props: TagProps<TTag>): TScope | undefined; | ||
create<TTag extends string | symbol>(tag: TTag, props: TagProps<TTag>, scope: TScope | undefined): TNode; | ||
/** | ||
* @internal | ||
* A weakmap which stores element trees by root. | ||
*/ | ||
_cache: WeakMap<object, Element<Portal>>; | ||
constructor(); | ||
/** | ||
* Renders an element tree into a specific root. | ||
* | ||
* @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 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 rendered value is exposed via render, schedule, | ||
@@ -305,19 +228,2 @@ * refresh, refs, or generator yield expressions. | ||
/** | ||
* Called in a preorder traversal for each host element. | ||
* | ||
* 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. | ||
* | ||
* @param el - The host element. | ||
* @param scope - The current scope. | ||
* | ||
* @returns The scope to be passed to create and scope for child host | ||
* elements. | ||
* | ||
* This method sets the scope for child host elements, not the current host | ||
* element. | ||
*/ | ||
scope(_el: Element<string | symbol>, scope: TScope | undefined): TScope; | ||
/** | ||
* Called for each string in an element tree. | ||
@@ -335,3 +241,3 @@ * | ||
*/ | ||
escape(text: string, _scope: TScope): string; | ||
escape(text: string, scope: TScope | undefined): string; | ||
/** | ||
@@ -345,54 +251,42 @@ * Called for each Raw element whose value prop is a string. | ||
*/ | ||
parse(text: string, _scope: TScope): TNode | string; | ||
parse(text: string, scope: TScope | undefined): TNode | string; | ||
patch<TTag extends string | symbol, TName extends string>(tag: TTag, node: TNode, name: TName, value: TagProps<TTag>[TName], oldValue: TagProps<TTag>[TName] | undefined, scope: TScope): unknown; | ||
arrange<TTag extends string | symbol>(tag: TTag, node: TNode, props: TagProps<TTag>, children: Array<TNode | string>, oldProps: TagProps<TTag> | undefined, oldChildren: Array<TNode | string> | undefined): unknown; | ||
dispose<TTag extends string | symbol>(tag: TTag, node: TNode, props: TagProps<TTag>): unknown; | ||
flush(root: TRoot): unknown; | ||
} | ||
/** | ||
* An abstract class which is subclassed to render to different target | ||
* environments. This class is responsible for kicking off the rendering | ||
* process and caching previous trees by root. | ||
* | ||
* @template TNode - The type of the node for a rendering environment. | ||
* @template TScope - Data which is passed down the tree. | ||
* @template TRoot - The type of the root for a rendering environment. | ||
* @template TResult - The type of exposed values. | ||
*/ | ||
export declare class Renderer<TNode extends object = object, TScope = unknown, TRoot extends TNode = TNode, TResult = ElementValue<TNode>> { | ||
/** | ||
* Called for each host element when it is committed for the first time. | ||
* | ||
* @param el - The host element. | ||
* @param scope - The current scope. | ||
* | ||
* @returns A “node” which determines the value of the host element. | ||
* @internal | ||
* A weakmap which stores element trees by root. | ||
*/ | ||
create(_el: Element<string | symbol>, _scope: TScope): TNode; | ||
cache: WeakMap<object, Retainer<TNode>>; | ||
impl: RendererImpl<TNode, TScope, TRoot, TResult>; | ||
constructor(impl: Partial<RendererImpl<TNode, TScope, TRoot, TResult>>); | ||
/** | ||
* Called for each host element when it is committed. | ||
* Renders an element tree into a specific root. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* @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 different renderers so that | ||
* events/provisions properly propagate. The context for a given root must be | ||
* the same or an error will be thrown. | ||
* | ||
* @returns The return value is ignored. | ||
* | ||
* Used to mutate the node associated with an element when new props are | ||
* passed. | ||
* @returns The result of rendering the children, or a possible promise of | ||
* the result if the element tree renders asynchronously. | ||
*/ | ||
patch(_el: Element<string | symbol>, _node: TNode): unknown; | ||
/** | ||
* Called for each host element so that elements can be arranged into a tree. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* @param children - An array of nodes and strings from child elements. | ||
* | ||
* @returns The return value is ignored. | ||
* | ||
* This method is also called by child components contexts as the last step | ||
* of a refresh. | ||
*/ | ||
arrange(_el: Element<string | symbol>, _node: TNode | TRoot, _children: Array<TNode | string>): unknown; | ||
/** | ||
* Called for each host element when it is unmounted. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* | ||
* @returns The return value is ignored. | ||
*/ | ||
dispose(_el: Element<string | symbol>, _node: TNode): unknown; | ||
/** | ||
* Called at the end of the rendering process for each root of the tree. | ||
* | ||
* @param root - The root prop passed to portals or the render method. | ||
* | ||
* @returns The return value is ignored. | ||
*/ | ||
complete(_root: TRoot): unknown; | ||
render(children: Children, root?: TRoot | undefined, bridge?: Context | undefined): Promise<TResult> | TResult; | ||
} | ||
@@ -408,32 +302,23 @@ export interface Context extends Crank.Context { | ||
/** | ||
* 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. | ||
* | ||
* @template [TProps=*] - The expected shape of the props passed to the | ||
* component. Used to strongly type the Context iterator methods. | ||
* @template [TResult=*] - The readable element value type. It is used in | ||
* places such as the return value of refresh and the argument passed to | ||
* schedule and cleanup callbacks. | ||
* @internal | ||
*/ | ||
export declare class Context<TProps = any, TResult = any> implements EventTarget { | ||
declare class ContextInternals<TNode = unknown, TScope = unknown, TRoot extends TNode = TNode, TResult = unknown> { | ||
/** | ||
* @internal | ||
* flags - A bitmask. See CONTEXT FLAGS above. | ||
*/ | ||
_f: number; | ||
f: number; | ||
/** | ||
* @internal | ||
* facade - The actual object passed as this to components. | ||
*/ | ||
facade: Context<unknown, TResult>; | ||
/** | ||
* renderer - The renderer which created this context. | ||
*/ | ||
_re: Renderer<unknown, unknown, unknown, TResult>; | ||
renderer: RendererImpl<TNode, TScope, TRoot, TResult>; | ||
/** | ||
* @internal | ||
* root - The root node as set by the nearest ancestor portal. | ||
*/ | ||
_rt: unknown; | ||
root: TRoot | undefined; | ||
/** | ||
* @internal | ||
* host - The nearest ancestor host element. | ||
* host - The nearest host or portal retainer. | ||
* | ||
@@ -444,56 +329,63 @@ * When refresh is called, the host element will be arranged as the last step | ||
*/ | ||
_ho: Element<string | symbol>; | ||
host: Retainer<TNode>; | ||
/** | ||
* @internal | ||
* parent - The parent context. | ||
*/ | ||
_pa: Context<unknown, TResult> | undefined; | ||
parent: ContextInternals<TNode, TScope, TRoot, TResult> | undefined; | ||
/** | ||
* @internal | ||
* scope - The value of the scope at the point of element’s creation. | ||
*/ | ||
_sc: unknown; | ||
scope: TScope | undefined; | ||
/** | ||
* @internal | ||
* el - The associated component element. | ||
* retainer - The internal node associated with this context. | ||
*/ | ||
_el: Element<Component>; | ||
ret: Retainer<TNode>; | ||
/** | ||
* @internal | ||
* iterator - The iterator returned by the component function. | ||
*/ | ||
_it: Iterator<Children, Children | void, unknown> | AsyncIterator<Children, Children | void, unknown> | undefined; | ||
iterator: Iterator<Children, Children | void, unknown> | AsyncIterator<Children, Children | void, unknown> | undefined; | ||
/*** async properties ***/ | ||
/** | ||
* @internal | ||
* 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. | ||
*/ | ||
_oa: Function | undefined; | ||
/** | ||
* @internal | ||
* inflightBlock | ||
*/ | ||
_ib: Promise<unknown> | undefined; | ||
inflightBlock: Promise<unknown> | undefined; | ||
/** | ||
* @internal | ||
* inflightValue | ||
*/ | ||
_iv: Promise<ElementValue<any>> | undefined; | ||
inflightValue: Promise<ElementValue<TNode>> | undefined; | ||
/** | ||
* @internal | ||
* enqueuedBlock | ||
*/ | ||
_eb: Promise<unknown> | undefined; | ||
enqueuedBlock: Promise<unknown> | undefined; | ||
/** | ||
* @internal | ||
* enqueuedValue | ||
*/ | ||
_ev: Promise<ElementValue<any>> | undefined; | ||
enqueuedValue: Promise<ElementValue<TNode>> | undefined; | ||
/** | ||
* onavailable - A callback used in conjunction with the IsAvailable flag to | ||
* implement the props async iterator. See the Symbol.asyncIterator method | ||
* and the resumeCtxIterator function. | ||
*/ | ||
onAvailable: Function | undefined; | ||
constructor(renderer: RendererImpl<TNode, TScope, TRoot, TResult>, root: TRoot | undefined, host: Retainer<TNode>, parent: ContextInternals<TNode, TScope, TRoot, TResult> | undefined, scope: TScope | undefined, ret: Retainer<TNode>); | ||
} | ||
export declare const ContextInternalsSymbol: unique symbol; | ||
/** | ||
* 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. | ||
* | ||
* @template [TProps=*] - The expected shape of the props passed to the | ||
* component. Used to strongly type the Context iterator methods. | ||
* @template [TResult=*] - The readable element value type. It is used in | ||
* places such as the return value of refresh and the argument passed to | ||
* schedule and cleanup callbacks. | ||
*/ | ||
export declare class Context<TProps = any, TResult = any> implements EventTarget { | ||
/** | ||
* @internal | ||
* Contexts should never be instantiated directly. | ||
*/ | ||
constructor(renderer: Renderer<unknown, unknown, unknown, TResult>, root: unknown, host: Element<string | symbol>, parent: Context<unknown, TResult> | undefined, scope: unknown, el: Element<Component>); | ||
[ContextInternalsSymbol]: ContextInternals<unknown, unknown, unknown, TResult>; | ||
constructor(internals: ContextInternals<unknown, unknown, unknown, TResult>); | ||
/** | ||
@@ -511,4 +403,4 @@ * The current props of the associated element. | ||
* 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. | ||
* or the refresh, schedule, cleanup, or flush methods. This property is | ||
* mainly for plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -537,2 +429,7 @@ get value(): TResult; | ||
/** | ||
* Registers a callback which fires when the component’s children are | ||
* rendered into the root. Will only fire once per callback and render. | ||
*/ | ||
flush(callback: (value: TResult) => unknown): void; | ||
/** | ||
* Registers a callback which fires when the component unmounts. Will only | ||
@@ -539,0 +436,0 @@ * fire once per callback. |
1763
crank.js
/// <reference types="./crank.d.ts" /> | ||
const NOOP = () => { }; | ||
const IDENTITY = (value) => value; | ||
function wrap(value) { | ||
@@ -77,25 +78,3 @@ return value === undefined ? [] : Array.isArray(value) ? value : [value]; | ||
const ElementSymbol = Symbol.for("crank.Element"); | ||
/*** ELEMENT FLAGS ***/ | ||
/** | ||
* A flag which is set when the element is mounted, used to detect whether an | ||
* element is being reused so that we clone it rather than accidentally | ||
* overwriting its state. | ||
* | ||
* Changing this flag value would likely be a breaking changes in terms of | ||
* interop between elements and renderers of different versions of Crank. | ||
* | ||
* TODO: Consider deleting this flag because we’re not using it anymore. | ||
*/ | ||
const IsInUse = 1 << 0; | ||
/** | ||
* A flag which tracks whether the element has previously rendered children, | ||
* used to clear elements which no longer render children in the next render. | ||
* We may deprecate this behavior and make elements without explicit children | ||
* uncontrolled. | ||
*/ | ||
const HadChildren = 1 << 1; | ||
// 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. | ||
/** | ||
* Elements are the basic building blocks of Crank applications. They are | ||
@@ -121,4 +100,3 @@ * JavaScript objects which are interpreted by special classes called renderers | ||
class Element { | ||
constructor(tag, props, key, ref) { | ||
this._f = 0; | ||
constructor(tag, props, key, ref, static_) { | ||
this.tag = tag; | ||
@@ -128,11 +106,4 @@ this.props = props; | ||
this.ref = ref; | ||
this._ch = undefined; | ||
this._n = undefined; | ||
this._fb = undefined; | ||
this._ic = undefined; | ||
this._ov = undefined; | ||
this.static_ = static_; | ||
} | ||
get hadChildren() { | ||
return (this._f & HadChildren) !== 0; | ||
} | ||
} | ||
@@ -155,2 +126,3 @@ Element.prototype.$$typeof = ElementSymbol; | ||
let ref; | ||
let static_ = false; | ||
const props1 = {}; | ||
@@ -172,2 +144,5 @@ if (props != null) { | ||
break; | ||
case "crank-static": | ||
static_ = !!props["crank-static"]; | ||
break; | ||
default: | ||
@@ -184,9 +159,6 @@ props1[name] = props[name]; | ||
} | ||
return new Element(tag, props1, key, ref); | ||
return new Element(tag, props1, key, ref, static_); | ||
} | ||
/** | ||
* Clones a given element, shallowly copying the props object. | ||
* | ||
* Used internally to make sure we don’t accidentally reuse elements when | ||
* rendering. | ||
*/ | ||
@@ -260,2 +232,14 @@ function cloneElement(el) { | ||
} | ||
class Retainer { | ||
constructor(el) { | ||
this.el = el; | ||
this.value = undefined; | ||
this.ctx = undefined; | ||
this.children = undefined; | ||
this.cached = undefined; | ||
this.fallback = undefined; | ||
this.inflight = undefined; | ||
this.onCommit = undefined; | ||
} | ||
} | ||
/** | ||
@@ -266,29 +250,17 @@ * Finds the value of the element according to its type. | ||
*/ | ||
function getValue(el) { | ||
if (typeof el._fb !== "undefined") { | ||
return typeof el._fb === "object" ? getValue(el._fb) : el._fb; | ||
function getValue(ret) { | ||
if (typeof ret.fallback !== "undefined") { | ||
return typeof ret.fallback === "object" | ||
? getValue(ret.fallback) | ||
: ret.fallback; | ||
} | ||
else if (el.tag === Portal) { | ||
return undefined; | ||
else if (ret.el.tag === Portal) { | ||
return; | ||
} | ||
else if (typeof el.tag !== "function" && el.tag !== Fragment) { | ||
return el._n; | ||
else if (typeof ret.el.tag !== "function" && ret.el.tag !== Fragment) { | ||
return ret.value; | ||
} | ||
return unwrap(getChildValues(el)); | ||
return unwrap(getChildValues(ret)); | ||
} | ||
/** | ||
* This function is only used to make sure <Copy /> elements wait for the | ||
* current run of async elements, but it’s somewhat complex so I put it here. | ||
*/ | ||
function getInflightValue(el) { | ||
const ctx = typeof el.tag === "function" ? el._n : undefined; | ||
if (ctx && ctx._f & IsUpdating && ctx._iv) { | ||
return ctx._iv; // inflightValue | ||
} | ||
else if (el._ic) { | ||
return el._ic; // inflightChildren | ||
} | ||
return getValue(el); | ||
} | ||
/** | ||
* Walks an element’s children to find its child values. | ||
@@ -298,5 +270,8 @@ * | ||
*/ | ||
function getChildValues(el) { | ||
function getChildValues(ret) { | ||
if (ret.cached) { | ||
return wrap(ret.cached); | ||
} | ||
const values = []; | ||
const children = wrap(el._ch); | ||
const children = wrap(ret.children); | ||
for (let i = 0; i < children.length; i++) { | ||
@@ -308,9 +283,26 @@ const child = children[i]; | ||
} | ||
return normalize(values); | ||
const values1 = normalize(values); | ||
const tag = ret.el.tag; | ||
if (typeof tag === "function" || (tag !== Fragment && tag !== Raw)) { | ||
ret.cached = unwrap(values1); | ||
} | ||
return values1; | ||
} | ||
const defaultRendererImpl = { | ||
create() { | ||
throw new Error("Not implemented"); | ||
}, | ||
scope: IDENTITY, | ||
read: IDENTITY, | ||
escape: IDENTITY, | ||
parse: IDENTITY, | ||
patch: NOOP, | ||
arrange: NOOP, | ||
dispose: NOOP, | ||
flush: NOOP, | ||
}; | ||
/** | ||
* 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. | ||
* process and caching previous trees by root. | ||
* | ||
@@ -323,4 +315,8 @@ * @template TNode - The type of the node for a rendering environment. | ||
class Renderer { | ||
constructor() { | ||
this._cache = new WeakMap(); | ||
constructor(impl) { | ||
this.cache = new WeakMap(); | ||
this.impl = { | ||
...defaultRendererImpl, | ||
...impl, | ||
}; | ||
} | ||
@@ -335,5 +331,5 @@ /** | ||
* @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. | ||
* elements in the tree. Useful for connecting different renderers so that | ||
* events/provisions properly propagate. The context for a given root must be | ||
* the same or an error will be thrown. | ||
* | ||
@@ -343,343 +339,177 @@ * @returns The result of rendering the children, or a possible promise of | ||
*/ | ||
render(children, root, ctx) { | ||
let portal; | ||
render(children, root, bridge) { | ||
let ret; | ||
const ctx = bridge && bridge[ContextInternalsSymbol]; | ||
if (typeof root === "object" && root !== null) { | ||
portal = this._cache.get(root); | ||
ret = this.cache.get(root); | ||
} | ||
if (portal === undefined) { | ||
portal = createElement(Portal, { children, root }); | ||
portal._n = ctx; | ||
let oldProps; | ||
if (ret === undefined) { | ||
ret = new Retainer(createElement(Portal, { children, root })); | ||
ret.value = root; | ||
ret.ctx = ctx; | ||
if (typeof root === "object" && root !== null && children != null) { | ||
this._cache.set(root, portal); | ||
this.cache.set(root, ret); | ||
} | ||
} | ||
else if (ret.ctx !== ctx) { | ||
throw new Error("Context mismatch"); | ||
} | ||
else { | ||
if (portal._n !== ctx) { | ||
throw new Error("Context mismatch"); | ||
} | ||
portal.props = { children, root }; | ||
oldProps = ret.el.props; | ||
ret.el = createElement(Portal, { children, root }); | ||
if (typeof root === "object" && root !== null && children == null) { | ||
this._cache.delete(root); | ||
this.cache.delete(root); | ||
} | ||
} | ||
const value = update(this, root, portal, ctx, undefined, portal); | ||
const scope = this.impl.scope(undefined, Portal, ret.el.props); | ||
const childValues = diffChildren(this.impl, root, ret, ctx, scope, ret, children); | ||
// We return the child values of the portal because portal elements | ||
// themselves have no readable value. | ||
if (isPromiseLike(value)) { | ||
return value.then(() => { | ||
const result = this.read(unwrap(getChildValues(portal))); | ||
if (root == null) { | ||
unmount(this, portal, undefined, portal); | ||
} | ||
return result; | ||
}); | ||
if (isPromiseLike(childValues)) { | ||
return childValues.then((childValues) => commitRootRender(this.impl, root, ctx, ret, childValues, oldProps)); | ||
} | ||
const result = this.read(unwrap(getChildValues(portal))); | ||
if (root == null) { | ||
unmount(this, portal, undefined, portal); | ||
} | ||
return result; | ||
return commitRootRender(this.impl, root, ctx, ret, childValues, oldProps); | ||
} | ||
/** | ||
* Called when an element’s rendered 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. | ||
* | ||
* 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. | ||
*/ | ||
read(value) { | ||
return value; | ||
} | ||
/** | ||
* Called in a preorder traversal for each host element. | ||
* | ||
* 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. | ||
* | ||
* @param el - The host element. | ||
* @param scope - The current scope. | ||
* | ||
* @returns The scope to be passed to create and scope for child host | ||
* elements. | ||
* | ||
* This method sets the scope for child host elements, not the current host | ||
* element. | ||
*/ | ||
scope(_el, scope) { | ||
return scope; | ||
} | ||
/** | ||
* Called for each string in an element tree. | ||
* | ||
* @param text - The string child. | ||
* @param scope - The current scope. | ||
* | ||
* @returns The escaped string. | ||
* | ||
* 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. | ||
*/ | ||
escape(text, _scope) { | ||
return text; | ||
} | ||
/** | ||
* Called for each Raw element whose value prop is a string. | ||
* | ||
* @param text - The string child. | ||
* @param scope - The current scope. | ||
* | ||
* @returns The parsed node or string. | ||
*/ | ||
parse(text, _scope) { | ||
return text; | ||
} | ||
/** | ||
* Called for each host element when it is committed for the first time. | ||
* | ||
* @param el - The host element. | ||
* @param scope - The current scope. | ||
* | ||
* @returns A “node” which determines the value of the host element. | ||
*/ | ||
create(_el, _scope) { | ||
throw new Error("Not implemented"); | ||
} | ||
/** | ||
* Called for each host element when it is committed. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* | ||
* @returns The return value is ignored. | ||
* | ||
* Used to mutate the node associated with an element when new props are | ||
* passed. | ||
*/ | ||
patch(_el, _node) { | ||
return; | ||
} | ||
// TODO: pass hints into arrange about where the dirty children start and end | ||
/** | ||
* Called for each host element so that elements can be arranged into a tree. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* @param children - An array of nodes and strings from child elements. | ||
* | ||
* @returns The return value is ignored. | ||
* | ||
* This method is also called by child components contexts as the last step | ||
* of a refresh. | ||
*/ | ||
arrange(_el, _node, _children) { | ||
return; | ||
} | ||
// 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 el - The host element. | ||
* @param node - The node associated with the host element. | ||
* | ||
* @returns The return value is ignored. | ||
*/ | ||
dispose(_el, _node) { | ||
return; | ||
} | ||
/** | ||
* Called at the end of the rendering process for each root of the tree. | ||
* | ||
* @param root - The root prop passed to portals or the render method. | ||
* | ||
* @returns The return value is ignored. | ||
*/ | ||
complete(_root) { | ||
return; | ||
} | ||
} | ||
/*** PRIVATE RENDERER FUNCTIONS ***/ | ||
function mount(renderer, root, host, ctx, scope, el) { | ||
el._f |= IsInUse; | ||
if (typeof el.tag === "function") { | ||
el._n = new Context(renderer, root, host, ctx, scope, el); | ||
return updateCtx(el._n); | ||
function commitRootRender(renderer, root, ctx, ret, childValues, oldProps) { | ||
// element is a host or portal element | ||
if (root !== undefined) { | ||
renderer.arrange(Portal, root, ret.el.props, childValues, oldProps, wrap(ret.cached)); | ||
flush(renderer, root); | ||
} | ||
else if (el.tag === Raw) { | ||
return commit(renderer, scope, el, []); | ||
ret.cached = unwrap(childValues); | ||
if (root == null) { | ||
unmount(renderer, ret, ctx, ret); | ||
} | ||
else if (el.tag !== Fragment) { | ||
if (el.tag === Portal) { | ||
root = el.props.root; | ||
} | ||
else { | ||
el._n = renderer.create(el, scope); | ||
renderer.patch(el, el._n); | ||
} | ||
host = el; | ||
scope = renderer.scope(host, scope); | ||
} | ||
return updateChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
return renderer.read(ret.cached); | ||
} | ||
function update(renderer, root, host, ctx, scope, el) { | ||
if (typeof el.tag === "function") { | ||
return updateCtx(el._n); | ||
} | ||
else if (el.tag === Raw) { | ||
return commit(renderer, scope, el, []); | ||
} | ||
else if (el.tag !== Fragment) { | ||
if (el.tag === Portal) { | ||
root = el.props.root; | ||
} | ||
else { | ||
renderer.patch(el, el._n); | ||
} | ||
host = el; | ||
scope = renderer.scope(host, scope); | ||
} | ||
return updateChildren(renderer, root, host, ctx, scope, el, el.props.children); | ||
} | ||
function createChildrenByKey(children) { | ||
const childrenByKey = new Map(); | ||
for (let i = 0; i < children.length; i++) { | ||
const child = children[i]; | ||
if (typeof child === "object" && typeof child.key !== "undefined") { | ||
childrenByKey.set(child.key, child); | ||
} | ||
} | ||
return childrenByKey; | ||
} | ||
function updateChildren(renderer, root, host, ctx, scope, el, children) { | ||
const oldChildren = wrap(el._ch); | ||
function diffChildren(renderer, root, host, ctx, scope, parent, children) { | ||
const oldRetained = wrap(parent.children); | ||
const newRetained = []; | ||
const newChildren = arrayify(children); | ||
const newChildren1 = []; | ||
const values = []; | ||
let graveyard; | ||
let childrenByKey; | ||
let seenKeys; | ||
let childrenByKey; | ||
let isAsync = false; | ||
let i = 0; | ||
for (let j = 0, il = oldChildren.length, jl = newChildren.length; j < jl; j++) { | ||
let oldChild = i >= il ? undefined : oldChildren[i]; | ||
let newChild = narrow(newChildren[j]); | ||
// ALIGNMENT | ||
let oldKey = typeof oldChild === "object" ? oldChild.key : undefined; | ||
let newKey = typeof newChild === "object" ? newChild.key : undefined; | ||
if (newKey !== undefined && seenKeys && seenKeys.has(newKey)) { | ||
console.error("Duplicate key", newKey); | ||
newKey = undefined; | ||
} | ||
if (oldKey === newKey) { | ||
if (childrenByKey !== undefined && newKey !== undefined) { | ||
childrenByKey.delete(newKey); | ||
let oi = 0, oldLength = oldRetained.length; | ||
for (let ni = 0, newLength = newChildren.length; ni < newLength; ni++) { | ||
// We make sure we don’t access indices out of bounds to prevent | ||
// deoptimizations. | ||
let ret = oi >= oldLength ? undefined : oldRetained[oi]; | ||
let child = narrow(newChildren[ni]); | ||
{ | ||
// Aligning new children with old retainers | ||
let oldKey = typeof ret === "object" ? ret.el.key : undefined; | ||
let newKey = typeof child === "object" ? child.key : undefined; | ||
if (newKey !== undefined && seenKeys && seenKeys.has(newKey)) { | ||
console.error("Duplicate key", newKey); | ||
newKey = undefined; | ||
} | ||
i++; | ||
} | ||
else { | ||
if (!childrenByKey) { | ||
childrenByKey = createChildrenByKey(oldChildren.slice(i)); | ||
} | ||
if (newKey === undefined) { | ||
while (oldChild !== undefined && oldKey !== undefined) { | ||
i++; | ||
oldChild = oldChildren[i]; | ||
oldKey = typeof oldChild === "object" ? oldChild.key : undefined; | ||
if (oldKey === newKey) { | ||
if (childrenByKey !== undefined && newKey !== undefined) { | ||
childrenByKey.delete(newKey); | ||
} | ||
i++; | ||
oi++; | ||
} | ||
else { | ||
oldChild = childrenByKey.get(newKey); | ||
if (oldChild !== undefined) { | ||
childrenByKey.delete(newKey); | ||
childrenByKey = childrenByKey || createChildrenByKey(oldRetained, oi); | ||
if (newKey === undefined) { | ||
while (ret !== undefined && oldKey !== undefined) { | ||
oi++; | ||
ret = oldRetained[oi]; | ||
oldKey = typeof ret === "object" ? ret.el.key : undefined; | ||
} | ||
oi++; | ||
} | ||
if (!seenKeys) { | ||
seenKeys = new Set(); | ||
else { | ||
ret = childrenByKey.get(newKey); | ||
if (ret !== undefined) { | ||
childrenByKey.delete(newKey); | ||
} | ||
(seenKeys = seenKeys || new Set()).add(newKey); | ||
} | ||
seenKeys.add(newKey); | ||
} | ||
} | ||
// UPDATING | ||
// Updating | ||
let value; | ||
if (typeof oldChild === "object" && | ||
typeof newChild === "object" && | ||
oldChild.tag === newChild.tag) { | ||
if (oldChild.tag === Portal && | ||
oldChild.props.root !== newChild.props.root) { | ||
renderer.arrange(oldChild, oldChild.props.root, []); | ||
renderer.complete(oldChild.props.root); | ||
if (typeof child === "object") { | ||
if (typeof ret === "object" && child.static_) { | ||
ret.el = child; | ||
value = getInflightValue(ret); | ||
} | ||
// TODO: implement Raw element parse caching | ||
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) { | ||
value = | ||
typeof oldChild === "object" | ||
? getInflightValue(oldChild) | ||
: oldChild; | ||
if (typeof newChild.ref === "function") { | ||
if (isPromiseLike(value)) { | ||
value.then(newChild.ref).catch(NOOP); | ||
else if (child.tag === Copy) { | ||
value = getInflightValue(ret); | ||
} | ||
else { | ||
let oldProps; | ||
if (typeof ret === "object" && ret.el.tag === child.tag) { | ||
oldProps = ret.el.props; | ||
ret.el = child; | ||
} | ||
else { | ||
if (typeof ret === "object") { | ||
(graveyard = graveyard || []).push(ret); | ||
} | ||
else { | ||
newChild.ref(value); | ||
} | ||
const fallback = ret; | ||
ret = new Retainer(child); | ||
ret.fallback = fallback; | ||
} | ||
newChild = oldChild; | ||
if (child.tag === Raw) { | ||
value = updateRaw(renderer, ret, scope, oldProps); | ||
} | ||
else if (child.tag === Fragment) { | ||
value = updateFragment(renderer, root, host, ctx, scope, ret); | ||
} | ||
else if (typeof child.tag === "function") { | ||
value = updateComponent(renderer, root, host, ctx, scope, ret, oldProps); | ||
} | ||
else { | ||
value = updateHost(renderer, root, ctx, scope, ret, oldProps); | ||
} | ||
} | ||
else { | ||
newChild = new Element(newChild.tag, newChild.props, newChild.key, newChild.ref); | ||
value = mount(renderer, root, host, ctx, scope, newChild); | ||
if (isPromiseLike(value)) { | ||
newChild._fb = oldChild; | ||
const ref = child.ref; | ||
if (isPromiseLike(value)) { | ||
isAsync = true; | ||
if (typeof ref === "function") { | ||
value = value.then((value) => { | ||
ref(renderer.read(value)); | ||
return value; | ||
}); | ||
} | ||
} | ||
else if (typeof ref === "function") { | ||
ref(renderer.read(value)); | ||
} | ||
} | ||
else if (typeof newChild === "string") { | ||
newChild = value = renderer.escape(newChild, scope); | ||
} | ||
newChildren1[j] = newChild; | ||
values[j] = value; | ||
isAsync = isAsync || isPromiseLike(value); | ||
if (typeof oldChild === "object" && oldChild !== newChild) { | ||
if (!graveyard) { | ||
graveyard = []; | ||
else { | ||
// child is a string or undefined | ||
if (typeof ret === "object") { | ||
(graveyard = graveyard || []).push(ret); | ||
} | ||
graveyard.push(oldChild); | ||
if (typeof child === "string") { | ||
value = ret = renderer.escape(child, scope); | ||
} | ||
else { | ||
ret = undefined; | ||
} | ||
} | ||
values[ni] = value; | ||
newRetained[ni] = ret; | ||
} | ||
el._ch = unwrap(newChildren1); | ||
// cleanup | ||
for (; i < oldChildren.length; i++) { | ||
const oldChild = oldChildren[i]; | ||
if (typeof oldChild === "object" && typeof oldChild.key === "undefined") { | ||
if (!graveyard) { | ||
graveyard = []; | ||
} | ||
graveyard.push(oldChild); | ||
// cleanup remaining retainers | ||
for (; oi < oldLength; oi++) { | ||
const ret = oldRetained[oi]; | ||
if (typeof ret === "object" && typeof ret.el.key === "undefined") { | ||
(graveyard = graveyard || []).push(ret); | ||
} | ||
} | ||
if (childrenByKey !== undefined && childrenByKey.size > 0) { | ||
if (!graveyard) { | ||
graveyard = []; | ||
} | ||
graveyard.push(...childrenByKey.values()); | ||
(graveyard = graveyard || []).push(...childrenByKey.values()); | ||
} | ||
parent.children = unwrap(newRetained); | ||
if (isAsync) { | ||
let values1 = Promise.all(values).finally(() => { | ||
let childValues1 = Promise.all(values).finally(() => { | ||
if (graveyard) { | ||
@@ -691,13 +521,15 @@ for (let i = 0; i < graveyard.length; i++) { | ||
}); | ||
let onvalues; | ||
values1 = Promise.race([ | ||
values1, | ||
new Promise((resolve) => (onvalues = resolve)), | ||
let onChildValues; | ||
childValues1 = Promise.race([ | ||
childValues1, | ||
new Promise((resolve) => (onChildValues = resolve)), | ||
]); | ||
if (el._ov) { | ||
el._ov(values1); | ||
if (parent.onCommit) { | ||
parent.onCommit(childValues1); | ||
} | ||
el._ov = onvalues; | ||
const children = (el._ic = values1.then((values) => commit(renderer, scope, el, normalize(values)))); | ||
return children; | ||
parent.onCommit = onChildValues; | ||
return childValues1.then((childValues) => { | ||
parent.inflight = parent.fallback = undefined; | ||
return normalize(childValues); | ||
}); | ||
} | ||
@@ -709,75 +541,157 @@ if (graveyard) { | ||
} | ||
if (el._ov) { | ||
el._ov(values); | ||
el._ov = undefined; | ||
if (parent.onCommit) { | ||
parent.onCommit(values); | ||
parent.onCommit = undefined; | ||
} | ||
return commit(renderer, scope, el, normalize(values)); | ||
parent.inflight = parent.fallback = undefined; | ||
// We can assert there are no promises in the array because isAsync is false | ||
return normalize(values); | ||
} | ||
function commit(renderer, scope, el, values) { | ||
if (el._ic) { | ||
el._ic = undefined; | ||
function createChildrenByKey(children, offset) { | ||
const childrenByKey = new Map(); | ||
for (let i = offset; i < children.length; i++) { | ||
const child = children[i]; | ||
if (typeof child === "object" && typeof child.el.key !== "undefined") { | ||
childrenByKey.set(child.el.key, child); | ||
} | ||
} | ||
// Need to handle (_fb) fallback being the empty string. | ||
if (typeof el._fb !== "undefined") { | ||
el._fb = undefined; | ||
return childrenByKey; | ||
} | ||
function getInflightValue(child) { | ||
if (typeof child !== "object") { | ||
return child; | ||
} | ||
let value; | ||
if (typeof el.tag === "function") { | ||
value = commitCtx(el._n, values); | ||
const ctx = typeof child.el.tag === "function" ? child.ctx : undefined; | ||
if (ctx && ctx.f & IsUpdating && ctx.inflightValue) { | ||
return ctx.inflightValue; | ||
} | ||
else if (el.tag === Raw) { | ||
if (typeof el.props.value === "string") { | ||
el._n = renderer.parse(el.props.value, scope); | ||
else if (child.inflight) { | ||
return child.inflight; | ||
} | ||
return getValue(child); | ||
} | ||
function updateRaw(renderer, ret, scope, oldProps) { | ||
const props = ret.el.props; | ||
if (typeof props.value === "string") { | ||
if (!oldProps || oldProps.value !== props.value) { | ||
ret.value = renderer.parse(props.value, scope); | ||
} | ||
else { | ||
el._n = el.props.value; | ||
} | ||
else { | ||
ret.value = props.value; | ||
} | ||
return ret.value; | ||
} | ||
function updateFragment(renderer, root, host, ctx, scope, ret) { | ||
const childValues = diffChildren(renderer, root, host, ctx, scope, ret, ret.el.props.children); | ||
if (isPromiseLike(childValues)) { | ||
ret.inflight = childValues.then((childValues) => unwrap(childValues)); | ||
return ret.inflight; | ||
} | ||
return unwrap(childValues); | ||
} | ||
function updateHost(renderer, root, ctx, scope, ret, oldProps) { | ||
const el = ret.el; | ||
const tag = el.tag; | ||
if (el.tag === Portal) { | ||
root = ret.value = el.props.root; | ||
} | ||
else if (!oldProps) { | ||
// We use the truthiness of oldProps to determine if this the first render. | ||
ret.value = renderer.create(tag, el.props, scope); | ||
} | ||
scope = renderer.scope(scope, tag, el.props); | ||
const childValues = diffChildren(renderer, root, ret, ctx, scope, ret, ret.el.props.children); | ||
if (isPromiseLike(childValues)) { | ||
ret.inflight = childValues.then((childValues) => commitHost(renderer, scope, ret, childValues, oldProps)); | ||
return ret.inflight; | ||
} | ||
return commitHost(renderer, scope, ret, childValues, oldProps); | ||
} | ||
function commitHost(renderer, scope, ret, childValues, oldProps) { | ||
const tag = ret.el.tag; | ||
const value = ret.value; | ||
let props = ret.el.props; | ||
let copied; | ||
if (tag !== Portal) { | ||
for (const propName in { ...oldProps, ...props }) { | ||
const propValue = props[propName]; | ||
if (propValue === Copy) { | ||
(copied = copied || new Set()).add(propName); | ||
} | ||
else if (propName !== "children") { | ||
renderer.patch(tag, value, propName, propValue, oldProps && oldProps[propName], scope); | ||
} | ||
} | ||
value = el._n; | ||
} | ||
else if (el.tag === Fragment) { | ||
value = unwrap(values); | ||
if (copied) { | ||
props = { ...ret.el.props }; | ||
for (const name of copied) { | ||
props[name] = oldProps && oldProps[name]; | ||
} | ||
ret.el = new Element(tag, props, ret.el.key, ret.el.ref); | ||
} | ||
else { | ||
if (el.tag === Portal) { | ||
renderer.arrange(el, el.props.root, values); | ||
renderer.complete(el.props.root); | ||
renderer.arrange(tag, value, props, childValues, oldProps, wrap(ret.cached)); | ||
ret.cached = unwrap(childValues); | ||
if (tag === Portal) { | ||
flush(renderer, ret.value); | ||
return; | ||
} | ||
return value; | ||
} | ||
function flush(renderer, root, initiator) { | ||
renderer.flush(root); | ||
if (typeof root !== "object" || root === null) { | ||
return; | ||
} | ||
const flushMap = flushMaps.get(root); | ||
if (flushMap) { | ||
if (initiator) { | ||
const flushMap1 = new Map(); | ||
for (let [ctx, callbacks] of flushMap) { | ||
if (!ctxContains(initiator, ctx)) { | ||
flushMap.delete(ctx); | ||
flushMap1.set(ctx, callbacks); | ||
} | ||
} | ||
if (flushMap1.size) { | ||
flushMaps.set(root, flushMap1); | ||
} | ||
else { | ||
flushMaps.delete(root); | ||
} | ||
} | ||
else { | ||
renderer.arrange(el, el._n, values); | ||
flushMaps.delete(root); | ||
} | ||
value = el._n; | ||
if (values.length) { | ||
el._f |= HadChildren; | ||
for (const [ctx, callbacks] of flushMap) { | ||
const value = renderer.read(getValue(ctx.ret)); | ||
for (const callback of callbacks) { | ||
callback(value); | ||
} | ||
} | ||
else { | ||
el._f &= ~HadChildren; | ||
} | ||
} | ||
if (el.ref) { | ||
el.ref(renderer.read(value)); | ||
} | ||
return value; | ||
} | ||
function unmount(renderer, host, ctx, el) { | ||
if (typeof el.tag === "function") { | ||
unmountCtx(el._n); | ||
ctx = el._n; | ||
function unmount(renderer, host, ctx, ret) { | ||
if (typeof ret.el.tag === "function") { | ||
ctx = ret.ctx; | ||
unmountComponent(ctx); | ||
} | ||
else if (el.tag === Portal) { | ||
host = el; | ||
renderer.arrange(host, host.props.root, []); | ||
renderer.complete(host.props.root); | ||
else if (ret.el.tag === Portal) { | ||
host = ret; | ||
renderer.arrange(Portal, host.value, host.el.props, [], host.el.props, wrap(host.cached)); | ||
flush(renderer, host.value); | ||
} | ||
else if (el.tag !== Fragment) { | ||
if (isEventTarget(el._n)) { | ||
const listeners = getListeners(ctx, host); | ||
for (let i = 0; i < listeners.length; i++) { | ||
const record = listeners[i]; | ||
el._n.removeEventListener(record.type, record.callback, record.options); | ||
else if (ret.el.tag !== Fragment) { | ||
if (isEventTarget(ret.value)) { | ||
const records = getListenerRecords(ctx, host); | ||
for (let i = 0; i < records.length; i++) { | ||
const record = records[i]; | ||
ret.value.removeEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
host = el; | ||
renderer.dispose(host, host._n); | ||
renderer.dispose(ret.el.tag, ret.value, ret.el.props); | ||
host = ret; | ||
} | ||
const children = wrap(el._ch); | ||
const children = wrap(ret.children); | ||
for (let i = 0; i < children.length; i++) { | ||
@@ -790,8 +704,7 @@ const child = children[i]; | ||
} | ||
// TODO: Now that we have element flags again, we should probably merge these flags. | ||
/*** 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. | ||
* cleared when the component has committed. Used to determine things like | ||
* whether the nearest host ancestor needs to be rearranged. | ||
*/ | ||
@@ -815,3 +728,3 @@ const IsUpdating = 1 << 0; | ||
* context async iterator. See the Symbol.asyncIterator method and the | ||
* resumeCtx function. | ||
* resumeCtxIterator function. | ||
*/ | ||
@@ -821,31 +734,65 @@ const IsAvailable = 1 << 3; | ||
* A flag which is set when a generator components returns, i.e. the done | ||
* property on the generator is set to true or throws. Done components will | ||
* stick to their last rendered value and ignore further updates. | ||
* property on the iteration is set to true. Generator components will stick to | ||
* their last rendered value and ignore further updates. | ||
*/ | ||
const IsDone = 1 << 4; | ||
/** | ||
* A flag which is set when a generator component errors. | ||
* | ||
* NOTE: This is mainly used to prevent some false positives in component | ||
* yields or returns undefined warnings. The reason we’re using this versus | ||
* IsUnmounted is a very troubling jest test (cascades sync generator parent | ||
* and sync generator child) where synchronous code causes a stack overflow | ||
* error in a non-deterministic way. Deeply disturbing stuff. | ||
*/ | ||
const IsErrored = 1 << 5; | ||
/** | ||
* A flag which is set when the component is unmounted. Unmounted components | ||
* are no longer in the element tree and cannot refresh or rerender. | ||
*/ | ||
const IsUnmounted = 1 << 5; | ||
const IsUnmounted = 1 << 6; | ||
/** | ||
* A flag which indicates that the component is a sync generator component. | ||
*/ | ||
const IsSyncGen = 1 << 6; | ||
const IsSyncGen = 1 << 7; | ||
/** | ||
* A flag which indicates that the component is an async generator component. | ||
*/ | ||
const IsAsyncGen = 1 << 7; | ||
const IsAsyncGen = 1 << 8; | ||
/** | ||
* A flag which is set while schedule callbacks are called. | ||
*/ | ||
const IsScheduling = 1 << 8; | ||
const IsScheduling = 1 << 9; | ||
/** | ||
* A flag which is set when a schedule callback calls refresh. | ||
*/ | ||
const IsSchedulingRefresh = 1 << 9; | ||
const IsSchedulingRefresh = 1 << 10; | ||
const provisionMaps = new WeakMap(); | ||
const scheduleMap = new WeakMap(); | ||
const cleanupMap = new WeakMap(); | ||
// keys are roots | ||
const flushMaps = new WeakMap(); | ||
/** | ||
* @internal | ||
*/ | ||
class ContextInternals { | ||
constructor(renderer, root, host, parent, scope, ret) { | ||
this.f = 0; | ||
this.facade = new Context(this); | ||
this.renderer = renderer; | ||
this.root = root; | ||
this.host = host; | ||
this.parent = parent; | ||
this.scope = scope; | ||
this.ret = ret; | ||
this.iterator = undefined; | ||
this.inflightBlock = undefined; | ||
this.inflightValue = undefined; | ||
this.enqueuedBlock = undefined; | ||
this.enqueuedValue = undefined; | ||
this.onAvailable = undefined; | ||
} | ||
} | ||
const ContextInternalsSymbol = Symbol.for("Crank.ContextInternals"); | ||
/** | ||
* A class which is instantiated and passed to every component as its this | ||
@@ -863,20 +810,4 @@ * value. Contexts form a tree just like elements and all components in the | ||
class Context { | ||
/** | ||
* @internal | ||
* Contexts should never be instantiated directly. | ||
*/ | ||
constructor(renderer, root, host, parent, scope, el) { | ||
this._f = 0; | ||
this._re = renderer; | ||
this._rt = root; | ||
this._ho = host; | ||
this._pa = parent; | ||
this._sc = scope; | ||
this._el = el; | ||
this._it = undefined; | ||
this._oa = undefined; | ||
this._ib = undefined; | ||
this._iv = undefined; | ||
this._eb = undefined; | ||
this._ev = undefined; | ||
constructor(internals) { | ||
this[ContextInternalsSymbol] = internals; | ||
} | ||
@@ -891,4 +822,5 @@ /** | ||
get props() { | ||
return this._el.props; | ||
return this[ContextInternalsSymbol].ret.el.props; | ||
} | ||
// TODO: Should we rename this??? | ||
/** | ||
@@ -898,18 +830,19 @@ * The current value of the associated element. | ||
* 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. | ||
* or the refresh, schedule, cleanup, or flush methods. This property is | ||
* mainly for plugins or utilities which wrap contexts. | ||
*/ | ||
get value() { | ||
return this._re.read(getValue(this._el)); | ||
return this[ContextInternalsSymbol].renderer.read(getValue(this[ContextInternalsSymbol].ret)); | ||
} | ||
*[Symbol.iterator]() { | ||
while (!(this._f & IsDone)) { | ||
if (this._f & IsIterating) { | ||
const internals = this[ContextInternalsSymbol]; | ||
while (!(internals.f & IsDone)) { | ||
if (internals.f & IsIterating) { | ||
throw new Error("Context iterated twice without a yield"); | ||
} | ||
else if (this._f & IsAsyncGen) { | ||
else if (internals.f & IsAsyncGen) { | ||
throw new Error("Use for await…of in async generator components"); | ||
} | ||
this._f |= IsIterating; | ||
yield this._el.props; | ||
internals.f |= IsIterating; | ||
yield internals.ret.el.props; | ||
} | ||
@@ -919,22 +852,24 @@ } | ||
// We use a do while loop rather than a while loop to handle an edge case | ||
// where an async generator component is unmounted synchronously. | ||
// where an async generator component is unmounted synchronously and | ||
// therefore “done” before it starts iterating over the context. | ||
const internals = this[ContextInternalsSymbol]; | ||
do { | ||
if (this._f & IsIterating) { | ||
if (internals.f & IsIterating) { | ||
throw new Error("Context iterated twice without a yield"); | ||
} | ||
else if (this._f & IsSyncGen) { | ||
else if (internals.f & IsSyncGen) { | ||
throw new Error("Use for…of in sync generator components"); | ||
} | ||
this._f |= IsIterating; | ||
if (this._f & IsAvailable) { | ||
this._f &= ~IsAvailable; | ||
internals.f |= IsIterating; | ||
if (internals.f & IsAvailable) { | ||
internals.f &= ~IsAvailable; | ||
} | ||
else { | ||
await new Promise((resolve) => (this._oa = resolve)); | ||
if (this._f & IsDone) { | ||
await new Promise((resolve) => (internals.onAvailable = resolve)); | ||
if (internals.f & IsDone) { | ||
break; | ||
} | ||
} | ||
yield this._el.props; | ||
} while (!(this._f & IsDone)); | ||
yield internals.ret.el.props; | ||
} while (!(internals.f & IsDone)); | ||
} | ||
@@ -954,12 +889,17 @@ /** | ||
refresh() { | ||
if (this._f & IsUnmounted) { | ||
const internals = this[ContextInternalsSymbol]; | ||
if (internals.f & IsUnmounted) { | ||
console.error("Component is unmounted"); | ||
return this._re.read(undefined); | ||
return internals.renderer.read(undefined); | ||
} | ||
else if (this._f & IsExecuting) { | ||
else if (internals.f & IsExecuting) { | ||
console.error("Component is already executing"); | ||
return this._re.read(undefined); | ||
return internals.renderer.read(undefined); | ||
} | ||
resumeCtx(this); | ||
return this._re.read(runCtx(this)); | ||
resumeCtxIterator(internals); | ||
const value = runComponent(internals); | ||
if (isPromiseLike(value)) { | ||
return value.then((value) => internals.renderer.read(value)); | ||
} | ||
return internals.renderer.read(value); | ||
} | ||
@@ -971,6 +911,7 @@ /** | ||
schedule(callback) { | ||
let callbacks = scheduleMap.get(this); | ||
const internals = this[ContextInternalsSymbol]; | ||
let callbacks = scheduleMap.get(internals); | ||
if (!callbacks) { | ||
callbacks = new Set(); | ||
scheduleMap.set(this, callbacks); | ||
scheduleMap.set(internals, callbacks); | ||
} | ||
@@ -980,2 +921,23 @@ callbacks.add(callback); | ||
/** | ||
* Registers a callback which fires when the component’s children are | ||
* rendered into the root. Will only fire once per callback and render. | ||
*/ | ||
flush(callback) { | ||
const internals = this[ContextInternalsSymbol]; | ||
if (typeof internals.root !== "object" || internals.root === null) { | ||
return; | ||
} | ||
let flushMap = flushMaps.get(internals.root); | ||
if (!flushMap) { | ||
flushMap = new Map(); | ||
flushMaps.set(internals.root, flushMap); | ||
} | ||
let callbacks = flushMap.get(internals); | ||
if (!callbacks) { | ||
callbacks = new Set(); | ||
flushMap.set(internals, callbacks); | ||
} | ||
callbacks.add(callback); | ||
} | ||
/** | ||
* Registers a callback which fires when the component unmounts. Will only | ||
@@ -985,6 +947,7 @@ * fire once per callback. | ||
cleanup(callback) { | ||
let callbacks = cleanupMap.get(this); | ||
const internals = this[ContextInternalsSymbol]; | ||
let callbacks = cleanupMap.get(internals); | ||
if (!callbacks) { | ||
callbacks = new Set(); | ||
cleanupMap.set(this, callbacks); | ||
cleanupMap.set(internals, callbacks); | ||
} | ||
@@ -994,3 +957,3 @@ callbacks.add(callback); | ||
consume(key) { | ||
for (let parent = this._pa; parent !== undefined; parent = parent._pa) { | ||
for (let parent = this[ContextInternalsSymbol].parent; parent !== undefined; parent = parent.parent) { | ||
const provisions = provisionMaps.get(parent); | ||
@@ -1003,6 +966,7 @@ if (provisions && provisions.has(key)) { | ||
provide(key, value) { | ||
let provisions = provisionMaps.get(this); | ||
const internals = this[ContextInternalsSymbol]; | ||
let provisions = provisionMaps.get(internals); | ||
if (!provisions) { | ||
provisions = new Map(); | ||
provisionMaps.set(this, provisions); | ||
provisionMaps.set(internals, provisions); | ||
} | ||
@@ -1012,161 +976,201 @@ provisions.set(key, value); | ||
addEventListener(type, listener, options) { | ||
let listeners; | ||
if (listener == null) { | ||
return; | ||
return addEventListener(this[ContextInternalsSymbol], type, listener, options); | ||
} | ||
removeEventListener(type, listener, options) { | ||
return removeEventListener(this[ContextInternalsSymbol], type, listener, options); | ||
} | ||
dispatchEvent(ev) { | ||
return dispatchEvent(this[ContextInternalsSymbol], ev); | ||
} | ||
} | ||
/*** PRIVATE CONTEXT FUNCTIONS ***/ | ||
function ctxContains(parent, child) { | ||
for (let current = child; current !== undefined; current = current.parent) { | ||
if (current === parent) { | ||
return true; | ||
} | ||
else { | ||
const listeners1 = listenersMap.get(this); | ||
if (listeners1) { | ||
listeners = listeners1; | ||
} | ||
return false; | ||
} | ||
function updateComponent(renderer, root, host, parent, scope, ret, oldProps) { | ||
let ctx; | ||
if (oldProps) { | ||
ctx = ret.ctx; | ||
} | ||
else { | ||
ctx = ret.ctx = new ContextInternals(renderer, root, host, parent, scope, ret); | ||
} | ||
ctx.f |= IsUpdating; | ||
resumeCtxIterator(ctx); | ||
return runComponent(ctx); | ||
} | ||
function updateComponentChildren(ctx, children) { | ||
if (ctx.f & IsUnmounted || ctx.f & IsErrored) { | ||
return; | ||
} | ||
else if (children === undefined) { | ||
console.error("A component has returned or yielded undefined. If this was intentional, return or yield null instead."); | ||
} | ||
const childValues = diffChildren(ctx.renderer, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children)); | ||
if (isPromiseLike(childValues)) { | ||
ctx.ret.inflight = childValues.then((childValues) => commitComponent(ctx, childValues)); | ||
return ctx.ret.inflight; | ||
} | ||
return commitComponent(ctx, childValues); | ||
} | ||
function commitComponent(ctx, values) { | ||
if (ctx.f & IsUnmounted) { | ||
return; | ||
} | ||
const listeners = listenersMap.get(ctx); | ||
if (listeners && listeners.length) { | ||
for (let i = 0; i < values.length; i++) { | ||
const value = values[i]; | ||
if (isEventTarget(value)) { | ||
for (let j = 0; j < listeners.length; j++) { | ||
const record = listeners[j]; | ||
value.addEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
else { | ||
listeners = []; | ||
listenersMap.set(this, listeners); | ||
} | ||
} | ||
options = normalizeOptions(options); | ||
let callback; | ||
if (typeof listener === "object") { | ||
callback = () => listener.handleEvent.apply(listener, arguments); | ||
} | ||
else { | ||
callback = listener; | ||
} | ||
const record = { type, callback, listener, options }; | ||
if (options.once) { | ||
record.callback = function () { | ||
const i = listeners.indexOf(record); | ||
if (i !== -1) { | ||
listeners.splice(i, 1); | ||
} | ||
const oldValues = wrap(ctx.ret.cached); | ||
let value = (ctx.ret.cached = unwrap(values)); | ||
if (ctx.f & IsScheduling) { | ||
ctx.f |= IsSchedulingRefresh; | ||
} | ||
else if (!(ctx.f & IsUpdating)) { | ||
// If we’re not updating the component, which happens when components are | ||
// refreshed, or when async generator components iterate, we have to do a | ||
// little bit housekeeping when a component’s child values have changed. | ||
if (!valuesEqual(oldValues, values)) { | ||
const records = getListenerRecords(ctx.parent, ctx.host); | ||
if (records.length) { | ||
for (let i = 0; i < values.length; i++) { | ||
const value = values[i]; | ||
if (isEventTarget(value)) { | ||
for (let j = 0; j < records.length; j++) { | ||
const record = records[j]; | ||
value.addEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
} | ||
return callback.apply(this, arguments); | ||
}; | ||
} | ||
if (listeners.some((record1) => record.type === record1.type && | ||
record.listener === record1.listener && | ||
!record.options.capture === !record1.options.capture)) { | ||
return; | ||
} | ||
listeners.push(record); | ||
for (const value of getChildValues(this._el)) { | ||
if (isEventTarget(value)) { | ||
value.addEventListener(record.type, record.callback, record.options); | ||
} | ||
// rearranging the nearest ancestor host element | ||
const host = ctx.host; | ||
const oldHostValues = wrap(host.cached); | ||
invalidate(ctx, host); | ||
const hostValues = getChildValues(host); | ||
ctx.renderer.arrange(host.el.tag, host.value, host.el.props, hostValues, | ||
// props and oldProps are the same because the host isn’t updated. | ||
host.el.props, oldHostValues); | ||
} | ||
flush(ctx.renderer, ctx.root, ctx); | ||
} | ||
removeEventListener(type, listener, options) { | ||
const listeners = listenersMap.get(this); | ||
if (listener == null || listeners == null) { | ||
return; | ||
const callbacks = scheduleMap.get(ctx); | ||
if (callbacks) { | ||
scheduleMap.delete(ctx); | ||
ctx.f |= IsScheduling; | ||
const value1 = ctx.renderer.read(value); | ||
for (const callback of callbacks) { | ||
callback(value1); | ||
} | ||
const options1 = normalizeOptions(options); | ||
const i = listeners.findIndex((record) => record.type === type && | ||
record.listener === listener && | ||
!record.options.capture === !options1.capture); | ||
if (i === -1) { | ||
return; | ||
ctx.f &= ~IsScheduling; | ||
// Handles an edge case where refresh() is called during a schedule(). | ||
if (ctx.f & IsSchedulingRefresh) { | ||
ctx.f &= ~IsSchedulingRefresh; | ||
value = getValue(ctx.ret); | ||
} | ||
const record = listeners[i]; | ||
listeners.splice(i, 1); | ||
for (const value of getChildValues(this._el)) { | ||
if (isEventTarget(value)) { | ||
value.removeEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
ctx.f &= ~IsUpdating; | ||
return value; | ||
} | ||
function invalidate(ctx, host) { | ||
for (let parent = ctx.parent; parent !== undefined && parent.host === host; parent = parent.parent) { | ||
parent.ret.cached = undefined; | ||
} | ||
host.cached = undefined; | ||
} | ||
function valuesEqual(values1, values2) { | ||
if (values1.length !== values2.length) { | ||
return false; | ||
} | ||
for (let i = 0; i < values1.length; i++) { | ||
const value1 = values1[i]; | ||
const value2 = values2[i]; | ||
if (value1 !== value2) { | ||
return false; | ||
} | ||
} | ||
dispatchEvent(ev) { | ||
const path = []; | ||
for (let parent = this._pa; parent !== undefined; parent = parent._pa) { | ||
path.push(parent); | ||
} | ||
// We patch the stopImmediatePropagation method because ev.cancelBubble | ||
// only informs us if stopPropagation was called and there are no | ||
// properties which inform us if stopImmediatePropagation was called. | ||
let immediateCancelBubble = false; | ||
const stopImmediatePropagation = ev.stopImmediatePropagation; | ||
setEventProperty(ev, "stopImmediatePropagation", () => { | ||
immediateCancelBubble = true; | ||
return stopImmediatePropagation.call(ev); | ||
}); | ||
setEventProperty(ev, "target", this); | ||
// The only possible errors in this block are errors thrown by callbacks, | ||
// and dispatchEvent will only log these errors rather than throwing | ||
// them. Therefore, we place all code in a try block, log errors in the | ||
// catch block, and use an unsafe return statement in the finally block. | ||
// | ||
// Each early return within the try block returns true because while the | ||
// return value is overridden in the finally block, TypeScript | ||
// (justifiably) does not recognize the unsafe return statement. | ||
return true; | ||
} | ||
/** | ||
* Enqueues and executes the component associated with the context. | ||
* | ||
* The functions stepComponent and runComponent work together | ||
* to implement the async queueing behavior of components. The runComponent | ||
* function calls the stepComponent 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 runComponent function caches block/value from the | ||
* stepComponent function on the context, according to whether the component | ||
* blocks. The “inflight” block/value properties are the currently executing | ||
* update, and the “enqueued” block/value properties represent an enqueued next | ||
* stepComponent. Enqueued steps are dequeued every time the current block | ||
* promise settles. | ||
*/ | ||
function runComponent(ctx) { | ||
if (!ctx.inflightBlock) { | ||
try { | ||
setEventProperty(ev, "eventPhase", CAPTURING_PHASE); | ||
for (let i = path.length - 1; i >= 0; i--) { | ||
const target = path[i]; | ||
const listeners = listenersMap.get(target); | ||
if (listeners) { | ||
setEventProperty(ev, "currentTarget", target); | ||
for (const record of listeners) { | ||
if (record.type === ev.type && record.options.capture) { | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
} | ||
const [block, value] = stepComponent(ctx); | ||
if (block) { | ||
ctx.inflightBlock = block | ||
.catch((err) => { | ||
if (!(ctx.f & IsUpdating)) { | ||
return propagateError(ctx.parent, err); | ||
} | ||
} | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
}) | ||
.finally(() => advanceComponent(ctx)); | ||
// stepComponent will only return a block if the value is asynchronous | ||
ctx.inflightValue = value; | ||
} | ||
{ | ||
const listeners = listenersMap.get(this); | ||
if (listeners) { | ||
setEventProperty(ev, "eventPhase", AT_TARGET); | ||
setEventProperty(ev, "currentTarget", this); | ||
for (const record of listeners) { | ||
if (record.type === ev.type) { | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
return value; | ||
} | ||
catch (err) { | ||
if (!(ctx.f & IsUpdating)) { | ||
return propagateError(ctx.parent, err); | ||
} | ||
throw err; | ||
} | ||
} | ||
else if (ctx.f & IsAsyncGen) { | ||
return ctx.inflightValue; | ||
} | ||
else if (!ctx.enqueuedBlock) { | ||
let resolve; | ||
ctx.enqueuedBlock = ctx.inflightBlock | ||
.then(() => { | ||
try { | ||
const [block, value] = stepComponent(ctx); | ||
resolve(value); | ||
if (block) { | ||
return block.catch((err) => { | ||
if (!(ctx.f & IsUpdating)) { | ||
return propagateError(ctx.parent, err); | ||
} | ||
} | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
}); | ||
} | ||
} | ||
if (ev.bubbles) { | ||
setEventProperty(ev, "eventPhase", BUBBLING_PHASE); | ||
for (let i = 0; i < path.length; i++) { | ||
const target = path[i]; | ||
const listeners = listenersMap.get(target); | ||
if (listeners) { | ||
setEventProperty(ev, "currentTarget", target); | ||
for (const record of listeners) { | ||
if (record.type === ev.type && !record.options.capture) { | ||
record.callback.call(this, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
catch (err) { | ||
if (!(ctx.f & IsUpdating)) { | ||
return propagateError(ctx.parent, err); | ||
} | ||
} | ||
} | ||
catch (err) { | ||
console.error(err); | ||
} | ||
finally { | ||
setEventProperty(ev, "eventPhase", NONE); | ||
setEventProperty(ev, "currentTarget", null); | ||
// eslint-disable-next-line no-unsafe-finally | ||
return !ev.defaultPrevented; | ||
} | ||
}) | ||
.finally(() => advanceComponent(ctx)); | ||
ctx.enqueuedValue = new Promise((resolve1) => (resolve = resolve1)); | ||
} | ||
return ctx.enqueuedValue; | ||
} | ||
/*** PRIVATE CONTEXT FUNCTIONS ***/ | ||
/** | ||
@@ -1189,35 +1193,39 @@ * This function is responsible for executing the component and handling all | ||
*/ | ||
function stepCtx(ctx) { | ||
const el = ctx._el; | ||
if (ctx._f & IsDone) { | ||
return [undefined, getValue(el)]; | ||
function stepComponent(ctx) { | ||
const ret = ctx.ret; | ||
if (ctx.f & IsDone) { | ||
return [undefined, getValue(ret)]; | ||
} | ||
const initial = !ctx._it; | ||
const initial = !ctx.iterator; | ||
if (initial) { | ||
ctx.f |= IsExecuting; | ||
clearEventListeners(ctx); | ||
let result; | ||
try { | ||
ctx._f |= IsExecuting; | ||
clearEventListeners(ctx); | ||
const result = el.tag.call(ctx, el.props); | ||
if (isIteratorLike(result)) { | ||
ctx._it = result; | ||
} | ||
else if (isPromiseLike(result)) { | ||
// async function component | ||
const result1 = result instanceof Promise ? result : Promise.resolve(result); | ||
const value = result1.then((result) => updateCtxChildren(ctx, result)); | ||
return [result1, value]; | ||
} | ||
else { | ||
// sync function component | ||
return [undefined, updateCtxChildren(ctx, result)]; | ||
} | ||
result = ret.el.tag.call(ctx.facade, ret.el.props); | ||
} | ||
catch (err) { | ||
ctx.f |= IsErrored; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~IsExecuting; | ||
ctx.f &= ~IsExecuting; | ||
} | ||
if (isIteratorLike(result)) { | ||
ctx.iterator = result; | ||
} | ||
else if (isPromiseLike(result)) { | ||
// async function component | ||
const result1 = result instanceof Promise ? result : Promise.resolve(result); | ||
const value = result1.then((result) => updateComponentChildren(ctx, result), (err) => { | ||
ctx.f |= IsErrored; | ||
throw err; | ||
}); | ||
return [result1, value]; | ||
} | ||
else { | ||
// sync function component | ||
return [undefined, updateComponentChildren(ctx, result)]; | ||
} | ||
} | ||
// The value passed back into the generator as the argument to the next | ||
// method is a promise if an async generator component has async children. | ||
// Sync generator components only resume when their children have fulfilled | ||
// so ctx._el._ic (the element’s inflight children) will never be defined. | ||
let oldValue; | ||
@@ -1228,19 +1236,23 @@ if (initial) { | ||
} | ||
else if (ctx._el._ic) { | ||
oldValue = ctx._el._ic.then(ctx._re.read, () => ctx._re.read(undefined)); | ||
else if (ctx.ret.inflight) { | ||
// The value passed back into the generator as the argument to the next | ||
// method is a promise if an async generator component has async children. | ||
// Sync generator components only resume when their children have fulfilled | ||
// so the element’s inflight child values will never be defined. | ||
oldValue = ctx.ret.inflight.then((value) => ctx.renderer.read(value), () => ctx.renderer.read(undefined)); | ||
} | ||
else { | ||
oldValue = ctx._re.read(getValue(el)); | ||
oldValue = ctx.renderer.read(getValue(ret)); | ||
} | ||
let iteration; | ||
ctx.f |= IsExecuting; | ||
try { | ||
ctx._f |= IsExecuting; | ||
iteration = ctx._it.next(oldValue); | ||
iteration = ctx.iterator.next(oldValue); | ||
} | ||
catch (err) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone | IsErrored; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~IsExecuting; | ||
ctx.f &= ~IsExecuting; | ||
} | ||
@@ -1250,14 +1262,14 @@ if (isPromiseLike(iteration)) { | ||
if (initial) { | ||
ctx._f |= IsAsyncGen; | ||
ctx.f |= IsAsyncGen; | ||
} | ||
const value = iteration.then((iteration) => { | ||
if (!(ctx._f & IsIterating)) { | ||
ctx._f &= ~IsAvailable; | ||
if (!(ctx.f & IsIterating)) { | ||
ctx.f &= ~IsAvailable; | ||
} | ||
ctx._f &= ~IsIterating; | ||
ctx.f &= ~IsIterating; | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone; | ||
} | ||
try { | ||
const value = updateCtxChildren(ctx, iteration.value); | ||
const value = updateComponentChildren(ctx, iteration.value); | ||
if (isPromiseLike(value)) { | ||
@@ -1272,3 +1284,3 @@ return value.catch((err) => handleChildError(ctx, err)); | ||
}, (err) => { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone | IsErrored; | ||
throw err; | ||
@@ -1280,11 +1292,11 @@ }); | ||
if (initial) { | ||
ctx._f |= IsSyncGen; | ||
ctx.f |= IsSyncGen; | ||
} | ||
ctx._f &= ~IsIterating; | ||
ctx.f &= ~IsIterating; | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone; | ||
} | ||
let value; | ||
try { | ||
value = updateCtxChildren(ctx, iteration.value); | ||
value = updateComponentChildren(ctx, iteration.value); | ||
if (isPromiseLike(value)) { | ||
@@ -1305,221 +1317,225 @@ value = value.catch((err) => handleChildError(ctx, err)); | ||
*/ | ||
function advanceCtx(ctx) { | ||
// _ib - inflightBlock | ||
// _iv - inflightValue | ||
// _eb - enqueuedBlock | ||
// _ev - enqueuedValue | ||
ctx._ib = ctx._eb; | ||
ctx._iv = ctx._ev; | ||
ctx._eb = undefined; | ||
ctx._ev = undefined; | ||
if (ctx._f & IsAsyncGen && !(ctx._f & IsDone)) { | ||
runCtx(ctx); | ||
function advanceComponent(ctx) { | ||
ctx.inflightBlock = ctx.enqueuedBlock; | ||
ctx.inflightValue = ctx.enqueuedValue; | ||
ctx.enqueuedBlock = undefined; | ||
ctx.enqueuedValue = undefined; | ||
if (ctx.f & IsAsyncGen && !(ctx.f & IsDone) && !(ctx.f & IsUnmounted)) { | ||
runComponent(ctx); | ||
} | ||
} | ||
/** | ||
* Enqueues and executes the component associated with the context. | ||
* | ||
* 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 blocks. 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 every time | ||
* the current block promise settles. | ||
*/ | ||
function runCtx(ctx) { | ||
if (!ctx._ib) { | ||
try { | ||
const [block, value] = stepCtx(ctx); | ||
if (block) { | ||
ctx._ib = block | ||
.catch((err) => { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}) | ||
.finally(() => advanceCtx(ctx)); | ||
// stepCtx will only return a block if the value is asynchronous | ||
ctx._iv = value; | ||
} | ||
return value; | ||
} | ||
catch (err) { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
throw err; | ||
} | ||
} | ||
else if (ctx._f & IsAsyncGen) { | ||
return ctx._iv; | ||
} | ||
else if (!ctx._eb) { | ||
let resolve; | ||
ctx._eb = ctx._ib | ||
.then(() => { | ||
try { | ||
const [block, value] = stepCtx(ctx); | ||
resolve(value); | ||
if (block) { | ||
return block.catch((err) => { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
}); | ||
} | ||
} | ||
catch (err) { | ||
if (!(ctx._f & IsUpdating)) { | ||
return propagateError(ctx._pa, err); | ||
} | ||
} | ||
}) | ||
.finally(() => advanceCtx(ctx)); | ||
ctx._ev = new Promise((resolve1) => (resolve = resolve1)); | ||
} | ||
return ctx._ev; | ||
} | ||
/** | ||
* Called to make props available to the props async iterator for async | ||
* generator components. | ||
*/ | ||
function resumeCtx(ctx) { | ||
if (ctx._oa) { | ||
ctx._oa(); | ||
ctx._oa = undefined; | ||
function resumeCtxIterator(ctx) { | ||
if (ctx.onAvailable) { | ||
ctx.onAvailable(); | ||
ctx.onAvailable = undefined; | ||
} | ||
else { | ||
ctx._f |= IsAvailable; | ||
ctx.f |= IsAvailable; | ||
} | ||
} | ||
function updateCtx(ctx) { | ||
ctx._f |= IsUpdating; | ||
resumeCtx(ctx); | ||
return runCtx(ctx); | ||
} | ||
function updateCtxChildren(ctx, children) { | ||
return updateChildren(ctx._re, ctx._rt, ctx._ho, ctx, ctx._sc, ctx._el, narrow(children)); | ||
} | ||
function commitCtx(ctx, values) { | ||
if (ctx._f & IsUnmounted) { | ||
return; | ||
// TODO: async unmounting | ||
function unmountComponent(ctx) { | ||
ctx.f |= IsUnmounted; | ||
clearEventListeners(ctx); | ||
const callbacks = cleanupMap.get(ctx); | ||
if (callbacks) { | ||
cleanupMap.delete(ctx); | ||
const value = ctx.renderer.read(getValue(ctx.ret)); | ||
for (const callback of callbacks) { | ||
callback(value); | ||
} | ||
} | ||
const listeners = listenersMap.get(ctx); | ||
if (listeners && listeners.length) { | ||
for (let i = 0; i < values.length; i++) { | ||
const value = values[i]; | ||
if (isEventTarget(value)) { | ||
for (let j = 0; j < listeners.length; j++) { | ||
const record = listeners[j]; | ||
value.addEventListener(record.type, record.callback, record.options); | ||
if (!(ctx.f & IsDone)) { | ||
ctx.f |= IsDone; | ||
resumeCtxIterator(ctx); | ||
if (ctx.iterator && typeof ctx.iterator.return === "function") { | ||
ctx.f |= IsExecuting; | ||
try { | ||
const iteration = ctx.iterator.return(); | ||
if (isPromiseLike(iteration)) { | ||
iteration.catch((err) => propagateError(ctx.parent, err)); | ||
} | ||
} | ||
finally { | ||
ctx.f &= ~IsExecuting; | ||
} | ||
} | ||
} | ||
if (ctx._f & IsScheduling) { | ||
ctx._f |= IsSchedulingRefresh; | ||
} | ||
/*** 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; | ||
const listenersMap = new WeakMap(); | ||
function addEventListener(ctx, type, listener, options) { | ||
let listeners; | ||
if (listener == null) { | ||
return; | ||
} | ||
else if (!(ctx._f & IsUpdating)) { | ||
// Rearrange the host. | ||
const listeners = getListeners(ctx._pa, ctx._ho); | ||
if (listeners.length) { | ||
for (let i = 0; i < values.length; i++) { | ||
const value = values[i]; | ||
if (isEventTarget(value)) { | ||
for (let j = 0; j < listeners.length; j++) { | ||
const record = listeners[j]; | ||
value.addEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
} | ||
else { | ||
const listeners1 = listenersMap.get(ctx); | ||
if (listeners1) { | ||
listeners = listeners1; | ||
} | ||
const host = ctx._ho; | ||
const hostValues = getChildValues(host); | ||
ctx._re.arrange(host, host.tag === Portal ? host.props.root : host._n, hostValues); | ||
if (hostValues.length) { | ||
host._f |= HadChildren; | ||
} | ||
else { | ||
host._f &= ~HadChildren; | ||
listeners = []; | ||
listenersMap.set(ctx, listeners); | ||
} | ||
ctx._re.complete(ctx._rt); | ||
} | ||
let value = unwrap(values); | ||
const callbacks = scheduleMap.get(ctx); | ||
if (callbacks && callbacks.size) { | ||
const callbacks1 = Array.from(callbacks); | ||
// We must clear the set of callbacks before calling them, because a | ||
// callback which refreshes the component would otherwise cause a stack | ||
// overflow. | ||
callbacks.clear(); | ||
const value1 = ctx._re.read(value); | ||
ctx._f |= IsScheduling; | ||
for (const callback of callbacks1) { | ||
try { | ||
callback(value1); | ||
options = normalizeListenerOptions(options); | ||
let callback; | ||
if (typeof listener === "object") { | ||
callback = () => listener.handleEvent.apply(listener, arguments); | ||
} | ||
else { | ||
callback = listener; | ||
} | ||
const record = { type, callback, listener, options }; | ||
if (options.once) { | ||
record.callback = function () { | ||
const i = listeners.indexOf(record); | ||
if (i !== -1) { | ||
listeners.splice(i, 1); | ||
} | ||
catch (err) { | ||
// TODO: handle schedule callback errors in a better way. | ||
console.error(err); | ||
} | ||
return callback.apply(this, arguments); | ||
}; | ||
} | ||
if (listeners.some((record1) => record.type === record1.type && | ||
record.listener === record1.listener && | ||
!record.options.capture === !record1.options.capture)) { | ||
return; | ||
} | ||
listeners.push(record); | ||
// TODO: is it possible to separate out the EventTarget delegation logic | ||
for (const value of getChildValues(ctx.ret)) { | ||
if (isEventTarget(value)) { | ||
value.addEventListener(record.type, record.callback, record.options); | ||
} | ||
ctx._f &= ~IsScheduling; | ||
if (ctx._f & IsSchedulingRefresh) { | ||
ctx._f &= ~IsSchedulingRefresh; | ||
value = getValue(ctx._el); | ||
} | ||
} | ||
function removeEventListener(ctx, type, listener, options) { | ||
const listeners = listenersMap.get(ctx); | ||
if (listener == null || listeners == null) { | ||
return; | ||
} | ||
const options1 = normalizeListenerOptions(options); | ||
const i = listeners.findIndex((record) => record.type === type && | ||
record.listener === listener && | ||
!record.options.capture === !options1.capture); | ||
if (i === -1) { | ||
return; | ||
} | ||
const record = listeners[i]; | ||
listeners.splice(i, 1); | ||
// TODO: is it possible to separate out the EventTarget delegation logic | ||
for (const value of getChildValues(ctx.ret)) { | ||
if (isEventTarget(value)) { | ||
value.removeEventListener(record.type, record.callback, record.options); | ||
} | ||
} | ||
ctx._f &= ~IsUpdating; | ||
return value; | ||
} | ||
// TODO: async unmounting | ||
function unmountCtx(ctx) { | ||
ctx._f |= IsUnmounted; | ||
clearEventListeners(ctx); | ||
const callbacks = cleanupMap.get(ctx); | ||
if (callbacks && callbacks.size) { | ||
const callbacks1 = Array.from(callbacks); | ||
callbacks.clear(); | ||
const value = ctx._re.read(getValue(ctx._el)); | ||
for (const callback of callbacks1) { | ||
try { | ||
callback(value); | ||
function dispatchEvent(ctx, ev) { | ||
const path = []; | ||
for (let parent = ctx.parent; parent !== undefined; parent = parent.parent) { | ||
path.push(parent); | ||
} | ||
// We patch the stopImmediatePropagation method because ev.cancelBubble | ||
// only informs us if stopPropagation was called and there are no | ||
// properties which inform us if stopImmediatePropagation was called. | ||
let immediateCancelBubble = false; | ||
const stopImmediatePropagation = ev.stopImmediatePropagation; | ||
setEventProperty(ev, "stopImmediatePropagation", () => { | ||
immediateCancelBubble = true; | ||
return stopImmediatePropagation.call(ev); | ||
}); | ||
setEventProperty(ev, "target", ctx.facade); | ||
// The only possible errors in this block are errors thrown by callbacks, | ||
// and dispatchEvent will only log these errors rather than throwing | ||
// them. Therefore, we place all code in a try block, log errors in the | ||
// catch block, and use an unsafe return statement in the finally block. | ||
// | ||
// Each early return within the try block returns true because while the | ||
// return value is overridden in the finally block, TypeScript | ||
// (justifiably) does not recognize the unsafe return statement. | ||
// | ||
// TODO: Run all callbacks even if one of them errors | ||
try { | ||
setEventProperty(ev, "eventPhase", CAPTURING_PHASE); | ||
for (let i = path.length - 1; i >= 0; i--) { | ||
const target = path[i]; | ||
const listeners = listenersMap.get(target); | ||
if (listeners) { | ||
setEventProperty(ev, "currentTarget", target.facade); | ||
for (const record of listeners) { | ||
if (record.type === ev.type && record.options.capture) { | ||
record.callback.call(target.facade, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
catch (err) { | ||
// TODO: handle cleanup callback errors in a better way. | ||
console.error(err); | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
if (!(ctx._f & IsDone)) { | ||
ctx._f |= IsDone; | ||
resumeCtx(ctx); | ||
if (ctx._it && typeof ctx._it.return === "function") { | ||
try { | ||
ctx._f |= IsExecuting; | ||
const iteration = ctx._it.return(); | ||
if (isPromiseLike(iteration)) { | ||
iteration.catch((err) => propagateError(ctx._pa, err)); | ||
{ | ||
const listeners = listenersMap.get(ctx); | ||
if (listeners) { | ||
setEventProperty(ev, "eventPhase", AT_TARGET); | ||
setEventProperty(ev, "currentTarget", ctx.facade); | ||
for (const record of listeners) { | ||
if (record.type === ev.type) { | ||
record.callback.call(ctx.facade, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
} | ||
finally { | ||
ctx._f &= ~IsExecuting; | ||
} | ||
if (ev.bubbles) { | ||
setEventProperty(ev, "eventPhase", BUBBLING_PHASE); | ||
for (let i = 0; i < path.length; i++) { | ||
const target = path[i]; | ||
const listeners = listenersMap.get(target); | ||
if (listeners) { | ||
setEventProperty(ev, "currentTarget", target.facade); | ||
for (const record of listeners) { | ||
if (record.type === ev.type && !record.options.capture) { | ||
record.callback.call(target.facade, ev); | ||
if (immediateCancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
if (ev.cancelBubble) { | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
catch (err) { | ||
// TODO: Use setTimeout to rethrow the error. | ||
console.error(err); | ||
} | ||
finally { | ||
setEventProperty(ev, "eventPhase", NONE); | ||
setEventProperty(ev, "currentTarget", null); | ||
// eslint-disable-next-line no-unsafe-finally | ||
return !ev.defaultPrevented; | ||
} | ||
} | ||
/*** 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; | ||
const listenersMap = new WeakMap(); | ||
function normalizeOptions(options) { | ||
function normalizeListenerOptions(options) { | ||
if (typeof options === "boolean") { | ||
@@ -1542,2 +1558,4 @@ return { capture: options }; | ||
} | ||
// TODO: Maybe we can pass in the current context directly, rather than | ||
// starting from the parent? | ||
/** | ||
@@ -1551,9 +1569,6 @@ * A function to reconstruct an array of every listener given a context and a | ||
* element passed in matches the parent context’s host element. | ||
* | ||
* TODO: Maybe we can pass in the current context directly, rather than | ||
* starting from the parent? | ||
*/ | ||
function getListeners(ctx, host) { | ||
function getListenerRecords(ctx, ret) { | ||
let listeners = []; | ||
while (ctx !== undefined && ctx._ho === host) { | ||
while (ctx !== undefined && ctx.host === ret) { | ||
const listeners1 = listenersMap.get(ctx); | ||
@@ -1563,3 +1578,3 @@ if (listeners1) { | ||
} | ||
ctx = ctx._pa; | ||
ctx = ctx.parent; | ||
} | ||
@@ -1571,3 +1586,3 @@ return listeners; | ||
if (listeners && listeners.length) { | ||
for (const value of getChildValues(ctx._el)) { | ||
for (const value of getChildValues(ctx.ret)) { | ||
if (isEventTarget(value)) { | ||
@@ -1585,17 +1600,19 @@ for (const record of listeners) { | ||
function handleChildError(ctx, err) { | ||
if (ctx._f & IsDone || !ctx._it || typeof ctx._it.throw !== "function") { | ||
if (ctx.f & IsDone || | ||
!ctx.iterator || | ||
typeof ctx.iterator.throw !== "function") { | ||
throw err; | ||
} | ||
resumeCtx(ctx); | ||
resumeCtxIterator(ctx); | ||
let iteration; | ||
try { | ||
ctx._f |= IsExecuting; | ||
iteration = ctx._it.throw(err); | ||
ctx.f |= IsExecuting; | ||
iteration = ctx.iterator.throw(err); | ||
} | ||
catch (err) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone | IsErrored; | ||
throw err; | ||
} | ||
finally { | ||
ctx._f &= ~IsExecuting; | ||
ctx.f &= ~IsExecuting; | ||
} | ||
@@ -1605,7 +1622,7 @@ if (isPromiseLike(iteration)) { | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone; | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
return updateComponentChildren(ctx, iteration.value); | ||
}, (err) => { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone | IsErrored; | ||
throw err; | ||
@@ -1615,5 +1632,5 @@ }); | ||
if (iteration.done) { | ||
ctx._f |= IsDone; | ||
ctx.f |= IsDone; | ||
} | ||
return updateCtxChildren(ctx, iteration.value); | ||
return updateComponentChildren(ctx, iteration.value); | ||
} | ||
@@ -1629,6 +1646,6 @@ function propagateError(ctx, err) { | ||
catch (err) { | ||
return propagateError(ctx._pa, err); | ||
return propagateError(ctx.parent, err); | ||
} | ||
if (isPromiseLike(result)) { | ||
return result.catch((err) => propagateError(ctx._pa, err)); | ||
return result.catch((err) => propagateError(ctx.parent, err)); | ||
} | ||
@@ -1638,3 +1655,3 @@ return result; | ||
export { Context, Copy, Element, Fragment, Portal, Raw, Renderer, cloneElement, createElement, isElement }; | ||
export { Context, ContextInternalsSymbol, Copy, Element, Fragment, Portal, Raw, Renderer, cloneElement, createElement, isElement }; | ||
//# sourceMappingURL=crank.js.map |
10
dom.d.ts
@@ -1,9 +0,5 @@ | ||
import { Children, Context, Element as CrankElement, ElementValue, Renderer } from "./crank"; | ||
export declare class DOMRenderer extends Renderer<Node, string | undefined> { | ||
import { Children, Context, ElementValue, Renderer } from "./crank"; | ||
export declare class DOMRenderer extends Renderer<Node, string> { | ||
constructor(); | ||
render(children: Children, root: Node, ctx?: Context): Promise<ElementValue<Node>> | ElementValue<Node>; | ||
parse(text: string): DocumentFragment; | ||
scope(el: CrankElement<string | symbol>, scope: string | undefined): string | undefined; | ||
create(el: CrankElement<string | symbol>, ns: string | undefined): Node; | ||
patch(el: CrankElement<string | symbol>, node: Element): void; | ||
arrange(el: CrankElement<string | symbol>, node: Node, children: Array<Node | string>): void; | ||
} | ||
@@ -10,0 +6,0 @@ export declare const renderer: DOMRenderer; |
204
dom.js
@@ -5,9 +5,3 @@ /// <reference types="./dom.d.ts" /> | ||
const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; | ||
class DOMRenderer extends Renderer { | ||
render(children, root, ctx) { | ||
if (root == null || typeof root.nodeType !== "number") { | ||
throw new TypeError(`Render root is not a node. Received: ${JSON.stringify(root && root.toString())}`); | ||
} | ||
return super.render(children, root, ctx); | ||
} | ||
const impl = { | ||
parse(text) { | ||
@@ -26,5 +20,6 @@ if (typeof document.createRange === "function") { | ||
} | ||
} | ||
scope(el, scope) { | ||
switch (el.tag) { | ||
}, | ||
scope(scope, tag) { | ||
// TODO: Should we handle xmlns??? | ||
switch (tag) { | ||
case Portal: | ||
@@ -38,107 +33,111 @@ case "foreignObject": | ||
} | ||
} | ||
create(el, ns) { | ||
if (typeof el.tag !== "string") { | ||
throw new Error(`Unknown tag: ${el.tag.toString()}`); | ||
}, | ||
create(tag, _props, ns) { | ||
if (typeof tag !== "string") { | ||
throw new Error(`Unknown tag: ${tag.toString()}`); | ||
} | ||
else if (el.tag === "svg") { | ||
else if (tag.toLowerCase() === "svg") { | ||
ns = SVG_NAMESPACE; | ||
} | ||
return ns | ||
? document.createElementNS(ns, el.tag) | ||
: document.createElement(el.tag); | ||
} | ||
patch(el, node) { | ||
const isSVG = node.namespaceURI === SVG_NAMESPACE; | ||
for (let name in el.props) { | ||
let forceAttribute = false; | ||
const value = el.props[name]; | ||
switch (name) { | ||
case "children": | ||
break; | ||
case "style": { | ||
const style = node.style; | ||
if (style == null) { | ||
node.setAttribute("style", value); | ||
return ns ? document.createElementNS(ns, tag) : document.createElement(tag); | ||
}, | ||
patch(_tag, | ||
// TODO: Why does this assignment work? | ||
node, name, | ||
// TODO: Stricter typings? | ||
value, oldValue, scope) { | ||
const isSVG = scope === SVG_NAMESPACE; | ||
switch (name) { | ||
case "style": { | ||
const style = node.style; | ||
if (style == null) { | ||
node.setAttribute("style", value); | ||
} | ||
else if (value == null || value === false) { | ||
node.removeAttribute("style"); | ||
} | ||
else if (value === true) { | ||
node.setAttribute("style", ""); | ||
} | ||
else if (typeof value === "string") { | ||
if (style.cssText !== value) { | ||
style.cssText = value; | ||
} | ||
else { | ||
if (value == null) { | ||
node.removeAttribute("style"); | ||
} | ||
else { | ||
if (typeof oldValue === "string") { | ||
style.cssText = ""; | ||
} | ||
for (const styleName in { ...oldValue, ...value }) { | ||
const styleValue = value && value[styleName]; | ||
if (styleValue == null) { | ||
style.removeProperty(styleName); | ||
} | ||
else if (typeof value === "string") { | ||
if (style.cssText !== value) { | ||
style.cssText = value; | ||
} | ||
else if (style.getPropertyValue(styleName) !== styleValue) { | ||
style.setProperty(styleName, styleValue); | ||
} | ||
else { | ||
for (const styleName in value) { | ||
const styleValue = value && value[styleName]; | ||
if (styleValue == null) { | ||
style.removeProperty(styleName); | ||
} | ||
else if (style.getPropertyValue(styleName) !== styleValue) { | ||
style.setProperty(styleName, styleValue); | ||
} | ||
} | ||
} | ||
} | ||
break; | ||
} | ||
case "class": | ||
case "className": | ||
if (value === true) { | ||
node.setAttribute("class", ""); | ||
break; | ||
} | ||
case "class": | ||
case "className": | ||
if (value === true) { | ||
node.setAttribute("class", ""); | ||
} | ||
else if (value == null) { | ||
node.removeAttribute("class"); | ||
} | ||
else if (!isSVG) { | ||
if (node.className !== value) { | ||
node["className"] = value; | ||
} | ||
else if (!value) { | ||
node.removeAttribute("class"); | ||
} | ||
else if (!isSVG) { | ||
if (node.className !== value) { | ||
node["className"] = value; | ||
} | ||
} | ||
else if (node.getAttribute("class") !== value) { | ||
node.setAttribute("class", value); | ||
} | ||
break; | ||
// Gleaned from: | ||
// https://github.com/preactjs/preact/blob/05e5d2c0d2d92c5478eeffdbd96681c96500d29f/src/diff/props.js#L111-L117 | ||
// TODO: figure out why we use setAttribute for each of these | ||
case "form": | ||
case "list": | ||
case "type": | ||
case "size": | ||
forceAttribute = true; | ||
// fallthrough | ||
default: { | ||
if (value == null) { | ||
node.removeAttribute(name); | ||
} | ||
else if (typeof value === "function" || | ||
typeof value === "object" || | ||
(!forceAttribute && !isSVG && name in node)) { | ||
} | ||
else if (node.getAttribute("class") !== value) { | ||
node.setAttribute("class", value); | ||
} | ||
break; | ||
default: { | ||
if (name in node && | ||
// boolean properties will coerce strings, but sometimes they map to | ||
// enumerated attributes, where truthy strings ("false", "no") map to | ||
// falsy properties, so we use attributes in this case. | ||
!(typeof value === "string" && | ||
typeof node[name] === "boolean")) { | ||
try { | ||
if (node[name] !== value) { | ||
node[name] = value; | ||
} | ||
return; | ||
} | ||
else if (value === true) { | ||
node.setAttribute(name, ""); | ||
catch (err) { | ||
// some properties are readonly so we fallback to setting them as | ||
// attributes | ||
} | ||
else if (value === false) { | ||
node.removeAttribute(name); | ||
} | ||
else if (node.getAttribute(name) !== value) { | ||
node.setAttribute(name, value); | ||
} | ||
} | ||
if (value === true) { | ||
value = ""; | ||
} | ||
else if (value == null || value === false) { | ||
node.removeAttribute(name); | ||
return; | ||
} | ||
if (node.getAttribute(name) !== value) { | ||
node.setAttribute(name, value); | ||
} | ||
} | ||
} | ||
} | ||
arrange(el, node, children) { | ||
if (el.tag === Portal && | ||
(node == null || typeof node.nodeType !== "number")) { | ||
}, | ||
arrange(tag, node, props, children, _oldProps, oldChildren) { | ||
if (tag === Portal && (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" in el.props || el.hadChildren)) { | ||
if (!("innerHTML" in props) && | ||
// We don’t want to update elements without explicit children (<div/>), | ||
// because these elements sometimes have child nodes added via raw | ||
// DOM manipulations. | ||
// However, if an element has previously rendered children, we clear the | ||
// them because it would be surprising not to clear Crank managed | ||
// children, even if the new element does not have explicit children. | ||
("children" in props || (oldChildren && oldChildren.length))) { | ||
if (children.length === 0) { | ||
@@ -184,2 +183,3 @@ node.textContent = ""; | ||
} | ||
// remove excess DOM nodes | ||
while (oldChild !== null) { | ||
@@ -190,2 +190,3 @@ const nextSibling = oldChild.nextSibling; | ||
} | ||
// append excess children | ||
for (; i < children.length; i++) { | ||
@@ -199,3 +200,14 @@ const newChild = children[i]; | ||
} | ||
}, | ||
}; | ||
class DOMRenderer extends Renderer { | ||
constructor() { | ||
super(impl); | ||
} | ||
render(children, root, ctx) { | ||
if (root == null || typeof root.nodeType !== "number") { | ||
throw new TypeError(`Render root is not a node. Received: ${JSON.stringify(root && root.toString())}`); | ||
} | ||
return super.render(children, root, ctx); | ||
} | ||
} | ||
@@ -202,0 +214,0 @@ const renderer = new DOMRenderer(); |
@@ -1,10 +0,7 @@ | ||
import { Element, ElementValue, Renderer } from "./crank"; | ||
import { Renderer } from "./crank"; | ||
interface Node { | ||
value: string; | ||
} | ||
export declare class HTMLRenderer extends Renderer<Node | string, undefined, unknown, string> { | ||
create(): Node; | ||
escape(text: string): string; | ||
read(value: ElementValue<Node>): string; | ||
arrange(el: Element<string | symbol>, node: Node, children: Array<Node | string>): void; | ||
export declare class HTMLRenderer extends Renderer<Node, undefined, any, string> { | ||
constructor(); | ||
} | ||
@@ -11,0 +8,0 @@ export declare const renderer: HTMLRenderer; |
31
html.js
@@ -85,9 +85,9 @@ /// <reference types="./html.d.ts" /> | ||
} | ||
class HTMLRenderer extends Renderer { | ||
const impl = { | ||
create() { | ||
return { value: "" }; | ||
} | ||
}, | ||
escape(text) { | ||
return escape(text); | ||
} | ||
}, | ||
read(value) { | ||
@@ -106,22 +106,27 @@ if (Array.isArray(value)) { | ||
} | ||
} | ||
arrange(el, node, children) { | ||
if (el.tag === Portal) { | ||
}, | ||
arrange(tag, node, props, children) { | ||
if (tag === Portal) { | ||
return; | ||
} | ||
else if (typeof el.tag !== "string") { | ||
throw new Error(`Unknown tag: ${el.tag.toString()}`); | ||
else if (typeof tag !== "string") { | ||
throw new Error(`Unknown tag: ${tag.toString()}`); | ||
} | ||
const attrs = printAttrs(el.props); | ||
const open = `<${el.tag}${attrs.length ? " " : ""}${attrs}>`; | ||
const attrs = printAttrs(props); | ||
const open = `<${tag}${attrs.length ? " " : ""}${attrs}>`; | ||
let result; | ||
if (voidTags.has(el.tag)) { | ||
if (voidTags.has(tag)) { | ||
result = open; | ||
} | ||
else { | ||
const close = `</${el.tag}>`; | ||
const contents = "innerHTML" in el.props ? el.props["innerHTML"] : join(children); | ||
const close = `</${tag}>`; | ||
const contents = "innerHTML" in props ? props["innerHTML"] : join(children); | ||
result = `${open}${contents}${close}`; | ||
} | ||
node.value = result; | ||
}, | ||
}; | ||
class HTMLRenderer extends Renderer { | ||
constructor() { | ||
super(impl); | ||
} | ||
@@ -128,0 +133,0 @@ } |
/// <reference types="./index.d.ts" /> | ||
export { Context, Copy, Element, Fragment, Portal, Raw, Renderer, cloneElement, createElement, isElement } from './crank.js'; | ||
export { Context, ContextInternalsSymbol, Copy, Element, Fragment, Portal, Raw, Renderer, cloneElement, createElement, isElement } from './crank.js'; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@bikeshaving/crank", | ||
"version": "0.3.11", | ||
"version": "0.4.0-beta.1", | ||
"description": "Write JSX-driven components with functions, promises and generators.", | ||
@@ -122,11 +122,11 @@ "homepage": "https://crank.js.org", | ||
"devDependencies": { | ||
"@types/jest": "^26.0.19", | ||
"@typescript-eslint/eslint-plugin": "^4.11.0", | ||
"@typescript-eslint/parser": "^4.11.0", | ||
"core-js": "^3.8.1", | ||
"eslint": "^7.16.0", | ||
"eslint-config-prettier": "^7.1.0", | ||
"eslint-plugin-jest": "^24.1.3", | ||
"eslint-plugin-prettier": "^3.3.0", | ||
"eslint-plugin-react": "^7.21.5", | ||
"@types/jest": "^26.0.23", | ||
"@typescript-eslint/eslint-plugin": "^4.26.1", | ||
"@typescript-eslint/parser": "^4.26.1", | ||
"core-js": "^3.14.0", | ||
"eslint": "^7.28.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-jest": "^24.3.6", | ||
"eslint-plugin-prettier": "^3.4.0", | ||
"eslint-plugin-react": "^7.24.0", | ||
"husky": "^4.3.6", | ||
@@ -136,9 +136,9 @@ "jest": "^26.6.3", | ||
"magic-string": "^0.25.7", | ||
"prettier": "^2.2.1", | ||
"rollup": "^2.35.1", | ||
"rollup-plugin-typescript2": "^0.29.0", | ||
"prettier": "^2.3.1", | ||
"rollup": "^2.51.2", | ||
"rollup-plugin-typescript2": "^0.30.0", | ||
"shx": "^0.3.3", | ||
"ts-jest": "^26.4.4", | ||
"ts-jest": "^26.5.6", | ||
"ts-transform-import-path-rewrite": "^0.3.0", | ||
"typescript": "^4.1.3" | ||
"typescript": "^4.4.0-beta" | ||
}, | ||
@@ -145,0 +145,0 @@ "publishConfig": { |
/** | ||
* A type which represents all valid values for an element tag. | ||
* | ||
* 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 functions are called “component” elements, and their | ||
* behavior is determined by the execution of the component function. | ||
*/ | ||
@@ -15,3 +10,3 @@ export declare type Tag = string | symbol | Component; | ||
*/ | ||
export declare type TagProps<TTag extends Tag> = TTag extends string ? JSX.IntrinsicElements[TTag] : TTag extends Component<infer TProps> ? TProps : unknown; | ||
export declare type TagProps<TTag extends Tag> = TTag extends string ? JSX.IntrinsicElements[TTag] : TTag extends Component<infer TProps> ? TProps : Record<string, unknown>; | ||
/*** | ||
@@ -93,26 +88,10 @@ * SPECIAL TAGS | ||
*/ | ||
export declare type Component<TProps = any> = (this: Context<TProps>, props: TProps) => Children | PromiseLike<Children> | Iterator<Children, Children | void, any> | AsyncIterator<Children, Children | void, any>; | ||
export declare type Component<TProps extends Record<string, unknown> = any> = (this: Context<TProps>, props: TProps) => Children | PromiseLike<Children> | Iterator<Children, Children | void, any> | AsyncIterator<Children, Children | void, any>; | ||
/** | ||
* A type to keep track of keys. Any value can be a key, though null and | ||
* undefined are ignored. | ||
*/ | ||
declare type Key = unknown; | ||
declare const ElementSymbol: unique symbol; | ||
/** | ||
* 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. | ||
* | ||
* @template {Tag} [TTag=Tag] - The type of the tag of the element. | ||
* | ||
* @example | ||
* // specific element types | ||
* let div: Element<"div">; | ||
* let portal: Element<Portal>; | ||
* let myEl: Element<MyComponent>; | ||
* | ||
* // general element types | ||
* let host: Element<string | symbol>; | ||
* let component: Element<Component>; | ||
* | ||
* Typically, you use a helper function like createElement to create elements | ||
* rather than instatiating this class directly. | ||
*/ | ||
export declare class Element<TTag extends Tag = Tag> { | ||
export interface Element<TTag extends Tag = Tag> { | ||
/** | ||
@@ -150,55 +129,27 @@ * @internal | ||
ref: ((value: unknown) => unknown) | undefined; | ||
/** | ||
* @internal | ||
* flags - A bitmask. See ELEMENT FLAGS. | ||
*/ | ||
_f: number; | ||
/** | ||
* @internal | ||
* children - The rendered children of the element. | ||
*/ | ||
_ch: Array<NarrowedChild> | NarrowedChild; | ||
/** | ||
* @internal | ||
* node - The node or context associated with the element. | ||
* | ||
* For host elements, this property is set to the return value of | ||
* Renderer.prototype.create when the component is mounted, i.e. DOM nodes | ||
* for the DOM renderer. | ||
* | ||
* For component elements, this property is set to a Context instance | ||
* (Context<TagProps<TTag>>). | ||
* | ||
* We assign both of these to the same property because they are mutually | ||
* exclusive. We use any because the Element type has no knowledge of | ||
* renderer nodes. | ||
*/ | ||
_n: any; | ||
/** | ||
* @internal | ||
* fallback - The element which this element is replacing. | ||
* | ||
* If an element renders asynchronously, we show any previously rendered | ||
* values in its place until it has committed for the first time. This | ||
* property is set to the previously rendered child. | ||
*/ | ||
_fb: NarrowedChild; | ||
/** | ||
* @internal | ||
* inflightChildren - The current async run of the element’s children. | ||
* | ||
* This property is used to make sure Copy element refs fire at the correct | ||
* time, and is also used to create yield values for async generator | ||
* components with async children. It is unset when the element is committed. | ||
*/ | ||
_ic: Promise<any> | undefined; | ||
/** | ||
* @internal | ||
* onvalue(s) - This property is set to the resolve function of a promise | ||
* which represents the next children, so that renderings can be raced. | ||
*/ | ||
_ov: Function | undefined; | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref: ((value: unknown) => unknown) | undefined); | ||
get hadChildren(): boolean; | ||
static_: boolean | undefined; | ||
} | ||
/** | ||
* 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. | ||
* | ||
* @template {Tag} [TTag=Tag] - The type of the tag of the element. | ||
* | ||
* @example | ||
* // specific element types | ||
* let div: Element<"div">; | ||
* let portal: Element<Portal>; | ||
* let myEl: Element<MyComponent>; | ||
* | ||
* // general element types | ||
* let host: Element<string | symbol>; | ||
* let component: Element<Component>; | ||
* | ||
* Typically, you use a helper function like createElement to create elements | ||
* rather than instatiating this class directly. | ||
*/ | ||
export declare class Element<TTag extends Tag = Tag> { | ||
constructor(tag: TTag, props: TagProps<TTag>, key: Key, ref?: ((value: unknown) => unknown) | undefined, static_?: boolean | undefined); | ||
} | ||
export declare function isElement(value: any): value is Element; | ||
@@ -217,17 +168,8 @@ /** | ||
* Clones a given element, shallowly copying the props object. | ||
* | ||
* Used internally to make sure we don’t accidentally reuse elements when | ||
* rendering. | ||
*/ | ||
export declare function cloneElement<TTag extends Tag>(el: Element<TTag>): Element<TTag>; | ||
/*** 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. | ||
* A helper type which repesents all possible rendered values of an element. | ||
* | ||
* @template TNode - The node type for the element assigned by the renderer. | ||
* @template TNode - The node type for the element provided by the renderer. | ||
* | ||
@@ -253,37 +195,18 @@ * When asking the question, what is the “value” of a specific element, the | ||
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 and | ||
* disposing of nodes. | ||
* | ||
* @template TNode - The type of the node for a rendering environment. | ||
* @template TScope - Data which is passed down the tree. | ||
* @template TRoot - The type of the root for a rendering environment. | ||
* @template TResult - The type of exposed values. | ||
*/ | ||
export declare class Renderer<TNode, TScope, TRoot = TNode, TResult = ElementValue<TNode>> { | ||
declare type RetainerChild<TNode> = Retainer<TNode> | string | undefined; | ||
declare class Retainer<TNode> { | ||
el: Element; | ||
ctx: ContextInternals<TNode> | undefined; | ||
children: Array<RetainerChild<TNode>> | RetainerChild<TNode>; | ||
value: TNode | string | undefined; | ||
cached: ElementValue<TNode>; | ||
fallback: RetainerChild<TNode>; | ||
inflight: Promise<ElementValue<TNode>> | undefined; | ||
onCommit: Function | undefined; | ||
constructor(el: Element); | ||
} | ||
export interface RendererImpl<TNode, TScope, TRoot extends TNode = TNode, TResult = ElementValue<TNode>> { | ||
scope<TTag extends string | symbol>(scope: TScope | undefined, tag: TTag, props: TagProps<TTag>): TScope | undefined; | ||
create<TTag extends string | symbol>(tag: TTag, props: TagProps<TTag>, scope: TScope | undefined): TNode; | ||
/** | ||
* @internal | ||
* A weakmap which stores element trees by root. | ||
*/ | ||
_cache: WeakMap<object, Element<Portal>>; | ||
constructor(); | ||
/** | ||
* Renders an element tree into a specific root. | ||
* | ||
* @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 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 rendered value is exposed via render, schedule, | ||
@@ -305,19 +228,2 @@ * refresh, refs, or generator yield expressions. | ||
/** | ||
* Called in a preorder traversal for each host element. | ||
* | ||
* 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. | ||
* | ||
* @param el - The host element. | ||
* @param scope - The current scope. | ||
* | ||
* @returns The scope to be passed to create and scope for child host | ||
* elements. | ||
* | ||
* This method sets the scope for child host elements, not the current host | ||
* element. | ||
*/ | ||
scope(_el: Element<string | symbol>, scope: TScope | undefined): TScope; | ||
/** | ||
* Called for each string in an element tree. | ||
@@ -335,3 +241,3 @@ * | ||
*/ | ||
escape(text: string, _scope: TScope): string; | ||
escape(text: string, scope: TScope | undefined): string; | ||
/** | ||
@@ -345,54 +251,42 @@ * Called for each Raw element whose value prop is a string. | ||
*/ | ||
parse(text: string, _scope: TScope): TNode | string; | ||
parse(text: string, scope: TScope | undefined): TNode | string; | ||
patch<TTag extends string | symbol, TName extends string>(tag: TTag, node: TNode, name: TName, value: TagProps<TTag>[TName], oldValue: TagProps<TTag>[TName] | undefined, scope: TScope): unknown; | ||
arrange<TTag extends string | symbol>(tag: TTag, node: TNode, props: TagProps<TTag>, children: Array<TNode | string>, oldProps: TagProps<TTag> | undefined, oldChildren: Array<TNode | string> | undefined): unknown; | ||
dispose<TTag extends string | symbol>(tag: TTag, node: TNode, props: TagProps<TTag>): unknown; | ||
flush(root: TRoot): unknown; | ||
} | ||
/** | ||
* An abstract class which is subclassed to render to different target | ||
* environments. This class is responsible for kicking off the rendering | ||
* process and caching previous trees by root. | ||
* | ||
* @template TNode - The type of the node for a rendering environment. | ||
* @template TScope - Data which is passed down the tree. | ||
* @template TRoot - The type of the root for a rendering environment. | ||
* @template TResult - The type of exposed values. | ||
*/ | ||
export declare class Renderer<TNode extends object = object, TScope = unknown, TRoot extends TNode = TNode, TResult = ElementValue<TNode>> { | ||
/** | ||
* Called for each host element when it is committed for the first time. | ||
* | ||
* @param el - The host element. | ||
* @param scope - The current scope. | ||
* | ||
* @returns A “node” which determines the value of the host element. | ||
* @internal | ||
* A weakmap which stores element trees by root. | ||
*/ | ||
create(_el: Element<string | symbol>, _scope: TScope): TNode; | ||
cache: WeakMap<object, Retainer<TNode>>; | ||
impl: RendererImpl<TNode, TScope, TRoot, TResult>; | ||
constructor(impl: Partial<RendererImpl<TNode, TScope, TRoot, TResult>>); | ||
/** | ||
* Called for each host element when it is committed. | ||
* Renders an element tree into a specific root. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* @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 different renderers so that | ||
* events/provisions properly propagate. The context for a given root must be | ||
* the same or an error will be thrown. | ||
* | ||
* @returns The return value is ignored. | ||
* | ||
* Used to mutate the node associated with an element when new props are | ||
* passed. | ||
* @returns The result of rendering the children, or a possible promise of | ||
* the result if the element tree renders asynchronously. | ||
*/ | ||
patch(_el: Element<string | symbol>, _node: TNode): unknown; | ||
/** | ||
* Called for each host element so that elements can be arranged into a tree. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* @param children - An array of nodes and strings from child elements. | ||
* | ||
* @returns The return value is ignored. | ||
* | ||
* This method is also called by child components contexts as the last step | ||
* of a refresh. | ||
*/ | ||
arrange(_el: Element<string | symbol>, _node: TNode | TRoot, _children: Array<TNode | string>): unknown; | ||
/** | ||
* Called for each host element when it is unmounted. | ||
* | ||
* @param el - The host element. | ||
* @param node - The node associated with the host element. | ||
* | ||
* @returns The return value is ignored. | ||
*/ | ||
dispose(_el: Element<string | symbol>, _node: TNode): unknown; | ||
/** | ||
* Called at the end of the rendering process for each root of the tree. | ||
* | ||
* @param root - The root prop passed to portals or the render method. | ||
* | ||
* @returns The return value is ignored. | ||
*/ | ||
complete(_root: TRoot): unknown; | ||
render(children: Children, root?: TRoot | undefined, bridge?: Context | undefined): Promise<TResult> | TResult; | ||
} | ||
@@ -408,32 +302,23 @@ export interface Context extends Crank.Context { | ||
/** | ||
* 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. | ||
* | ||
* @template [TProps=*] - The expected shape of the props passed to the | ||
* component. Used to strongly type the Context iterator methods. | ||
* @template [TResult=*] - The readable element value type. It is used in | ||
* places such as the return value of refresh and the argument passed to | ||
* schedule and cleanup callbacks. | ||
* @internal | ||
*/ | ||
export declare class Context<TProps = any, TResult = any> implements EventTarget { | ||
declare class ContextInternals<TNode = unknown, TScope = unknown, TRoot extends TNode = TNode, TResult = unknown> { | ||
/** | ||
* @internal | ||
* flags - A bitmask. See CONTEXT FLAGS above. | ||
*/ | ||
_f: number; | ||
f: number; | ||
/** | ||
* @internal | ||
* facade - The actual object passed as this to components. | ||
*/ | ||
facade: Context<unknown, TResult>; | ||
/** | ||
* renderer - The renderer which created this context. | ||
*/ | ||
_re: Renderer<unknown, unknown, unknown, TResult>; | ||
renderer: RendererImpl<TNode, TScope, TRoot, TResult>; | ||
/** | ||
* @internal | ||
* root - The root node as set by the nearest ancestor portal. | ||
*/ | ||
_rt: unknown; | ||
root: TRoot | undefined; | ||
/** | ||
* @internal | ||
* host - The nearest ancestor host element. | ||
* host - The nearest host or portal retainer. | ||
* | ||
@@ -444,56 +329,63 @@ * When refresh is called, the host element will be arranged as the last step | ||
*/ | ||
_ho: Element<string | symbol>; | ||
host: Retainer<TNode>; | ||
/** | ||
* @internal | ||
* parent - The parent context. | ||
*/ | ||
_pa: Context<unknown, TResult> | undefined; | ||
parent: ContextInternals<TNode, TScope, TRoot, TResult> | undefined; | ||
/** | ||
* @internal | ||
* scope - The value of the scope at the point of element’s creation. | ||
*/ | ||
_sc: unknown; | ||
scope: TScope | undefined; | ||
/** | ||
* @internal | ||
* el - The associated component element. | ||
* retainer - The internal node associated with this context. | ||
*/ | ||
_el: Element<Component>; | ||
ret: Retainer<TNode>; | ||
/** | ||
* @internal | ||
* iterator - The iterator returned by the component function. | ||
*/ | ||
_it: Iterator<Children, Children | void, unknown> | AsyncIterator<Children, Children | void, unknown> | undefined; | ||
iterator: Iterator<Children, Children | void, unknown> | AsyncIterator<Children, Children | void, unknown> | undefined; | ||
/*** async properties ***/ | ||
/** | ||
* @internal | ||
* 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. | ||
*/ | ||
_oa: Function | undefined; | ||
/** | ||
* @internal | ||
* inflightBlock | ||
*/ | ||
_ib: Promise<unknown> | undefined; | ||
inflightBlock: Promise<unknown> | undefined; | ||
/** | ||
* @internal | ||
* inflightValue | ||
*/ | ||
_iv: Promise<ElementValue<any>> | undefined; | ||
inflightValue: Promise<ElementValue<TNode>> | undefined; | ||
/** | ||
* @internal | ||
* enqueuedBlock | ||
*/ | ||
_eb: Promise<unknown> | undefined; | ||
enqueuedBlock: Promise<unknown> | undefined; | ||
/** | ||
* @internal | ||
* enqueuedValue | ||
*/ | ||
_ev: Promise<ElementValue<any>> | undefined; | ||
enqueuedValue: Promise<ElementValue<TNode>> | undefined; | ||
/** | ||
* onavailable - A callback used in conjunction with the IsAvailable flag to | ||
* implement the props async iterator. See the Symbol.asyncIterator method | ||
* and the resumeCtxIterator function. | ||
*/ | ||
onAvailable: Function | undefined; | ||
constructor(renderer: RendererImpl<TNode, TScope, TRoot, TResult>, root: TRoot | undefined, host: Retainer<TNode>, parent: ContextInternals<TNode, TScope, TRoot, TResult> | undefined, scope: TScope | undefined, ret: Retainer<TNode>); | ||
} | ||
export declare const ContextInternalsSymbol: unique symbol; | ||
/** | ||
* 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. | ||
* | ||
* @template [TProps=*] - The expected shape of the props passed to the | ||
* component. Used to strongly type the Context iterator methods. | ||
* @template [TResult=*] - The readable element value type. It is used in | ||
* places such as the return value of refresh and the argument passed to | ||
* schedule and cleanup callbacks. | ||
*/ | ||
export declare class Context<TProps = any, TResult = any> implements EventTarget { | ||
/** | ||
* @internal | ||
* Contexts should never be instantiated directly. | ||
*/ | ||
constructor(renderer: Renderer<unknown, unknown, unknown, TResult>, root: unknown, host: Element<string | symbol>, parent: Context<unknown, TResult> | undefined, scope: unknown, el: Element<Component>); | ||
[ContextInternalsSymbol]: ContextInternals<unknown, unknown, unknown, TResult>; | ||
constructor(internals: ContextInternals<unknown, unknown, unknown, TResult>); | ||
/** | ||
@@ -511,4 +403,4 @@ * The current props of the associated element. | ||
* 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. | ||
* or the refresh, schedule, cleanup, or flush methods. This property is | ||
* mainly for plugins or utilities which wrap contexts. | ||
*/ | ||
@@ -537,2 +429,7 @@ get value(): TResult; | ||
/** | ||
* Registers a callback which fires when the component’s children are | ||
* rendered into the root. Will only fire once per callback and render. | ||
*/ | ||
flush(callback: (value: TResult) => unknown): void; | ||
/** | ||
* Registers a callback which fires when the component unmounts. Will only | ||
@@ -539,0 +436,0 @@ * fire once per callback. |
@@ -1,9 +0,5 @@ | ||
import { Children, Context, Element as CrankElement, ElementValue, Renderer } from "./crank"; | ||
export declare class DOMRenderer extends Renderer<Node, string | undefined> { | ||
import { Children, Context, ElementValue, Renderer } from "./crank"; | ||
export declare class DOMRenderer extends Renderer<Node, string> { | ||
constructor(); | ||
render(children: Children, root: Node, ctx?: Context): Promise<ElementValue<Node>> | ElementValue<Node>; | ||
parse(text: string): DocumentFragment; | ||
scope(el: CrankElement<string | symbol>, scope: string | undefined): string | undefined; | ||
create(el: CrankElement<string | symbol>, ns: string | undefined): Node; | ||
patch(el: CrankElement<string | symbol>, node: Element): void; | ||
arrange(el: CrankElement<string | symbol>, node: Node, children: Array<Node | string>): void; | ||
} | ||
@@ -10,0 +6,0 @@ export declare const renderer: DOMRenderer; |
@@ -1,10 +0,7 @@ | ||
import { Element, ElementValue, Renderer } from "./crank"; | ||
import { Renderer } from "./crank"; | ||
interface Node { | ||
value: string; | ||
} | ||
export declare class HTMLRenderer extends Renderer<Node | string, undefined, unknown, string> { | ||
create(): Node; | ||
escape(text: string): string; | ||
read(value: ElementValue<Node>): string; | ||
arrange(el: Element<string | symbol>, node: Node, children: Array<Node | string>): void; | ||
export declare class HTMLRenderer extends Renderer<Node, undefined, any, string> { | ||
constructor(); | ||
} | ||
@@ -11,0 +8,0 @@ export declare const renderer: HTMLRenderer; |
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 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
708836
7420