@oazmi/tsignal
Version:
a topological order respecting signals library inspired by SolidJS
146 lines (145 loc) • 7.36 kB
JavaScript
/** a minimal implementation of JSX runtime element creation. <br>
* to use in `esbuild`'s javascript build API, you will need to do one of the following options (or do both):
*
* 1) option 1 (preferred): <br>
* for JSX to work with your IDE's LSP, and for esbuild to automatically discover the hyperscript functions,
* you will need to include the following two comment lines at the top of your `.tsx` script:
* ```tsx
* /** \@jsx h *\/
* /** \@jsxFrag hf *\/
* ```
*
* 2) option 2 (no LSP support): <br>
* in the esbuild build options (`BuildOptions`), set `jsxFactory = "h"` and `jsxFragment = "hf"`.
* ```ts
* import { build, stop } from "https://deno.land/x/esbuild/mod.js"
* build({
* entryPoints: ["./path/to/your/script.tsx"],
* jsxFactory: "h",
* jsxFragment: "Fragment",
* // other build options
* minify: true,
* })
* stop()
* ```
*
* and now in your `.jsx` script, you should:
* - import `createHyperScript` from this module
* - create a reactive signal `Context`
* - call `createHyperScript` with the signal context `ctx` as the argument
* - the returned tuple will contain 3 elements:
* - the first element should be named `h` (which is the name you declare as `\@jsx h` in **option 1** or `jsxFactory = "h"` in **option 2**)
* - the second element should be named `hf` (which is the name you declare as `\@jsxFrag hf` in **option 1** or `jsxFragment = "hf"` in **option 2**)
* - the third can be named anything
*
* @example
* ```tsx
* // the `\@jsx h` comment comes here, but I can't show multiline comments in this documentation.
* // the `\@jsxFrag hf` comment comes here, but I can't show multiline comments in this documentation.
*
* import { createHyperScript } from "./path/to/tsignal/jsx-runtime/mod.ts"
* import { Context } from "./path/to/tsignal/mod.ts"
*
* const ctx = new Context()
* const [h, hf, namespaceStack] = createHyperScript(ctx)
*
* const my_elem = <div>Hello world</div>
* const my_fragment_elems = <>
* <span>World<span>
* <span>Hello<span>
* </>
* const my_elem2 = <div>...my_fragment_elems</div>
* document.body.appendChild(my_elem)
* document.body.appendChild(my_elem2)
*
* // when creating svgs or xml, you will have to change the DOM namespace, so that the correct kinds of `Node`s are created.
* namespaceStack.push("svg")
* const my_svg = <svg viewBox="0 0 200 200">
* <g transform="translate(100, 50)">
* <text text-anchor="middle">SVG says Hi!</text>
* <text y="25" text-anchor="middle">SVG stands for "SUGOI! Vector Graphics"</text>
* </g>
* </svg>
* namespaceStack.pop()
* ```
*
* @module
*/
import { array_isArray, bind_array_pop, bind_array_push, bind_stack_seek, isFunction, object_entries } from "../deps.js";
import { AttrSignal_Factory, TextNodeSignal_Factory } from "../dom_signal.js";
const specialTagNameSpaces = {
"svg": "http://www.w3.org/2000/svg"
};
// TODO: think of how to make `createHyperScript` extendable via add-on style plugins.
// for example, it would become possible for you to declare that `ctx.addClass(TextNodeSignal_Factory)` should be used for creating reactive text nodes,
// and `ctx.addClass(AttrSignal_Factory)` should be used for creating attribute nodes. etc...
// TODO: make hyperscript handler for `<input />` html types (and buttons, links, etc...) that
// can fire an update cycle (such as setting a state signal, or firing an effect signal).
// essentially, being able to `ctx.runId(the_signal_id)`
/** create hyperscript functions to create elements and fragments during runtime after your JSX or TSX have been transformed. */
export const createHyperScript = (ctx) => {
const
// TODO: add `HtmlNodeSignal_Factory` too, and test it. or maybe implement an addon style feature to add `HtmlNodeSignal_Factory`, and other custom signal classes.
createText = ctx.addClass(TextNodeSignal_Factory), createAttr = ctx.addClass(AttrSignal_Factory);
const namespace_stack = [], namespace_stack_push = bind_array_push(namespace_stack), namespace_stack_pop = bind_array_pop(namespace_stack), namespace_stack_seek = bind_stack_seek(namespace_stack);
const createElement = (component, props, ...children) => {
props ??= {};
const current_namespace_tag = namespace_stack_seek();
if (current_namespace_tag) {
return createElementNS(specialTagNameSpaces[current_namespace_tag], component, props, ...children);
}
const child_nodes = children.flatMap((child) => child instanceof Node
? child
: createText(child)[2]), is_component_generator = isFunction(component), component_node = is_component_generator
? component(props)
: component === undefined
? []
: document.createElement(component);
// now, depending on whether or not the `component_node` is a fragment (i.e. an array of nodes),
// we append the children as child elements if the `component_node` is a single `Element`,
// or we insert the children as siblings if the `component_node` is an array of elements (`Element[]`)
if (!array_isArray(component_node)) {
// we only assign the props as attributes iff the created node did not come from a component generator. this is also the behavior exhibited by ReactJS.
if (!is_component_generator) {
// assign the props as reactive attributes of the new element node
for (const [attr_name, attr_value] of object_entries(props)) {
// we always attach the attribute node to the designated `component_node` before making it reactive,
// because the reactivity may require the existence of a parent node (`attr.ownerElement`)
const attr = document.createAttribute(attr_name);
component_node.setAttributeNode(attr);
createAttr(attr, attr_value);
}
}
component_node.append(...child_nodes);
}
else {
component_node.push(...child_nodes);
}
return component_node;
};
const createElementNS = (namespace_uri, component, props, ...children) => {
props ??= {};
const component_node = document.createElementNS(namespace_uri, component), child_nodes = children.flatMap((child) => child instanceof Node
? child
: createText(child)[2]);
// assign the props as reactive attributes of the new element node
for (const [attr_name, attr_value] of object_entries(props)) {
// svg doesn't work when their attributes are made with a namespaceURI (i.e. createAttributeNS doesn't work for svgs). strange.
// const attr = document.createAttributeNS(namespace_uri, attr_name)
component_node.setAttributeNode(createAttr(attr_name, attr_value)[2]);
}
component_node.append(...child_nodes);
return component_node;
};
const namespaceStack = {
push: namespace_stack_push,
pop: namespace_stack_pop,
seek: namespace_stack_seek,
};
const createFragment = createElement.bind(undefined, undefined);
return [
createElement,
createFragment,
namespaceStack
];
};