campfire.js
Version:
a cozy web framework
205 lines (204 loc) • 7.88 kB
TypeScript
import { NuBuilder } from "./dom/NuBuilder.ts";
import type { CfHTMLElementInterface } from "./dom/config.ts";
import { Store } from "./stores/mod.ts";
export interface UpdateEvent<ST> {
type: "update";
value: ST extends Store<infer T> ? T : never;
}
export type AppendEvent<ST> = ST extends Store<infer T> ? (T extends Array<infer L> ? {
type: "append";
value: L;
idx: number;
} : {
type: "append";
value: T;
}) : never;
export type ChangeEvent<ST> = ST extends Store<infer T> ? (T extends Record<string, infer M> ? {
type: "change";
value: M;
key: string;
} : (T extends Array<infer L> ? {
type: "change";
value: L;
idx: number;
} : {
type: "change";
value: T;
})) : never;
export type DeletionEvent<ST> = ST extends Store<infer T> ? T extends Record<string, infer M> ? {
type: "deletion";
value: M;
key: string;
} : T extends Array<infer L> ? {
type: "deletion";
value: L;
idx: number;
} : {
type: "deletion";
value: T;
} : never;
export interface ClearEvent {
type: "clear";
}
export type StoreEvent<ST> = ChangeEvent<ST> | DeletionEvent<ST> | AppendEvent<ST> | UpdateEvent<ST> | ClearEvent;
export type EventType = StoreEvent<any>["type"];
export type SubscriberTypeMap<ST> = {
[K in EventType]: Extract<StoreEvent<ST>, {
type: K;
}>;
};
/** A signature for a subscriber function. */
export type AnySubscriber<ST> = (event: StoreEvent<ST>) => void;
/** A signature for a subscriber of a specific event. */
export type EventSubscriber<K extends EventType, ST> = (event: SubscriberTypeMap<ST>[K]) => void;
/** The function signature for a function returned by `template()`. */
export type Template = (e: Record<string, any>) => string;
export type UnwrapStore<D> = {
[K in keyof D]: D[K] extends Store<infer V> ? V : never;
};
export type StoreEventFromObject<D> = {
[K in keyof D]: D[K] extends Store<any> ? StoreEvent<D[K]> : never;
}[keyof D];
type NuBuilderInstance<Elem extends HTMLElement, Deps extends Record<string, Store<any>>> = NuBuilder<Elem, Deps, string>;
export type RenderBuilder<Elem extends HTMLElement, Deps> = Omit<NuBuilderInstance<Elem, Deps extends Record<string, Store<any>> ? Deps : never>, "children" | "done" | "ref" | "on" | "gimme" | "deps" | "render">;
/**
* Function signature for rendering reactive content.
*
* @template Elem The type of HTML element being rendered
* @template Deps The type of the dependencies object
*
* @param props The unwrapped values from the store dependencies
* @param opts Additional options and utilities for rendering:
* - event: The event that triggered this render (if applicable)
* - b: A builder instance for fluent element modification
* - elt: Reference to the element being rendered
* - first: Boolean flag indicating whether this is the first render (true) or a re-render (false)
*
* @returns A string, builder instance, or void
*
* @example
* ```js
* // Using the first parameter to conditionally render initial state
* const [element] = cf.nu("div")
* .deps({ count: counterStore })
* .render(({ count }, { b, first }) => {
* // Do one-time setup on first render
* if (first) {
* b.attr("data-initialized", "true");
* b.style("transition", "all 0.3s ease");
* }
*
* return b.content(`Count is: ${count}`);
* })
* .done();
* ```
*/
export type RenderFunction<Elem extends HTMLElement, Deps extends Record<string, Store<any>>> = (props: UnwrapStore<Deps>, opts: {
event?: StoreEventFromObject<Deps> & {
triggeredBy: string;
};
b: RenderBuilder<Elem, Deps>;
elt: Elem;
first: boolean;
}) => string | RenderBuilder<Elem, Deps> | void;
export type StringStyleProps = keyof {
[K in keyof CSSStyleDeclaration as CSSStyleDeclaration[K] extends string ? K : never]: true;
};
export type DOMEventHandlers = {
[K in keyof HTMLElementEventMap]?: (event: HTMLElementEventMap[K]) => void;
};
/**
* Properties for the HTML element to be created.
*/
export interface ElementProperties<T extends HTMLElement, D extends Record<string, Store<any>>> {
/**
* String that will be set as the inner HTML of the created element. By default,
* this is escaped using cf.escape() - however, if you supply `raw: true` in
* the args object passed as nu's second argument, escaping is disabled.
*/
contents?: string;
/**
* Rendering function to use to update the element on redraws.
* As of 4.0.0-rc15, render functions receive a 'first' parameter in the opts
* object that indicates if this is the first render (true) or a re-render (false).
*/
render?: RenderFunction<T, D>;
/**
* Classes for the newly created element. Will be combined with whatever is
* passed into nu().
*/
classes?: Record<string, boolean>;
/**
* Whether or not to escape the `contents` string. If `raw` is true, the
* string is not escaped.
*/
raw?: boolean;
/** Miscellaneous properties to set on the created element,
* for example, `type: "button"` or `checked: true`
*/
misc?: Record<string, unknown>;
/**
* Contains styles that will be applied to the new element. Property names
* must be the same as those in `CSSStyleDeclaration`.
*/
style?: Partial<Record<StringStyleProps, string | number>>;
/**
* An object containing event handlers that will be applied using addEventListener.
* For example: `{'click': (e) => console.log(e)}`
*/
on?: DOMEventHandlers;
/** Attributes that will be set on the element using `Element.setAttribute`. */
attrs?: Record<string, string | number | boolean>;
/**
* A list of elements to query from the element. Will be returned as
* subsequent members of the returned Array after the element itself.
* Returns null when a selector isn't found to preserve order of returned
* elements.
*/
gimme?: string[];
/**
* A Record<string, Store> of the element's dependencies. The element's
* render function will be called every time any of the deps change.
*/
deps?: D;
/**
* Children of the element to mount. They will be mounted into `cf-slot`s
* corresponding to the Record's keys and preserved between re-renders of
* the parent.
*
* As of 4.0.0-rc15, you can provide an array of elements for a slot, and they
* will all be mounted to that slot in order.
*
* Exercise caution when passing nu().done() directly - all children
* returned will be mounted to the element.
*/
children?: Record<string, CfHTMLElementInterface | CfHTMLElementInterface[]>;
/**
* A user-supplied ID to track the element by its reference.
*/
track?: string;
}
/**
* An interface to store data parsed from an element descriptor string passed to `nu`.
* @internal
*/
export interface TagStringParseResult {
/** The tagName parsed from the info string. */
tag?: string | undefined;
/** The id parsed from the info string. */
id?: string | undefined;
/** An array of classes parsed from the info string. */
classes?: string[] | undefined;
}
export type ElementPosition = {
before: CfHTMLElementInterface;
} | {
after: CfHTMLElementInterface;
} | {
into: CfHTMLElementInterface;
at?: "start";
};
type TagName = keyof HTMLElementTagNameMap;
export type EltInfoToTag<T extends string> = T extends `${infer Tag extends TagName}#${string}.${string}` ? Tag : T extends `${infer Tag extends TagName}#${string}` ? Tag : T extends `${infer Tag extends TagName}.${string}` ? Tag : T extends `${infer Tag extends TagName}` ? Tag : 'div';
export type InferElementType<T extends string> = T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[EltInfoToTag<T>] : HTMLElement;
export {};