Skip to main content

Built and signed on GitHub Actions

🦮 Internal components for mizu.js

This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
This package works with Cloudflare Workers
This package works with Node.js
This package works with Deno
This package works with Bun
This package works with Browsers
JSR Score
100%
Published
a week ago (0.7.1)
class Renderer

Mizu directive renderer.

Constructors

new
Renderer(
window: Window,
unnamed 1?: RendererOptions,
)

Renderer constructor.

Properties

readonly
directives: Readonly<Directive[]>

Directive list.

Linked Document.

readonly
ready: Promise<this>

Whether the Renderer is ready to be used.

This promise resolves once the initial Renderer.load() call is completed.

Linked Window.

Methods

cache<T>(directive: Directive["name"]): T

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))
cache<T>(
directive: Directive["name"],
cache: T,
): T

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)

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,
unnamed 1: { directive: string; expression: string; },
): 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(
that: Nullable<HTMLElement | Comment>,
expression: string,
): Promise<unknown>

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.

Note

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")

Flush the reactive render queue.

getAttributes(
element: Optional<HTMLElement | Comment>,
names: Arrayable<string> | RegExp,
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>,
names: Arrayable<string> | RegExp,
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)

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))

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(directives: Arrayable<Arrayable<Partial<Directive> | string>>): Promise<this>

Load additional Directives.

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:

Important

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)

Note

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>"))

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(
element: HTMLElement | Comment,
name: string,
value?: string,
): 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"]`))

Replace Comment by restoring its original HTMLElement.

Calling this method on a Commentthat 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"]`))

Static Properties

readonly
internal: "__mizu_internal"

Internal identifier prefix.

This is used to avoid conflicts with user-defined variables in expressions.

Add Package

deno add jsr:@mizu/internal

Import symbol

import { Renderer } from "@mizu/internal/engine";

---- OR ----

Import directly with a jsr specifier

import { Renderer } from "jsr:@mizu/internal/engine";

Add Package

npx jsr add @mizu/internal

Import symbol

import { Renderer } from "@mizu/internal/engine";

Add Package

yarn dlx jsr add @mizu/internal

Import symbol

import { Renderer } from "@mizu/internal/engine";

Add Package

pnpm dlx jsr add @mizu/internal

Import symbol

import { Renderer } from "@mizu/internal/engine";

Add Package

bunx jsr add @mizu/internal

Import symbol

import { Renderer } from "@mizu/internal/engine";