feat: adding support for jsx

This commit is contained in:
Daniel Ledda
2022-05-26 15:11:17 +02:00
parent 2e8e3c858a
commit 182c38232e
9 changed files with 95 additions and 32 deletions

View File

@@ -1,3 +1,7 @@
# Ladder # Ladder
Most libraries give you a framework. This is just a ladder. Most libraries give you a framework. This is just a ladder.
## What's in the box?
- JSX-friendly

4
jsx-test.tsx Normal file
View File

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

13
jsxFactory.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
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

@@ -1,5 +1,23 @@
import { ISubscription } from "./Publisher"; import { ISubscription } from "./Publisher";
export interface Stringable {
toString(): string;
}
export type Captable = Stringable | string | null;
export interface ICapsule<T extends Captable = Captable> {
watch(watcher: (newVal: T) => void, after?: boolean): ISubscription;
toString(): string;
val: T;
}
export function isCapsule(maybeCapsule: any): maybeCapsule is ICapsule {
return Object.prototype.hasOwnProperty.call(maybeCapsule, 'val')
&& typeof maybeCapsule.watch === "function"
&& typeof maybeCapsule.toString === "function";
}
class CapsuleSubscription implements ISubscription { class CapsuleSubscription implements ISubscription {
private unbindCallback?: () => void; private unbindCallback?: () => void;
@@ -12,13 +30,7 @@ class CapsuleSubscription implements ISubscription {
} }
} }
interface Stringable { export default class Capsule<T extends Captable = Captable> implements ICapsule {
toString(): string;
}
type Captable = Stringable | string | null;
export type MaybeCapsule<T> = T | Capsule<T>;
export default class Capsule<T extends Captable = Captable> {
private watchers: Array<(newVal: T) => void> | null = null; private watchers: Array<(newVal: T) => void> | null = null;
private afterWatchers: Array<(newVal: T) => void> | null = null; private afterWatchers: Array<(newVal: T) => void> | null = null;
private value: T; private value: T;
@@ -30,7 +42,7 @@ export default class Capsule<T extends Captable = Captable> {
this.isString = typeof val === "string"; this.isString = typeof val === "string";
} }
static new<T extends Captable>(val: MaybeCapsule<T>): Capsule<T> { static new<T extends Captable>(val: T | Capsule<T>): Capsule<T> {
if (val instanceof Capsule) { if (val instanceof Capsule) {
return val; return val;
} else { } else {
@@ -84,12 +96,11 @@ export default class Capsule<T extends Captable = Captable> {
} }
toString(): string { toString(): string {
if (!this.asString) {
if (this.isString) { if (this.isString) {
return this.val as unknown as string; return this.value as unknown as string;
} else {
this.asString = this.val?.toString() ?? "null";
} }
if (!this.asString) {
this.asString = this.val?.toString() ?? "null";
} }
return this.asString; return this.asString;
} }

View File

@@ -1,3 +1,5 @@
import {SubNode} from "./helpers";
export type RungOptions = {}; export type RungOptions = {};
export default abstract class Rung { export default abstract class Rung {
@@ -32,3 +34,5 @@ export default abstract class Rung {
protected abstract build(): HTMLElement; protected abstract build(): HTMLElement;
} }
export type FunctionalRung<Props extends Record<string, any>, N extends Node> = (attributes: Props, subNodes: SubNode[]) => N;

View File

@@ -1,12 +1,12 @@
import Capsule from "./Capsule"; import {isCapsule, ICapsule} from "./Capsule";
import {ISubscription} from "./Publisher"; import {ISubscription} from "./Publisher";
import Rung from "./Rung"; import Rung, {FunctionalRung} from "./Rung";
type IRenderAttributes<T extends keyof HTMLElementTagNameMap> = Partial<{ export type IRenderAttributes<T extends keyof HTMLElementTagNameMap> = Partial<{
[K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | Capsule<HTMLElementTagNameMap[T][K]> [K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | ICapsule<HTMLElementTagNameMap[T][K]>
}> & { }> & {
classes?: string[], classes?: string[],
saveTo?: Capsule<HTMLElementTagNameMap[T] | null>, saveTo?: ICapsule<HTMLElementTagNameMap[T] | null>,
}; };
type IdSelector = `#${ string }`; type IdSelector = `#${ string }`;
@@ -32,7 +32,32 @@ export function q(text: string): Text {
return document.createTextNode(text); return document.createTextNode(text);
} }
export function h<T extends keyof HTMLElementTagNameMap>(type: T, attributes?: IRenderAttributes<T>, subNodes?: (Rung | Node | Capsule)[]): HTMLElementTagNameMap[T] { type InstantiationType = FunctionalRung<any, any> | keyof HTMLElementTagNameMap;
type Props<T> =
T extends FunctionalRung<infer Attributes, infer Return>
? Attributes
: T extends keyof HTMLElementTagNameMap
? IRenderAttributes<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 InstantiationType>(type: T, attributes?: Props<T> | null, ...subNodes: SubNode[]) {
if (typeof type === "function") {
return type(attributes, subNodes);
} else {
return createStandardElement(type, attributes ?? {}, subNodes);
}
}
function createStandardElement<T extends keyof HTMLElementTagNameMap>(
type: T,
attributes: IRenderAttributes<T> | null,
subNodes: SubNode[]
): HTMLElementTagNameMap[T] {
const element = document.createElement(type); const element = document.createElement(type);
if (attributes) { if (attributes) {
if (attributes.classes) { if (attributes.classes) {
@@ -43,13 +68,11 @@ export function h<T extends keyof HTMLElementTagNameMap>(type: T, attributes?: I
} }
applyAttributes(element, attributes); applyAttributes(element, attributes);
} }
if (subNodes) {
attachSubs(element, subNodes); attachSubs(element, subNodes);
}
return element; return element;
} }
function nodeCapsuleWatcher<T>(newVal: T extends Capsule<infer U> ? U : never, textNode: Text, sub: ISubscription): void { function nodeCapsuleWatcher<T>(newVal: T extends ICapsule<infer U> ? U : never, textNode: Text, sub: ISubscription): void {
if (!textNode.parentNode) { if (!textNode.parentNode) {
sub.unbind(); sub.unbind();
textNode.remove(); textNode.remove();
@@ -58,14 +81,14 @@ function nodeCapsuleWatcher<T>(newVal: T extends Capsule<infer U> ? U : never, t
} }
} }
function attachSubs(node: Element | DocumentFragment, subNodes: (Rung | Node | Capsule)[]): void { function attachSubs(node: Element | DocumentFragment, subNodes: SubNode[]): void {
for (let i = 0; i < subNodes.length; i++) { for (let i = 0; i < subNodes.length; i++) {
const subNode = subNodes[i]; const subNode = subNodes[i];
if (subNode instanceof Rung) { if (subNode instanceof Rung) {
node.append(subNode.render()); node.append(subNode.render());
} else if (subNode instanceof Capsule) { } else if (isCapsule(subNode)) {
const textNode = q(subNode.toString()); const textNode = q(subNode.toString());
const sub = subNode.watch((newVal) => nodeCapsuleWatcher<Capsule>(newVal, textNode, sub)); const sub = subNode.watch((newVal) => nodeCapsuleWatcher<ICapsule>(newVal, textNode, sub));
node.append(textNode); node.append(textNode);
} else { } else {
node.append(subNode); node.append(subNode);
@@ -78,8 +101,8 @@ function applyAttributes<T extends keyof HTMLElementTagNameMap>(element: HTMLEle
if (Object.prototype.hasOwnProperty.call(attributes, key)) { if (Object.prototype.hasOwnProperty.call(attributes, key)) {
const attribute = (attributes as Record<string, unknown>)[key]; const attribute = (attributes as Record<string, unknown>)[key];
if (attribute) { if (attribute) {
if (attribute instanceof Capsule) { if (isCapsule(attribute)) {
const attributeAsCapsule = attribute as Capsule; const attributeAsCapsule = attribute as ICapsule;
const elementWithAttributeKey = element as unknown as Record<string, typeof attributeAsCapsule.val>; const elementWithAttributeKey = element as unknown as Record<string, typeof attributeAsCapsule.val>;
elementWithAttributeKey[key] = attributeAsCapsule.val; elementWithAttributeKey[key] = attributeAsCapsule.val;
attribute.watch((newVal) => elementWithAttributeKey[key] = newVal); attribute.watch((newVal) => elementWithAttributeKey[key] = newVal);

View File

@@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"sourceMap": true "sourceMap": true,
"strict": true,
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "frag"
}, },
"exclude": [ "exclude": [
"node_modules" "node_modules"