feat: added jsx support and some tests
This commit is contained in:
11
index.html
Normal file
11
index.html
Normal 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>
|
||||
35
index.ts
35
index.ts
@@ -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';
|
||||
@@ -1,4 +0,0 @@
|
||||
import { h, frag, q } from "./index";
|
||||
import "./jsxFactory";
|
||||
|
||||
const MyCoolDiv = () => <div>My Cool Div!</div>;
|
||||
13
jsxFactory.d.ts
vendored
13
jsxFactory.d.ts
vendored
@@ -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 {}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
26
lib/Rung.ts
26
lib/Rung.ts
@@ -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;
|
||||
|
||||
@@ -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
27
lib/jsxFactory.ts
Normal 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 {}
|
||||
}
|
||||
}
|
||||
@@ -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
49
test.tsx
Normal 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");
|
||||
Reference in New Issue
Block a user