feat: adding support for jsx
This commit is contained in:
@@ -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
4
jsx-test.tsx
Normal 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
13
jsxFactory.d.ts
vendored
Normal 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 {}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export class Publisher<EventType extends LEvent, PublisherType> implements IPubl
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IPublisher<T extends LEvent> {
|
export interface IPublisher<T extends LEvent> {
|
||||||
addSubscriber(subscriber: ISubscriber<T>, subscribeTo: T | T[]): {unbind: () => void};
|
addSubscriber(subscriber: ISubscriber<T>, subscribeTo: T | T[]): { unbind: () => void };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISubscription {
|
export interface ISubscription {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user