RenderAdapter

Interface for adapting the rendering process to a specific target environment.

Syntax

interface RenderAdapter<
TNode,
TScope,
TRoot extends TNode | undefined = TNode,
TResult = ElementValue<TNode>
> {
create(data: CreateData): TNode;
adopt(data: AdoptData): Array<TNode> | undefined;
text(data: TextData): TNode;
scope(data: ScopeData): TScope | undefined;
raw(data: RawData): ElementValue<TNode>;
patch(data: PatchData): void;
arrange(data: ArrangeData): void;
remove(data: RemoveData): void;
read(value: ElementValue<TNode>): TResult;
finalize(root: TRoot): void;
}

Type parameters

Methods

create()

create(data: {
tag: string | symbol;
tagName: string;
props: Record<string, any>;
scope: TScope | undefined;
root: TRoot | undefined;
}): TNode

Creates a new node for the given element tag and props.

Called when Crank encounters a new element that needs to be rendered for the first time.

adopt()

adopt(data: {
tag: string | symbol;
tagName: string;
props: Record<string, any>;
node: TNode | undefined;
scope: TScope | undefined;
root: TRoot | undefined;
}): Array<TNode> | undefined

Adopts existing nodes during hydration.

Called when hydrating server-rendered content. Returns an array of child nodes if the provided node matches the expected tag, or undefined if hydration should fail.

text()

text(data: {
value: string;
scope: TScope | undefined;
oldNode: TNode | undefined;
hydrationNodes: Array<TNode> | undefined;
root: TRoot | undefined;
}): TNode

Creates or updates a text node.

Called when rendering text content. Should create a new text node or update an existing one.

scope()

scope(data: {
tag: string | symbol;
tagName: string;
props: Record<string, any>;
scope: TScope | undefined;
root: TRoot | undefined;
}): TScope | undefined

Computes scope context for child elements.

Used to pass rendering context like theme, namespaces, or coordinate systems down the tree.

raw()

raw(data: {
value: string | TNode;
scope: TScope | undefined;
hydrationNodes: Array<TNode> | undefined;
root: TRoot | undefined;
}): ElementValue<TNode>

Handles raw values (strings or nodes) that bypass normal element processing.

Called when rendering Raw elements for direct node or HTML insertion.

patch()

patch(data: {
tag: string | symbol;
tagName: string;
node: TNode;
props: Record<string, any>;
oldProps: Record<string, any> | undefined;
scope: TScope | undefined;
root: TRoot | undefined;
copyProps: Set<string> | undefined;
isHydrating: boolean;
quietProps: Set<string> | undefined;
}): void

Updates a node's properties.

Called when element props change. Should efficiently update only changed properties.

arrange()

arrange(data: {
tag: string | symbol;
tagName: string;
node: TNode;
props: Record<string, any>;
children: Array<TNode>;
oldProps: Record<string, any> | undefined;
root: TRoot | undefined;
}): void

Arranges child nodes within their parent.

Called after children are rendered to organize them in the correct order.

remove()

remove(data: {
node: TNode;
parentNode: TNode;
isNested: boolean;
root: TRoot | undefined;
}): void

Removes a node from its parent.

Called when an element is being unmounted. The isNested parameter indicates if this is a child of an already-removed element.

read()

read(value: ElementValue<TNode>): TResult

Reads the final rendered value from an ElementValue.

Allows transforming the internal node representation into the public API.

finalize()

finalize(root: TRoot): void

Performs final rendering to the root container.

Called after the entire render cycle is complete. Use for triggering actual rendering in non-immediate environments (canvas, WebGL, etc.).

Examples

Basic adapter implementation

import type {RenderAdapter} from "@b9g/crank";

interface TerminalNode {
type: string;
text?: string;
children: TerminalNode[];
styles: Record<string, any>;
}

const terminalAdapter: Partial<RenderAdapter<TerminalNode, void>> = {
create({tag, props}) {
return {
type: tag as string,
children: [],
styles: props.style || {},
};
},

text({value}) {
return {
type: "text",
text: value,
children: [],
styles: {},
};
},

patch({node, props, oldProps}) {
if (props.style !== oldProps?.style) {
Object.assign(node.styles, props.style);
}
},

arrange({node, children}) {
node.children = children;
},

remove({node, parentNode, isNested}) {
if (!isNested) {
const index = parentNode.children.indexOf(node);
if (index > -1) {
parentNode.children.splice(index, 1);
}
}
},

read(value) {
return value;
},
};

Using scope for namespaces

const domAdapter: Partial<RenderAdapter<Node, string>> = {
scope({tag, props, scope}) {
// Handle XML namespaces
if (tag === "svg") {
return "http://www.w3.org/2000/svg";
}
if (tag === "math") {
return "http://www.w3.org/1998/Math/MathML";
}
return props.xmlns || scope;
},

create({tag, scope, root}) {
const doc = root?.ownerDocument || document;
if (scope) {
return doc.createElementNS(scope, tag as string);
}
return doc.createElement(tag as string);
},
// ... other methods
};

See also

Edit on GitHub