Mizu directive renderer.
Renderer(window: Window,unnamed 1?: RendererOptions,)
Renderer
constructor.
Retrieve Directive
-specific cache registry.
Directive-specific caches can be used to store related data.
These are automatically exposed by Renderer.render()
during Directive.setup()
, Directive.execute()
and Directive.cleanup()
executions.
import { Window } from "@mizu/internal/vdom" const directive = { name: "*foo", phase: Phase.TESTING, init(renderer) { if (!renderer.cache(this.name)) { renderer.cache<Cache<typeof directive>>(this.name, new WeakSet()) } }, setup(renderer, element, { cache }) { console.assert(cache instanceof WeakSet) console.assert(renderer.cache(directive.name) instanceof WeakSet) cache.add(element) } } as Directive<WeakSet<HTMLElement | Comment>> & { name: string } const renderer = await new Renderer(new Window(), { directives: [directive] }).ready const element = renderer.createElement("div", { attributes: { "*foo": "" } }) await renderer.render(element) console.assert(renderer.cache<Cache<typeof directive>>(directive.name).has(element))
Set Directive
-specific cache registry.
These are expected to be initialized by Renderer.load()
during Directive.init()
execution if a cache is needed.
import { Window } from "@mizu/internal/vdom" const directive = { name: "*foo", phase: Phase.TESTING, init(renderer) { renderer.cache(this.name, new WeakSet()) }, } as Directive<WeakSet<HTMLElement | Comment>> & { name: string } const renderer = await new Renderer(new Window(), { directives: [directive] }).ready console.assert(renderer.cache(directive.name) instanceof WeakSet)
cache<T>(directive: "*"): WeakMap<HTMLElement | Comment, HTMLElement>
Retrieve generic cache registry.
This cache is automatically created upon Renderer
instantiation.
It is shared between all Renderer.directives
and is mostly used to check whether a node was already processed,
and to map back Comment
nodes to their original HTMLElement
if they were replaced by Renderer.comment()
.
This cache should not be used to store Directive
-specific data.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready console.assert(renderer.cache("*") instanceof WeakMap)
comment(element: HTMLElement,): Comment
Replace a HTMLElement
by a Comment
.
Specified directive
and expression
are used to set Comment.nodeValue
and help identify which Directive
performed the replacement.
Use Renderer.uncomment()
to restore the original HTMLElement
.
Original HTMLElement
can be retrieved through the generic Renderer.cache()
.
If you hold a reference to a replaced HTMLElement
, use Renderer.getComment()
to retrieve the replacement Comment
.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const parent = renderer.createElement("div") const element = parent.appendChild(renderer.createElement("div")) const comment = renderer.comment(element, { directive: "foo", expression: "bar" }) console.assert(!parent.contains(element)) console.assert(parent.contains(comment)) console.assert(renderer.cache("*").get(comment) === element)
Create a new Attr
.
This bypasses the attribute name validation check.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const attribute = renderer.createAttribute("*foo", "bar") console.assert(attribute.name === "*foo") console.assert(attribute.value === "bar")
createElement<T extends HTMLElement>(tagname: string,properties?: Record<PropertyKey, unknown>,): T
Create a new HTMLElement
within Renderer.document
.
It is possible to specify additional properties that will be assigned to the element.
The attributes
property is handled by Renderer.setAttribute()
which allows to set attributes with non-standard characters.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const element = renderer.createElement("div", { innerHTML: "foo", attributes: { "*foo": "bar" } }) console.assert(element.tagName === "DIV") console.assert(element.innerHTML === "foo") console.assert(element.getAttribute("*foo") === "bar")
Create a new NamedNodeMap
.
This bypasses the illegal constructor check.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const nodemap = renderer.createNamedNodeMap() console.assert(nodemap.constructor.name.includes("NamedNodeMap"))
debug(message: string,target?: Nullable<HTMLElement | Comment>,): void
Generate a debug message.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const element = renderer.createElement("div") renderer.debug("foo", element)
Evaluate an expression with given HTMLElement
(or Comment
), Context
, State
and arguments.
Passed HTMLElement
or Comment
can be accessed through the this
keyword in the expression.
Both context
and state
are exposed through with
statements,
meaning that their properties can be accessed directly in the expression without prefixing them.
The difference between both is that the latter is not reactive and is intended to be used for specific stateful data added by a Directive
.
The root Renderer.internal
prefix is used internally to manage evaluation state, and thus cannot be used as a variable name.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready console.assert(await renderer.evaluate(null, "1 + 1") === 2) console.assert(await renderer.evaluate(null, "foo => foo", { args: ["bar"] }) === "bar") console.assert(await renderer.evaluate(null, "$foo", { state: { $foo: "bar" } }) === "bar")
flushReactiveRenderQueue(): Promise<void>
Flush the reactive render queue.
getAttributes(element: Optional<HTMLElement | Comment>,options?: { first: false; },): Attr[]
Retrieve all matching Attr
from an HTMLElement
.
It is designed to handle attributes that follows the syntax described in Renderer.parseAttribute()
.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const element = renderer.createElement("div", { attributes: { "*foo.modifier[value]": "bar" } }) console.assert(renderer.getAttributes(element, "*foo").length === 1)
getAttributes(element: Optional<HTMLElement | Comment>,options: { first: true; },): Nullable<Attr>
Retrieve the first matching Attr
from an HTMLElement
.
It is designed to handle attributes that follows the syntax described in Renderer.parseAttribute()
.
If no matching Attr
is found, null
is returned.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const element = renderer.createElement("div", { attributes: { "*foo.modifier[value]": "bar" } }) console.assert(renderer.getAttributes(element, "*foo", { first: true })?.value === "bar") console.assert(renderer.getAttributes(element, "*bar", { first: true }) === null)
getComment(element: HTMLElement): Nullable<Comment>
Retrieve the Comment
associated with an HTMLElement
replaced by Renderer.comment()
.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const element = renderer.document.documentElement.appendChild(renderer.createElement("div")) const comment = renderer.comment(element, { directive: "foo", expression: "bar" }) console.assert(renderer.getComment(element) === comment)
internal(name: string): `${Renderer.internal}_${string}`
Generate an internal identifier for specified name by prefixing it with Renderer.internal
.
When creating internal variables or functions in expressions, this method should always be used to name them. It ensures that they won't collide with end-user-defined variables or functions.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready console.assert(renderer.internal("foo").startsWith(`${Renderer.internal}_`))
Retrieve Renderer.internal
prefix.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready console.assert(renderer.internal() === Renderer.internal)
isComment(element: HTMLElement | Comment): element is Comment
Type guard for Comment
.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const element = renderer.createElement("div") const comment = renderer.comment(element, { directive: "foo", expression: "bar" }) console.assert(!renderer.isComment(element)) console.assert(renderer.isComment(comment))
isHtmlElement(element: HTMLElement | Comment): element is HTMLElement
Type guard for HTMLElement
.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const element = renderer.createElement("div") const comment = renderer.comment(element, { directive: "foo", expression: "bar" }) console.assert(renderer.isHtmlElement(element)) console.assert(!renderer.isHtmlElement(comment))
Load additional Directive
s.
A Directive
needs to have both a valid Directive.phase
and Directive.name
to be valid.
If a Directive
with the same name already exists, it is ignored.
It is possible to dynamically import()
a Directive
by passing a string
instead.
Note that in this case the resolved module must have an export default
statement.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const directive = { name: "*foo", phase: Phase.TESTING } as Directive & { name: string } await renderer.load([directive, import.meta.resolve("@mizu/test")]) console.assert(renderer.directives.includes(directive))
parseAttribute<T extends AttrTypings>(attribute: Attr,typings?: Nullable<T>,options?: Omit<RendererParseAttributeOptions, "modifiers"> & { modifiers: true; },): InferAttrTypings<T>
Parse an Attr
from an HTMLElement
.
Prefixes can automatically be stripped from the attribute name by setting the prefix
option.
It is especially useful when Attr
were extracted with a RegExp
matching.
The typings
descriptor is passed to enforce types and validate values on Attr.value
and modifiers.
The following AttrTypings
are supported:
AttrBoolean
, matchingBOOLEAN
token described below.AttrBoolean.default
istrue
AttrNumber
, matchingNUMBER
token described below.AttrNumber.default
is0
.AttrNumber.integer
will round the value to the nearest integer.AttrNumber.min
will clamp the value to a minimum value.AttrNumber.max
will clamp the value to a maximum value.
AttrDuration
, matchingDURATION
described below.AttrDuration.default
is0
.- Value is normalized to milliseconds.
- Value is clamped to a minimum of 0, and is rounded to the nearest integer.
AttrString
(the default), matchingSTRING
token described below.AttrString.default
is""
, or firstAttrString.allowed
value if set.AttrString.allowed
will restrict the value to a specific set of strings, in a similar fashion to an enum.
A AttrAny.default
is only applied when a key has been explicitly defined but its value was not.
Use AttrAny.enforce
to force the default value to be applied event if the key was not defined.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const modifier = (attribute: Attr, typing: AttrBoolean) => renderer.parseAttribute(attribute, { modifiers: { value: typing } }, { modifiers: true }).modifiers const [a, b] = Array.from(renderer.createElement("div", { attributes: { "*a.value": "", "*b": "" } }).attributes) // `a.value === true` because it was defined, and the default for `AttrBoolean` is `true` console.assert(modifier(a, { type: Boolean }).value === true) // `a.value === false` because it was defined, and the default was set to `false` console.assert(modifier(a, { type: Boolean, default: false }).value === false) // `b.value === undefined` because it was not explicitly defined, despite the default for `AttrBoolean` being `true` console.assert(modifier(b, { type: Boolean }).value === undefined) // `b.value === true` because it was not explicitly defined, but the default was enforced console.assert(modifier(b, { type: Boolean, enforce: true }).value === true) // `b.value === false` because it was not explicitly defined, and the default was set to `false` and was enforced console.assert(modifier(b, { type: Boolean, default: false, enforce: true }).value === false)
Modifiers parsing must be explicitly enabled with modifiers: true
.
This is to prevent unnecessary parsing when modifiers are not needed.
Supported syntax is described below.
Please note that this syntax is still ruled by HTML standards and might not be fully accurate or subject to limitations.
It is advised to refrain from using especially complex Attr.name
that contain specials characters or structures that may prove both confusing and challenging to parse.
┌────────┬──────┬─────┬───────────┬─────┬───────┐ │ PREFIX │ NAME │ TAG │ MODIFIERS │ '=' │ VALUE │ └────────┴──────┴─────┴───────────┴─────┴───────┘ PREFIX └─[ PREFIX]── STRING NAME ├─[ ESCAPED]── STRING '{' STRING '}' └─[ UNDOTTED]── [^.] TAG ├─[ TAG]── '[' STRING ']' └─[ NONE]── ∅ MODIFIERS ├─[ MODIFIER]── '.' MODIFIER MODIFIERS └─[ NONE]── ∅ MODIFIER ├─[ KEY]── STRING └─[KEY+VALUE]── STRING '[' MODIFIER_VALUE ']' MODIFIER_VALUE └─[ VALUE]── BOOLEAN | NUMBER | DURATION | STRING BOOLEAN ├─[ TRUE]── 'yes' | 'on' | 'true' └─[ FALSE]── 'no' | 'off' | 'false' NUMBER ├─[EXPL_NEG_N]── '-' POSITIVE_NUMBER ├─[EXPL_POS_N]── '+' POSITIVE_NUMBER └─[IMPL_POS_N]── POSITIVE_NUMBER POSITIVE_NUMBER ├─[ INTEGER]── [0-9] ├─[EXPL_FLOAT]── [0-9] '.' [0-9] └─[IMPL_FLOAT]── '.' [0-9] DURATION └─[ DURATION]── POSITIVE_NUMBER DURATION_UNIT DURATION_UNIT ├─[ MINUTES]── 'm' ├─[ SECONDS]── 's' ├─[EXPL_MILLI]── 'ms' └─[IMPL_MILLI]── ∅ STRING └─[ ANY]── [*]
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const element = renderer.createElement("div", { attributes: { "*foo.bar[baz]": "true" } }) const [attribute] = Array.from(element.attributes) let parsed const typings = { type: Boolean, modifiers: { bar: { type: String } } } parsed = renderer.parseAttribute(attribute, typings, { modifiers: true }) console.assert(parsed.name === "*foo") console.assert(parsed.value === true) console.assert(parsed.modifiers.bar === "baz") parsed = renderer.parseAttribute(attribute, typings, { modifiers: false, prefix: "*" }) console.assert(parsed.name === "foo") console.assert(parsed.value === true) console.assert(!("modifiers" in parsed))
const typedef = { // "yes", "on", "true", "no", "off", "false" boolean: { type: Boolean, default: false }, // "0", "3.1415", ".42", "69", etc. number: { type: Number, default: 0, min: -Infinity, max: Infinity, integer: false }, // "10", "10ms", "10s", "10m", etc. duration: { type: Date, default: "0ms" }, // "foo", "bar", "foobar", etc. string: { type: String, default: "" }, // "foo", "bar" enum: { type: String, get default() { return this.allowed[0] } , allowed: ["foo", "bar"] }, }
parseAttribute<T extends AttrTypings>(attribute: Attr,typings?: Nullable<T>,options?: Omit<RendererParseAttributeOptions, "modifiers"> & { modifiers?: false; },): Omit<InferAttrTypings<T>, "modifiers">
Same as Renderer.parseAttribute()
but without modifiers.
render<T extends Element>(element: T,options?: Omit<RendererRenderOptions, "select" | "stringify"> & { stringify?: false; },): Promise<T>
Render Element
and its subtree with specified Context
and State
against Renderer.directives
.
import { Window } from "@mizu/internal/vdom" import _test from "@mizu/test" const renderer = await new Renderer(new Window(), { directives: [ _test ] }).ready const element = renderer.createElement("div", { attributes: { "~test.text": "foo" } }) const result = await renderer.render(element, { context: new Context({ foo: "bar" }) }) console.assert(result.textContent === "bar")
render<T extends Element>(element: HTMLElement,options?: Omit<RendererRenderOptions, "select" | "stringify">
& Required<Pick<RendererRenderOptions, "select">>
& { stringify?: false; },): Promise<Nullable<T>>
Render Element
and its subtree with specified Context
and State
against Renderer.directives
and query select the return using a CSS selector.
import { Window } from "@mizu/internal/vdom" import _test from "@mizu/test" const renderer = await new Renderer(new Window(), { directives: [ _test ] }).ready const element = renderer.createElement("div", { innerHTML: renderer.createElement("span", { attributes: { "~test.text": "foo" } }).outerHTML }) const result = await renderer.render(element, { context: new Context({ foo: "bar" }), select: "span" }) console.assert(result?.tagName === "SPAN") console.assert(result?.textContent === "bar")
render(element: HTMLElement,options?: Omit<RendererRenderOptions, "reactive" | "stringify"> & { stringify: true; },): Promise<string>
Render Element
and its subtree with specified Context
and State
against Renderer.directives
and returns it as an HTML string.
import { Window } from "@mizu/internal/vdom" import _test from "@mizu/test" const renderer = await new Renderer(new Window(), { directives: [ _test ] }).ready const element = renderer.createElement("div", { attributes: { "~test.text": "foo" } }) const result = await renderer.render(element, { context: new Context({ foo: "bar" }), stringify: true }) console.assert(result.startsWith("<!DOCTYPE html>"))
replaceElementWithChildNodes(a: HTMLElement,b: HTMLElement,): void
Replace a HTMLElement
with another HTMLElement.childNodes
.
Note that the HTMLElement
is entirely replaced, meaning that is is actually removed from the DOM.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const parent = renderer.createElement("div") const slot = parent.appendChild(renderer.createElement("slot")) as HTMLSlotElement const content = renderer.createElement("div", { innerHTML: "<span>foo</span><span>bar</span>" }) renderer.replaceElementWithChildNodes(slot, content) console.assert(parent.innerHTML === "<span>foo</span><span>bar</span>")
setAttribute(): void
Set an Attr
on a HTMLElement
or updates a Comment.nodeValue
.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const element = renderer.createElement("div") renderer.setAttribute(element, "*foo", "bar") console.assert(element.getAttribute("*foo") === "bar") const comment = renderer.comment(element, { directive: "foo", expression: "bar" }) renderer.setAttribute(comment, "*foo", "bar") console.assert(comment.nodeValue?.includes(`[*foo="bar"]`))
uncomment(comment: Comment): HTMLElement
Replace Comment
by restoring its original HTMLElement
.
Calling this method on a Comment
that was not created by Renderer.comment()
will throw a ReferenceError
.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const parent = renderer.createElement("div") const element = parent.appendChild(renderer.createElement("div")) const comment = renderer.comment(element, { directive: "foo", expression: "bar" }) renderer.uncomment(comment) console.assert(!parent.contains(comment)) console.assert(parent.contains(element)) console.assert(!renderer.cache("*").has(comment))
warn(message: string,target?: Nullable<HTMLElement | Comment>,): void
Generate a warning message.
If no warnings callback was provided, the warning message is applied with Renderer.setAttribute()
with the name *warn
to the target
HTMLElement
or Comment
if there is one.
import { Window } from "@mizu/internal/vdom" const renderer = await new Renderer(new Window()).ready const element = renderer.createElement("div") renderer.warn("foo", element) console.assert(element.getAttribute("*warn") === "foo") const comment = renderer.comment(element, { directive: "foo", expression: "bar" }) renderer.warn("foo", comment) console.assert(comment.nodeValue?.includes(`[*warn="foo"]`))
internal: "__mizu_internal"
Internal identifier prefix.
This is used to avoid conflicts with user-defined variables in expressions.