feat: added jsx support and some tests

This commit is contained in:
Daniel Ledda
2022-05-26 22:00:23 +02:00
parent 182c38232e
commit e5e0c47f68
10 changed files with 173 additions and 75 deletions

View File

@@ -13,7 +13,7 @@ export interface ICapsule<T extends Captable = Captable> {
}
export function isCapsule(maybeCapsule: any): maybeCapsule is ICapsule {
return Object.prototype.hasOwnProperty.call(maybeCapsule, 'val')
return typeof maybeCapsule.val !== "undefined"
&& typeof maybeCapsule.watch === "function"
&& typeof maybeCapsule.toString === "function";
}

View File

@@ -3,36 +3,36 @@ import {SubNode} from "./helpers";
export type RungOptions = {};
export default abstract class Rung {
protected el: HTMLElement | null = null;
protected node: Node | null = null;
protected constructor(options: RungOptions) {}
render(): HTMLElement {
if (!this.el) {
this.el = this.build();
render(): Node {
if (!this.node) {
this.node = this.build();
}
return this.el;
return this.node;
}
protected getEl(): HTMLElement {
protected getEl(): Node {
return this.render();
}
redraw(): void {
const oldNode = this.el;
if (!oldNode || !this.el) {
const oldNode = this.node;
if (!oldNode || !this.node) {
return;
}
const parent = this.el.parentElement;
const parent = this.node.parentElement;
if (parent) {
this.el = this.build();
parent.replaceChild(this.el, oldNode);
this.node = this.build();
parent.replaceChild(this.node, oldNode);
} else {
this.render();
}
}
protected abstract build(): HTMLElement;
protected abstract build(): Node;
}
export type FunctionalRung<Props extends Record<string, any>, N extends Node> = (attributes: Props, subNodes: SubNode[]) => N;
export type FunctionalRung<Props extends Record<string, any>, N extends HTMLElement> = (attributes: Props, subNodes?: SubNode[]) => N;

View File

@@ -2,17 +2,21 @@ import {isCapsule, ICapsule} from "./Capsule";
import {ISubscription} from "./Publisher";
import Rung, {FunctionalRung} from "./Rung";
export type IRenderAttributes<T extends keyof HTMLElementTagNameMap> = Partial<{
[K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | ICapsule<HTMLElementTagNameMap[T][K]>
}> & {
type CommonRenderAttributes<T> = {
classes?: string[],
saveTo?: ICapsule<HTMLElementTagNameMap[T] | null>,
saveTo?: ICapsule<T | null>,
};
export type IRenderAttributes<T extends keyof HTMLElementTagNameMap | DocumentFragment> =
T extends DocumentFragment
? Partial<{ [K in keyof DocumentFragment]: DocumentFragment[K] | ICapsule<DocumentFragment[K]> }> & CommonRenderAttributes<DocumentFragment>
: T extends keyof HTMLElementTagNameMap
? Partial<{
[K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | ICapsule<HTMLElementTagNameMap[T][K]>
}> & CommonRenderAttributes<HTMLElementTagNameMap[T]>
: never;
type IdSelector = `#${ string }`;
export function bootstrap(app: Rung, id: IdSelector) {
const rootNode = document.querySelector(id);
export function bootstrap(app: Rung, id: string) {
const rootNode = document.getElementById(id);
if (!rootNode) {
throw new Error(`No node was found with the id ${id} to attach to`);
} else {
@@ -20,8 +24,11 @@ export function bootstrap(app: Rung, id: IdSelector) {
}
}
export function frag(subs?: Node[]): DocumentFragment {
export function frag(attributes: IRenderAttributes<DocumentFragment> | null, subs?: SubNode[]): DocumentFragment {
const frag = document.createDocumentFragment();
if (attributes) {
applyAttributes(frag, attributes);
}
if (subs) {
attachSubs(frag, subs);
}
@@ -32,27 +39,62 @@ export function q(text: string): Text {
return document.createTextNode(text);
}
type InstantiationType = FunctionalRung<any, any> | keyof HTMLElementTagNameMap;
type InstantiationType = FunctionalRung<any, any> | keyof HTMLElementTagNameMap | Rung;
type Props<T> =
T extends FunctionalRung<infer Attributes, infer Return>
? Attributes
? Attributes & CommonRenderAttributes<Return>
: T extends keyof HTMLElementTagNameMap
? IRenderAttributes<T>
: never;
: T extends Rung
? CommonRenderAttributes<T>
: never;
export type SubNode = Rung | Node | ICapsule;
export function h<T extends keyof HTMLElementTagNameMap>(type: T, attributes?: Props<T>, ...subNodes: SubNode[]): HTMLElementTagNameMap[T];
export function h<T extends FunctionalRung<any, any>, U extends Props<T>, V extends ReturnType<T>>(type: T, attributes?: U, ...subNodes: SubNode[]): V;
export function h<T extends FunctionalRung<any, any>, U extends Props<T>>(type: T, attributes?: U, ...subNodes: SubNode[]): ReturnType<T>;
export function h<T extends Rung>(type: T, attributes: CommonRenderAttributes<T>): ReturnType<T["render"]>;
export function h<T extends InstantiationType>(type: T, attributes?: Props<T> | null, ...subNodes: SubNode[]) {
if (typeof type === "function") {
return type(attributes, subNodes);
return createFunctionalRungElement(type, attributes, subNodes);
} else if (type instanceof Rung) {
const rendered = type.render();
if (attributes?.classes && rendered instanceof HTMLElement) {
rendered.classList.add(...attributes.classes);
}
if (attributes?.saveTo) {
attributes.saveTo.val = rendered;
}
return rendered;
} else {
return createStandardElement(type, attributes ?? {}, subNodes);
}
}
function createFunctionalRungElement<T extends FunctionalRung<any, any>, U extends Props<T>>(
type: T,
attributes: U,
subNodes: SubNode[]
): ReturnType<T> {
for (let i = 0; i < subNodes.length; i++) {
const subNode = subNodes[i];
if (isCapsule(subNode)) {
const textNode = q(subNode.toString());
const sub = subNode.watch((newVal) => nodeCapsuleWatcher<ICapsule>(newVal, textNode, sub));
subNodes[i] = textNode;
}
}
const rendered = subNodes.length > 0 ? type(attributes, subNodes) : type(attributes);
if (attributes?.classes && rendered instanceof HTMLElement) {
rendered.classList.add(...attributes.classes);
}
if (attributes?.saveTo) {
attributes.saveTo.val = rendered;
}
return rendered;
}
function createStandardElement<T extends keyof HTMLElementTagNameMap>(
type: T,
attributes: IRenderAttributes<T> | null,
@@ -77,7 +119,7 @@ function nodeCapsuleWatcher<T>(newVal: T extends ICapsule<infer U> ? U : never,
sub.unbind();
textNode.remove();
} else {
textNode.replaceWith(newVal?.toString() ?? q("[dead ref]"));
textNode.textContent = newVal?.toString() ?? "[dead ref]";
}
}
@@ -96,7 +138,9 @@ function attachSubs(node: Element | DocumentFragment, subNodes: SubNode[]): void
}
}
function applyAttributes<T extends keyof HTMLElementTagNameMap>(element: HTMLElement, attributes: IRenderAttributes<T>): void {
function applyAttributes<T extends keyof HTMLElementTagNameMap>(element: HTMLElementTagNameMap[T], attributes: IRenderAttributes<T>): void;
function applyAttributes(element: DocumentFragment, attributes: IRenderAttributes<DocumentFragment>): void;
function applyAttributes(element: HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | DocumentFragment, attributes: IRenderAttributes<any>): void {
for (const key in attributes) {
if (Object.prototype.hasOwnProperty.call(attributes, key)) {
const attribute = (attributes as Record<string, unknown>)[key];

27
lib/jsxFactory.ts Normal file
View File

@@ -0,0 +1,27 @@
import { IRenderAttributes } from './helpers';
import Rung from "./Rung";
import {ICapsule} from "./Capsule";
type RenderAttributesMap = {
[TagName in keyof HTMLElementTagNameMap]: IRenderAttributes<TagName>;
};
declare global {
namespace JSX {
interface Element extends Node {}
interface ElementClass extends Rung {}
interface AttributeCollection {
[name: string]: string | boolean | (() => any);
className: string;
}
interface IntrinsicAttributes {
saveTo?: ICapsule<Node | null>;
classes?: string[];
}
interface IntrinsicClassAttributes {
saveTo?: ICapsule<Node | null>;
classes?: string[];
}
interface IntrinsicElements extends RenderAttributesMap {}
}
}