diff --git a/README.md b/README.md
index 4ad2f07..4fbc073 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,7 @@
# Ladder
-Most libraries give you a framework. This is just a ladder.
\ No newline at end of file
+Most libraries give you a framework. This is just a ladder.
+
+## What's in the box?
+
+- JSX-friendly
\ No newline at end of file
diff --git a/index.ts b/index.ts
index 72a9ade..b17d0cf 100644
--- a/index.ts
+++ b/index.ts
@@ -23,4 +23,4 @@ export {
frag,
h,
q,
-} from './lib/helpers';
+} from './lib/helpers';
\ No newline at end of file
diff --git a/jsx-test.tsx b/jsx-test.tsx
new file mode 100644
index 0000000..6fe636b
--- /dev/null
+++ b/jsx-test.tsx
@@ -0,0 +1,4 @@
+import { h, frag, q } from "./index";
+import "./jsxFactory";
+
+const MyCoolDiv = () =>
My Cool Div!
;
\ No newline at end of file
diff --git a/jsxFactory.d.ts b/jsxFactory.d.ts
new file mode 100644
index 0000000..5de0bc5
--- /dev/null
+++ b/jsxFactory.d.ts
@@ -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;
+ };
+ export interface IntrinsicElements extends RenderAttributes {}
+}
\ No newline at end of file
diff --git a/lib/Capsule.ts b/lib/Capsule.ts
index 295b03f..3015e5b 100644
--- a/lib/Capsule.ts
+++ b/lib/Capsule.ts
@@ -1,5 +1,23 @@
import { ISubscription } from "./Publisher";
+export interface Stringable {
+ toString(): string;
+}
+
+export type Captable = Stringable | string | null;
+
+export interface ICapsule {
+ 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 {
private unbindCallback?: () => void;
@@ -12,13 +30,7 @@ class CapsuleSubscription implements ISubscription {
}
}
-interface Stringable {
- toString(): string;
-}
-type Captable = Stringable | string | null;
-export type MaybeCapsule = T | Capsule;
-
-export default class Capsule {
+export default class Capsule implements ICapsule {
private watchers: Array<(newVal: T) => void> | null = null;
private afterWatchers: Array<(newVal: T) => void> | null = null;
private value: T;
@@ -30,7 +42,7 @@ export default class Capsule {
this.isString = typeof val === "string";
}
- static new(val: MaybeCapsule): Capsule {
+ static new(val: T | Capsule): Capsule {
if (val instanceof Capsule) {
return val;
} else {
@@ -84,12 +96,11 @@ export default class Capsule {
}
toString(): string {
+ if (this.isString) {
+ return this.value as unknown as string;
+ }
if (!this.asString) {
- if (this.isString) {
- return this.val as unknown as string;
- } else {
- this.asString = this.val?.toString() ?? "null";
- }
+ this.asString = this.val?.toString() ?? "null";
}
return this.asString;
}
diff --git a/lib/Publisher.ts b/lib/Publisher.ts
index dadd1b8..d40c4ab 100644
--- a/lib/Publisher.ts
+++ b/lib/Publisher.ts
@@ -82,7 +82,7 @@ export class Publisher implements IPubl
}
export interface IPublisher {
- addSubscriber(subscriber: ISubscriber, subscribeTo: T | T[]): {unbind: () => void};
+ addSubscriber(subscriber: ISubscriber, subscribeTo: T | T[]): { unbind: () => void };
}
export interface ISubscription {
diff --git a/lib/Rung.ts b/lib/Rung.ts
index aab9a08..d71b015 100644
--- a/lib/Rung.ts
+++ b/lib/Rung.ts
@@ -1,3 +1,5 @@
+import {SubNode} from "./helpers";
+
export type RungOptions = {};
export default abstract class Rung {
@@ -32,3 +34,5 @@ export default abstract class Rung {
protected abstract build(): HTMLElement;
}
+
+export type FunctionalRung, N extends Node> = (attributes: Props, subNodes: SubNode[]) => N;
\ No newline at end of file
diff --git a/lib/helpers.ts b/lib/helpers.ts
index c0765c7..95ff38d 100644
--- a/lib/helpers.ts
+++ b/lib/helpers.ts
@@ -1,12 +1,12 @@
-import Capsule from "./Capsule";
+import {isCapsule, ICapsule} from "./Capsule";
import {ISubscription} from "./Publisher";
-import Rung from "./Rung";
+import Rung, {FunctionalRung} from "./Rung";
-type IRenderAttributes = Partial<{
- [K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | Capsule
+export type IRenderAttributes = Partial<{
+ [K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | ICapsule
}> & {
classes?: string[],
- saveTo?: Capsule,
+ saveTo?: ICapsule,
};
type IdSelector = `#${ string }`;
@@ -32,7 +32,32 @@ export function q(text: string): Text {
return document.createTextNode(text);
}
-export function h(type: T, attributes?: IRenderAttributes, subNodes?: (Rung | Node | Capsule)[]): HTMLElementTagNameMap[T] {
+type InstantiationType = FunctionalRung | keyof HTMLElementTagNameMap;
+
+type Props =
+ T extends FunctionalRung
+ ? Attributes
+ : T extends keyof HTMLElementTagNameMap
+ ? IRenderAttributes
+ : never;
+
+export type SubNode = Rung | Node | ICapsule;
+
+export function h(type: T, attributes?: Props, ...subNodes: SubNode[]): HTMLElementTagNameMap[T];
+export function h, U extends Props, V extends ReturnType>(type: T, attributes?: U, ...subNodes: SubNode[]): V;
+export function h(type: T, attributes?: Props | null, ...subNodes: SubNode[]) {
+ if (typeof type === "function") {
+ return type(attributes, subNodes);
+ } else {
+ return createStandardElement(type, attributes ?? {}, subNodes);
+ }
+}
+
+function createStandardElement(
+ type: T,
+ attributes: IRenderAttributes | null,
+ subNodes: SubNode[]
+): HTMLElementTagNameMap[T] {
const element = document.createElement(type);
if (attributes) {
if (attributes.classes) {
@@ -43,13 +68,11 @@ export function h(type: T, attributes?: I
}
applyAttributes(element, attributes);
}
- if (subNodes) {
- attachSubs(element, subNodes);
- }
+ attachSubs(element, subNodes);
return element;
}
-function nodeCapsuleWatcher(newVal: T extends Capsule ? U : never, textNode: Text, sub: ISubscription): void {
+function nodeCapsuleWatcher(newVal: T extends ICapsule ? U : never, textNode: Text, sub: ISubscription): void {
if (!textNode.parentNode) {
sub.unbind();
textNode.remove();
@@ -58,14 +81,14 @@ function nodeCapsuleWatcher(newVal: T extends Capsule ? 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++) {
const subNode = subNodes[i];
if (subNode instanceof Rung) {
node.append(subNode.render());
- } else if (subNode instanceof Capsule) {
+ } else if (isCapsule(subNode)) {
const textNode = q(subNode.toString());
- const sub = subNode.watch((newVal) => nodeCapsuleWatcher(newVal, textNode, sub));
+ const sub = subNode.watch((newVal) => nodeCapsuleWatcher(newVal, textNode, sub));
node.append(textNode);
} else {
node.append(subNode);
@@ -78,8 +101,8 @@ function applyAttributes(element: HTMLEle
if (Object.prototype.hasOwnProperty.call(attributes, key)) {
const attribute = (attributes as Record)[key];
if (attribute) {
- if (attribute instanceof Capsule) {
- const attributeAsCapsule = attribute as Capsule;
+ if (isCapsule(attribute)) {
+ const attributeAsCapsule = attribute as ICapsule;
const elementWithAttributeKey = element as unknown as Record;
elementWithAttributeKey[key] = attributeAsCapsule.val;
attribute.watch((newVal) => elementWithAttributeKey[key] = newVal);
diff --git a/tsconfig.json b/tsconfig.json
index 1193e0e..7fb514a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2020",
- "sourceMap": true
+ "sourceMap": true,
+ "strict": true,
+ "jsx": "react",
+ "jsxFactory": "h",
+ "jsxFragmentFactory": "frag"
},
"exclude": [
"node_modules"