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

11
index.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ladder Test Playground</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./test.tsx"></script>
</body>
</html>

View File

@@ -1,26 +1,9 @@
export {
ISubscription,
IPublisher,
Publisher,
} from './lib/Publisher';
export {
default as ISubscriber,
LEvent,
} from './lib/Subscriber';
export {
default as Rung,
RungOptions,
} from './lib/Rung';
export {
default as Capsule,
} from './lib/Capsule';
export {
bootstrap,
frag,
h,
q,
} from './lib/helpers';
import "./lib/jsxFactory";
export type { ISubscription, IPublisher } from './lib/Publisher';
export { Publisher } from './lib/Publisher';
export type { default as ISubscriber, LEvent } from './lib/Subscriber';
export { default as Rung } from './lib/Rung';
export type { RungOptions } from './lib/Rung';
export { default as Capsule } from './lib/Capsule';
export type { ICapsule } from './lib/Capsule';
export { bootstrap, frag, h, q } from './lib/helpers';

View File

@@ -1,4 +0,0 @@
import { h, frag, q } from "./index";
import "./jsxFactory";
const MyCoolDiv = () => <div>My Cool Div!</div>;

13
jsxFactory.d.ts vendored
View File

@@ -1,13 +0,0 @@
import { IRenderAttributes } from './lib/helpers';
declare namespace JSX {
type Element = Node;
export interface AttributeCollection {
[name: string]: string | boolean | (() => any);
className: string;
}
type RenderAttributes = {
[TagName in keyof HTMLElementTagNameMap]: IRenderAttributes<TagName>;
};
export interface IntrinsicElements extends RenderAttributes {}
}

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 {}
}
}

View File

@@ -6,6 +6,7 @@
"author": "Daniel Ledda <dan.j.ledda@gmail.com>",
"license": "MIT",
"devDependencies": {
"typescript": "^4.7.2"
}
"typescript": "^4.7.2",
"vite": "^2.9.9"
},
}

49
test.tsx Normal file
View File

@@ -0,0 +1,49 @@
import { h, q, frag, bootstrap, Rung, Capsule } from "./index";
import {SubNode} from "./lib/helpers";
const MyCoolDiv = (props: { isRed: boolean }, subNodes?: SubNode[]) => h("div", { classes: props.isRed ? ["red"] : [] }, ...subNodes ?? []);
class App extends Rung {
private counter = Capsule.new<number>(0);
private rungs = Capsule.new<HTMLDivElement | null>(null);
constructor() {
super({});
this.counter.watch((count) => {
if (this.rungs.val) {
this.rungs.val.replaceChildren(
...new Array(count).fill(null).map((_, i) => {
return <div className={'rung'}/>;
})
);
}
});
}
build() {
return <>
<style>{`
.rung {
width: 30px;
height: 30px;
border: solid black;
border-width: 0 2px 2px 2px;
}
.rung:last-of-type {
border-width: 0 2px 0 2px;
}
.rung:first-of-type {
border-width: 0 2px 2px 2px;
}
`}</style>
<h1>Ladder</h1>
<button onclick={() => this.counter.val--}>-</button>
<span>{this.counter}</span>
<button onclick={() => this.counter.val++}>+</button>
<div saveTo={this.rungs}/>
</>;
}
}
const app = new App();
bootstrap(app, "app");