@seahax/elemental
Advanced tools
+5
-5
| type Alpha = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'; | ||
| type ChildValue = Node | string | number | bigint | false | null | undefined; | ||
| type AttrValue = string | number | bigint | boolean | null | undefined; | ||
| type TagMap = HTMLElementTagNameMap & HTMLElementDeprecatedTagNameMap; | ||
| type IfEquals<T, U, Y = unknown, N = never> = (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2 ? Y : N; | ||
| type ElementType<TElement> = TElement extends string ? TElement extends keyof TagMap ? TagMap[TElement] : HTMLElement : TElement extends CustomElementConstructor ? InstanceType<TElement> : TElement extends Element | Document | ShadowRoot ? TElement : never; | ||
| type ElementProps<TElement> = { | ||
| type Props<TElement> = { | ||
| readonly [P in keyof TElement & string as TElement[P] extends ((...args: any[]) => any) | null | undefined ? never : IfEquals<Record<P, TElement[P]>, Pick<TElement, P>, `:${P}`, never>]?: TElement[P]; | ||
| }; | ||
| type ElementAttrs<TElement> = TElement extends string | CustomElementConstructor | Element ? Readonly<Record<`${Alpha}${string}`, AttrValue>> : {}; | ||
| export type HtmlConfig<TElement> = ElementAttrs<TElement> & ElementProps<ElementType<TElement>>; | ||
| type Attrs<TElement> = TElement extends string | CustomElementConstructor | Element ? Readonly<Record<`${Alpha}${string}`, AttrValue>> : {}; | ||
| export type ChildValue = Node | string | number | bigint | false | null | undefined; | ||
| export type HtmlConfig<TElement> = Attrs<TElement> & Props<ElementType<TElement>>; | ||
| export type ElementType<TElement> = TElement extends string ? TElement extends keyof TagMap ? TagMap[TElement] : HTMLElement : TElement extends CustomElementConstructor ? InstanceType<TElement> : TElement extends Element | Document | ShadowRoot ? TElement : never; | ||
| /** | ||
@@ -13,0 +13,0 @@ * Create or update an HTML element. |
+1
-1
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"html.js","names":[],"sources":["../src/html.ts"],"sourcesContent":["// prettier-ignore\ntype Alpha = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';\ntype ChildValue = Node | string | number | bigint | false | null | undefined;\ntype AttrValue = string | number | bigint | boolean | null | undefined;\ntype TagMap = HTMLElementTagNameMap & HTMLElementDeprecatedTagNameMap;\n\ntype IfEquals<T, U, Y = unknown, N = never> =\n (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2 ? Y : N;\n\ntype ElementType<TElement> = TElement extends string\n ? TElement extends keyof TagMap\n ? TagMap[TElement]\n : HTMLElement\n : TElement extends CustomElementConstructor\n ? InstanceType<TElement>\n : TElement extends Element | Document | ShadowRoot\n ? TElement\n : never;\n\ntype ElementProps<TElement> = {\n readonly [P in keyof TElement & string as TElement[P] extends ((...args: any[]) => any) | null | undefined\n ? never\n : IfEquals<Record<P, TElement[P]>, Pick<TElement, P>, `:${P}`, never>]?: TElement[P];\n};\n\ntype ElementAttrs<TElement> = TElement extends string | CustomElementConstructor | Element\n ? Readonly<Record<`${Alpha}${string}`, AttrValue>>\n : {};\n\nexport type HtmlConfig<TElement> = ElementAttrs<TElement> & ElementProps<ElementType<TElement>>;\n\n/**\n * Create or update an HTML element.\n */\nexport function html<\n TElement extends keyof TagMap | (string & {}) | CustomElementConstructor | Element | Document | ShadowRoot,\n>(el: TElement, config?: HtmlConfig<TElement>, children?: readonly ChildValue[]): ElementType<NoInfer<TElement>>;\nexport function html<\n TElement extends keyof TagMap | (string & {}) | CustomElementConstructor | Element | Document | ShadowRoot,\n>(el: TElement, children?: readonly ChildValue[]): ElementType<TElement>;\n\nexport function html(\n el: string | CustomElementConstructor | Element | Document | ShadowRoot,\n configOrChildren: readonly ChildValue[] | Readonly<Record<string, any>> = {},\n children?: readonly ChildValue[],\n): Node {\n if (typeof el === 'function') {\n let name = customElements.getName(el);\n\n if (name == null) {\n name = `ce-${crypto.randomUUID()}`;\n customElements.define(name, el);\n }\n\n el = name;\n }\n\n if (typeof el === 'string') el = document.createElement(el);\n\n let config: Readonly<Record<string, any>>;\n [config, children] = Array.isArray(configOrChildren)\n ? [{}, configOrChildren as readonly ChildValue[]]\n : [configOrChildren as Readonly<Record<string, any>>, children];\n\n if ('setAttribute' in el) {\n for (const [name, rawValue] of Object.entries(config)) {\n if (rawValue === undefined || name.startsWith(':')) continue;\n\n if (rawValue === null || rawValue === false) {\n if (el.hasAttribute(name)) el.removeAttribute(name);\n } else {\n const value = rawValue == true ? '' : String(rawValue);\n if (el.getAttribute(name) !== value) {\n try {\n el.setAttribute(name, value);\n } catch (error) {\n console.warn(error);\n }\n }\n }\n }\n }\n\n for (const [rawName, value] of Object.entries(config)) {\n if (value === undefined || !rawName.startsWith(':')) continue;\n const name = rawName.slice(1);\n if (Reflect.get(el, name) !== value) Reflect.set(el, name, value);\n }\n\n if (children) {\n const childNodes = children\n .filter((child) => child != null && child !== false)\n .map((child) => (typeof child !== 'object' ? document.createTextNode(String(child)) : child));\n\n el.replaceChildren(...childNodes);\n }\n\n return el;\n}\n"],"mappings":";AAyCA,SAAgB,EACd,GACA,IAA0E,EAAE,EAC5E,GACM;AACN,KAAI,OAAO,KAAO,YAAY;EAC5B,IAAI,IAAO,eAAe,QAAQ,EAAG;AAOrC,EALI,MACF,IAAO,MAAM,OAAO,YAAY,IAChC,eAAe,OAAO,GAAM,EAAG,GAGjC,IAAK;;AAGP,CAAI,OAAO,KAAO,aAAU,IAAK,SAAS,cAAc,EAAG;CAE3D,IAAI;AAKJ,KAJA,CAAC,GAAQ,KAAY,MAAM,QAAQ,EAAiB,GAChD,CAAC,EAAE,EAAE,EAA0C,GAC/C,CAAC,GAAmD,EAAS,EAE7D,kBAAkB,GACpB;OAAK,IAAM,CAAC,GAAM,MAAa,OAAO,QAAQ,EAAO,CAC/C,aAAa,KAAA,KAAa,EAAK,WAAW,IAAI,EAElD,KAAI,MAAa,QAAQ,MAAa,IAChC,EAAG,aAAa,EAAK,IAAE,EAAG,gBAAgB,EAAK;OAC9C;GACL,IAAM,IAAQ,KAAY,IAAO,KAAK,OAAO,EAAS;AACtD,OAAI,EAAG,aAAa,EAAK,KAAK,EAC5B,KAAI;AACF,MAAG,aAAa,GAAM,EAAM;YACrB,GAAO;AACd,YAAQ,KAAK,EAAM;;;;AAO7B,MAAK,IAAM,CAAC,GAAS,MAAU,OAAO,QAAQ,EAAO,EAAE;AACrD,MAAI,MAAU,KAAA,KAAa,CAAC,EAAQ,WAAW,IAAI,CAAE;EACrD,IAAM,IAAO,EAAQ,MAAM,EAAE;AAC7B,EAAI,QAAQ,IAAI,GAAI,EAAK,KAAK,KAAO,QAAQ,IAAI,GAAI,GAAM,EAAM;;AAGnE,KAAI,GAAU;EACZ,IAAM,IAAa,EAChB,QAAQ,MAAU,KAAS,QAAQ,MAAU,GAAM,CACnD,KAAK,MAAW,OAAO,KAAU,WAAoD,IAAzC,SAAS,eAAe,OAAO,EAAM,CAAC,CAAU;AAE/F,IAAG,gBAAgB,GAAG,EAAW;;AAGnC,QAAO"} | ||
| {"version":3,"file":"html.js","names":[],"sources":["../src/html.ts"],"sourcesContent":["// prettier-ignore\ntype Alpha = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';\ntype AttrValue = string | number | bigint | boolean | null | undefined;\ntype TagMap = HTMLElementTagNameMap & HTMLElementDeprecatedTagNameMap;\n\ntype IfEquals<T, U, Y = unknown, N = never> =\n (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2 ? Y : N;\n\ntype Props<TElement> = {\n readonly [P in keyof TElement & string as TElement[P] extends ((...args: any[]) => any) | null | undefined\n ? never\n : IfEquals<Record<P, TElement[P]>, Pick<TElement, P>, `:${P}`, never>]?: TElement[P];\n};\n\ntype Attrs<TElement> = TElement extends string | CustomElementConstructor | Element\n ? Readonly<Record<`${Alpha}${string}`, AttrValue>>\n : {};\n\nexport type ChildValue = Node | string | number | bigint | false | null | undefined;\n\nexport type HtmlConfig<TElement> = Attrs<TElement> & Props<ElementType<TElement>>;\n\nexport type ElementType<TElement> = TElement extends string\n ? TElement extends keyof TagMap\n ? TagMap[TElement]\n : HTMLElement\n : TElement extends CustomElementConstructor\n ? InstanceType<TElement>\n : TElement extends Element | Document | ShadowRoot\n ? TElement\n : never;\n\n/**\n * Create or update an HTML element.\n */\nexport function html<\n TElement extends keyof TagMap | (string & {}) | CustomElementConstructor | Element | Document | ShadowRoot,\n>(el: TElement, config?: HtmlConfig<TElement>, children?: readonly ChildValue[]): ElementType<NoInfer<TElement>>;\nexport function html<\n TElement extends keyof TagMap | (string & {}) | CustomElementConstructor | Element | Document | ShadowRoot,\n>(el: TElement, children?: readonly ChildValue[]): ElementType<TElement>;\n\nexport function html(\n el: string | CustomElementConstructor | Element | Document | ShadowRoot,\n configOrChildren: readonly ChildValue[] | Readonly<Record<string, any>> = {},\n children?: readonly ChildValue[],\n): Node {\n if (typeof el === 'function') {\n let name = customElements.getName(el);\n\n if (name == null) {\n name = `ce-${crypto.randomUUID()}`;\n customElements.define(name, el);\n }\n\n el = name;\n }\n\n if (typeof el === 'string') el = document.createElement(el);\n\n let config: Readonly<Record<string, any>>;\n [config, children] = Array.isArray(configOrChildren)\n ? [{}, configOrChildren as readonly ChildValue[]]\n : [configOrChildren as Readonly<Record<string, any>>, children];\n\n if ('setAttribute' in el) {\n for (const [name, rawValue] of Object.entries(config)) {\n if (rawValue === undefined || name.startsWith(':')) continue;\n\n if (rawValue === null || rawValue === false) {\n if (el.hasAttribute(name)) el.removeAttribute(name);\n } else {\n const value = rawValue == true ? '' : String(rawValue);\n if (el.getAttribute(name) !== value) {\n try {\n el.setAttribute(name, value);\n } catch (error) {\n console.warn(error);\n }\n }\n }\n }\n }\n\n for (const [rawName, value] of Object.entries(config)) {\n if (value === undefined || !rawName.startsWith(':')) continue;\n const name = rawName.slice(1);\n if (Reflect.get(el, name) !== value) Reflect.set(el, name, value);\n }\n\n if (children) {\n const childNodes = children\n .filter((child) => child != null && child !== false)\n .map((child) => (typeof child !== 'object' ? document.createTextNode(String(child)) : child));\n\n el.replaceChildren(...childNodes);\n }\n\n return el;\n}\n"],"mappings":";AA0CA,SAAgB,EACd,GACA,IAA0E,EAAE,EAC5E,GACM;AACN,KAAI,OAAO,KAAO,YAAY;EAC5B,IAAI,IAAO,eAAe,QAAQ,EAAG;AAOrC,EALI,MACF,IAAO,MAAM,OAAO,YAAY,IAChC,eAAe,OAAO,GAAM,EAAG,GAGjC,IAAK;;AAGP,CAAI,OAAO,KAAO,aAAU,IAAK,SAAS,cAAc,EAAG;CAE3D,IAAI;AAKJ,KAJA,CAAC,GAAQ,KAAY,MAAM,QAAQ,EAAiB,GAChD,CAAC,EAAE,EAAE,EAA0C,GAC/C,CAAC,GAAmD,EAAS,EAE7D,kBAAkB,GACpB;OAAK,IAAM,CAAC,GAAM,MAAa,OAAO,QAAQ,EAAO,CAC/C,aAAa,KAAA,KAAa,EAAK,WAAW,IAAI,EAElD,KAAI,MAAa,QAAQ,MAAa,IAChC,EAAG,aAAa,EAAK,IAAE,EAAG,gBAAgB,EAAK;OAC9C;GACL,IAAM,IAAQ,KAAY,IAAO,KAAK,OAAO,EAAS;AACtD,OAAI,EAAG,aAAa,EAAK,KAAK,EAC5B,KAAI;AACF,MAAG,aAAa,GAAM,EAAM;YACrB,GAAO;AACd,YAAQ,KAAK,EAAM;;;;AAO7B,MAAK,IAAM,CAAC,GAAS,MAAU,OAAO,QAAQ,EAAO,EAAE;AACrD,MAAI,MAAU,KAAA,KAAa,CAAC,EAAQ,WAAW,IAAI,CAAE;EACrD,IAAM,IAAO,EAAQ,MAAM,EAAE;AAC7B,EAAI,QAAQ,IAAI,GAAI,EAAK,KAAK,KAAO,QAAQ,IAAI,GAAI,GAAM,EAAM;;AAGnE,KAAI,GAAU;EACZ,IAAM,IAAa,EAChB,QAAQ,MAAU,KAAS,QAAQ,MAAU,GAAM,CACnD,KAAK,MAAW,OAAO,KAAU,WAAoD,IAAzC,SAAS,eAAe,OAAO,EAAM,CAAC,CAAU;AAE/F,IAAG,gBAAgB,GAAG,EAAW;;AAGnC,QAAO"} |
@@ -5,4 +5,4 @@ //#region src/router/compareRouteMatches.ts | ||
| if (e === null || t === null || e.component !== t.component) return !1; | ||
| let n = Object.entries(e.attributes), r = new Map(Object.entries(t.attributes)); | ||
| return n.length === r.size && n.every(([e, t]) => r.has(e) && r.get(e) === t); | ||
| let n = Object.entries(e.config), r = new Map(Object.entries(t.config)); | ||
| return !(!(n.length === r.size && n.every(([e, t]) => r.has(e) && r.get(e) === t)) || !(e.children.length === t.children.length && e.children.every((e, n) => e === t.children[n]))); | ||
| } | ||
@@ -9,0 +9,0 @@ //#endregion |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"compareRouteMatches.js","names":[],"sources":["../../src/router/compareRouteMatches.ts"],"sourcesContent":["import type { RouteMatch } from './defineRouterComponent.ts';\n\nexport function compareRouteMatches(route: RouteMatch | null, oldRoute: RouteMatch | null): boolean {\n if (Object.is(route, oldRoute)) return true;\n if (route === null || oldRoute === null) return false;\n if (route.component !== oldRoute.component) return false;\n\n const attrEntries = Object.entries(route.attributes);\n const oldAttrEntries = new Map(Object.entries(oldRoute.attributes));\n\n const match =\n attrEntries.length === oldAttrEntries.size &&\n attrEntries.every(([key, value]) => oldAttrEntries.has(key) && oldAttrEntries.get(key) === value);\n\n return match;\n}\n"],"mappings":";AAEA,SAAgB,EAAoB,GAA0B,GAAsC;AAClG,KAAI,OAAO,GAAG,GAAO,EAAS,CAAE,QAAO;AAEvC,KADI,MAAU,QAAQ,MAAa,QAC/B,EAAM,cAAc,EAAS,UAAW,QAAO;CAEnD,IAAM,IAAc,OAAO,QAAQ,EAAM,WAAW,EAC9C,IAAiB,IAAI,IAAI,OAAO,QAAQ,EAAS,WAAW,CAAC;AAMnE,QAHE,EAAY,WAAW,EAAe,QACtC,EAAY,OAAO,CAAC,GAAK,OAAW,EAAe,IAAI,EAAI,IAAI,EAAe,IAAI,EAAI,KAAK,EAAM"} | ||
| {"version":3,"file":"compareRouteMatches.js","names":[],"sources":["../../src/router/compareRouteMatches.ts"],"sourcesContent":["import type { RouteMatch } from './defineRouterComponent.ts';\n\nexport function compareRouteMatches(route: RouteMatch | null, oldRoute: RouteMatch | null): boolean {\n if (Object.is(route, oldRoute)) return true;\n if (route === null || oldRoute === null) return false;\n if (route.component !== oldRoute.component) return false;\n\n const configEntries = Object.entries(route.config);\n const oldConfigEntries = new Map(Object.entries(oldRoute.config));\n const configsMatch =\n configEntries.length === oldConfigEntries.size &&\n configEntries.every(([key, value]) => oldConfigEntries.has(key) && oldConfigEntries.get(key) === value);\n if (!configsMatch) return false;\n\n const childrenMatch =\n route.children.length === oldRoute.children.length &&\n route.children.every((child, index) => child === oldRoute.children[index]);\n if (!childrenMatch) return false;\n\n return true;\n}\n"],"mappings":";AAEA,SAAgB,EAAoB,GAA0B,GAAsC;AAClG,KAAI,OAAO,GAAG,GAAO,EAAS,CAAE,QAAO;AAEvC,KADI,MAAU,QAAQ,MAAa,QAC/B,EAAM,cAAc,EAAS,UAAW,QAAO;CAEnD,IAAM,IAAgB,OAAO,QAAQ,EAAM,OAAO,EAC5C,IAAmB,IAAI,IAAI,OAAO,QAAQ,EAAS,OAAO,CAAC;AAWjE,QAFA,EALI,EAFF,EAAc,WAAW,EAAiB,QAC1C,EAAc,OAAO,CAAC,GAAK,OAAW,EAAiB,IAAI,EAAI,IAAI,EAAiB,IAAI,EAAI,KAAK,EAAM,KAMrG,EAFF,EAAM,SAAS,WAAW,EAAS,SAAS,UAC5C,EAAM,SAAS,OAAO,GAAO,MAAU,MAAU,EAAS,SAAS,GAAO"} |
| import { type ComponentConstructor, type ComponentOptions } from '../defineComponent.ts'; | ||
| import type { HtmlConfig } from '../html.ts'; | ||
| type RouteParamNames<T extends string> = T extends `${infer TPrefix}/${infer TSuffix}` ? TPrefix extends `${':' | '*'}${infer TName}` ? TName | RouteParamNames<TSuffix> : RouteParamNames<TSuffix> : T extends `${':' | '*'}${infer TName}` ? TName : never; | ||
@@ -9,2 +10,11 @@ export type RouteParams<T extends string> = string extends T ? Record<string, string> : { | ||
| readonly fallback?: CustomElementConstructor; | ||
| /** | ||
| * Component to render when an error is thrown by the `config` callback | ||
| * of a route, which indicates that a path or search parameter is invalid. | ||
| * | ||
| * - Child text contains the error message. | ||
| * - Attribute `data-error-name` contains the `Error` instance `name`. | ||
| * - Attribute `data-error-code` contains the `Error` instance `code`. | ||
| */ | ||
| readonly invalid?: CustomElementConstructor; | ||
| } | ||
@@ -18,5 +28,6 @@ export interface RouterBuilder { | ||
| * @param component Component to render when this route is matched. | ||
| * @param attributes Get attributes to be applied to the component. | ||
| * @param config Get the config (attributes and properties) to be applied to | ||
| * the component. | ||
| */ | ||
| readonly addRoute: <const TPattern extends `/${string}`>(pattern: TPattern, component: CustomElementConstructor, attributes?: (pathParams: RouteParams<TPattern>, searchParams: URLSearchParams) => Record<string, string>) => this; | ||
| readonly addRoute: <const TPattern extends `/${string}`, TComponent extends CustomElementConstructor>(pattern: TPattern, component: TComponent, config?: (pathParams: RouteParams<TPattern>, searchParams: URLSearchParams) => HtmlConfig<TComponent>) => this; | ||
| /** Build the router component class. */ | ||
@@ -23,0 +34,0 @@ readonly build: () => ComponentConstructor<{}>; |
@@ -10,3 +10,3 @@ import { createRouteMatcher as e } from "./createRouteMatcher.js"; | ||
| component: n, | ||
| attributes: a, | ||
| config: a, | ||
| matcher: o | ||
@@ -13,0 +13,0 @@ }), i; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"defineRouter.js","names":[],"sources":["../../src/router/defineRouter.ts"],"sourcesContent":["import { type ComponentConstructor, type ComponentOptions } from '../defineComponent.ts';\nimport { createRouteMatcher, type RouteMatcher } from './createRouteMatcher.ts';\nimport { defineRouterComponent } from './defineRouterComponent.ts';\n\ninterface Route {\n readonly component: CustomElementConstructor;\n readonly attributes: (pathParams: Record<string, string>, searchParams?: URLSearchParams) => Record<string, string>;\n readonly matcher: RouteMatcher;\n}\n\ntype RouteParamNames<T extends string> = T extends `${infer TPrefix}/${infer TSuffix}`\n ? TPrefix extends `${':' | '*'}${infer TName}`\n ? TName | RouteParamNames<TSuffix>\n : RouteParamNames<TSuffix>\n : T extends `${':' | '*'}${infer TName}`\n ? TName\n : never;\n\nexport type RouteParams<T extends string> = string extends T\n ? Record<string, string>\n : { readonly [K in RouteParamNames<T>]: string };\n\nexport interface RouterOptions extends Pick<ComponentOptions<{}>, 'tagName'> {\n /** Component to render when no routes match. */\n readonly fallback?: CustomElementConstructor;\n}\n\nexport interface RouterBuilder {\n /**\n * Add a route. Routes are handled in the order they are added (`Map`\n * insertion order).\n *\n * @param pattern Route matching pattern, e.g. `/blog/:slug`.\n * @param component Component to render when this route is matched.\n * @param attributes Get attributes to be applied to the component.\n */\n readonly addRoute: <const TPattern extends `/${string}`>(\n pattern: TPattern,\n component: CustomElementConstructor,\n attributes?: (pathParams: RouteParams<TPattern>, searchParams: URLSearchParams) => Record<string, string>,\n ) => this;\n\n /** Build the router component class. */\n readonly build: () => ComponentConstructor<{}>;\n}\n\n/** Define a component that renders content based on route matching. */\nexport function defineRouter(options?: RouterOptions): RouterBuilder {\n const routes = new Map<`/${string}`, Route>();\n\n const builder: RouterBuilder = {\n addRoute: (pattern, component, attributes = () => ({})) => {\n const matcher = createRouteMatcher(pattern);\n routes.set(pattern, { component, attributes: attributes as Route['attributes'], matcher });\n return builder;\n },\n build: () => {\n return defineRouterComponent(new Map(routes), options);\n },\n };\n\n return builder;\n}\n"],"mappings":";;;AA+CA,SAAgB,EAAa,GAAwC;CACnE,IAAM,oBAAS,IAAI,KAA0B,EAEvC,IAAyB;EAC7B,WAAW,GAAS,GAAW,WAAoB,EAAE,MAAM;GACzD,IAAM,IAAU,EAAmB,EAAQ;AAE3C,UADA,EAAO,IAAI,GAAS;IAAE;IAAuB;IAAmC;IAAS,CAAC,EACnF;;EAET,aACS,EAAsB,IAAI,IAAI,EAAO,EAAE,EAAQ;EAEzD;AAED,QAAO"} | ||
| {"version":3,"file":"defineRouter.js","names":[],"sources":["../../src/router/defineRouter.ts"],"sourcesContent":["import { type ComponentConstructor, type ComponentOptions } from '../defineComponent.ts';\nimport type { HtmlConfig } from '../html.ts';\nimport { createRouteMatcher, type RouteMatcher } from './createRouteMatcher.ts';\nimport { defineRouterComponent } from './defineRouterComponent.ts';\n\ninterface Route {\n readonly component: CustomElementConstructor;\n readonly config: (pathParams: Record<string, string>, searchParams?: URLSearchParams) => HtmlConfig<HTMLElement>;\n readonly matcher: RouteMatcher;\n}\n\ntype RouteParamNames<T extends string> = T extends `${infer TPrefix}/${infer TSuffix}`\n ? TPrefix extends `${':' | '*'}${infer TName}`\n ? TName | RouteParamNames<TSuffix>\n : RouteParamNames<TSuffix>\n : T extends `${':' | '*'}${infer TName}`\n ? TName\n : never;\n\nexport type RouteParams<T extends string> = string extends T\n ? Record<string, string>\n : { readonly [K in RouteParamNames<T>]: string };\n\nexport interface RouterOptions extends Pick<ComponentOptions<{}>, 'tagName'> {\n /** Component to render when no routes match. */\n readonly fallback?: CustomElementConstructor;\n /**\n * Component to render when an error is thrown by the `config` callback\n * of a route, which indicates that a path or search parameter is invalid.\n *\n * - Child text contains the error message.\n * - Attribute `data-error-name` contains the `Error` instance `name`.\n * - Attribute `data-error-code` contains the `Error` instance `code`.\n */\n readonly invalid?: CustomElementConstructor;\n}\n\nexport interface RouterBuilder {\n /**\n * Add a route. Routes are handled in the order they are added (`Map`\n * insertion order).\n *\n * @param pattern Route matching pattern, e.g. `/blog/:slug`.\n * @param component Component to render when this route is matched.\n * @param config Get the config (attributes and properties) to be applied to\n * the component.\n */\n readonly addRoute: <const TPattern extends `/${string}`, TComponent extends CustomElementConstructor>(\n pattern: TPattern,\n component: TComponent,\n config?: (pathParams: RouteParams<TPattern>, searchParams: URLSearchParams) => HtmlConfig<TComponent>,\n ) => this;\n\n /** Build the router component class. */\n readonly build: () => ComponentConstructor<{}>;\n}\n\n/** Define a component that renders content based on route matching. */\nexport function defineRouter(options?: RouterOptions): RouterBuilder {\n const routes = new Map<`/${string}`, Route>();\n\n const builder: RouterBuilder = {\n addRoute: (pattern, component, config = () => ({})) => {\n const matcher = createRouteMatcher(pattern);\n routes.set(pattern, { component, config: config as Route['config'], matcher });\n return builder;\n },\n build: () => {\n return defineRouterComponent(new Map(routes), options);\n },\n };\n\n return builder;\n}\n"],"mappings":";;;AA0DA,SAAgB,EAAa,GAAwC;CACnE,IAAM,oBAAS,IAAI,KAA0B,EAEvC,IAAyB;EAC7B,WAAW,GAAS,GAAW,WAAgB,EAAE,MAAM;GACrD,IAAM,IAAU,EAAmB,EAAQ;AAE3C,UADA,EAAO,IAAI,GAAS;IAAE;IAAmB;IAA2B;IAAS,CAAC,EACvE;;EAET,aACS,EAAsB,IAAI,IAAI,EAAO,EAAE,EAAQ;EAEzD;AAED,QAAO"} |
| import { type ComponentConstructor } from '../defineComponent.ts'; | ||
| import { type ChildValue, type HtmlConfig } from '../html.ts'; | ||
| import type { RouterOptions } from './defineRouter.ts'; | ||
| interface Route { | ||
| readonly component: CustomElementConstructor; | ||
| readonly attributes: (pathParams: Record<string, string>, searchParams?: URLSearchParams) => Record<string, string>; | ||
| readonly config: (pathParams: Record<string, string>, searchParams?: URLSearchParams) => HtmlConfig<HTMLElement>; | ||
| readonly matcher: (segments: readonly string[]) => Record<string, string> | null; | ||
@@ -10,6 +11,7 @@ } | ||
| readonly component: CustomElementConstructor; | ||
| readonly attributes: Record<string, string>; | ||
| readonly config: HtmlConfig<HTMLElement>; | ||
| readonly children: readonly ChildValue[]; | ||
| readonly pattern: string; | ||
| } | ||
| export declare function defineRouterComponent(routeEntries: ReadonlyMap<`/${string}`, Route>, { fallback, ...options }?: RouterOptions): ComponentConstructor<{}>; | ||
| export declare function defineRouterComponent(routeEntries: ReadonlyMap<`/${string}`, Route>, { fallback, invalid, ...options }?: RouterOptions): ComponentConstructor<{}>; | ||
| export {}; |
@@ -9,24 +9,43 @@ import { useRef as e } from "../hooks/useRef.js"; | ||
| //#region src/router/defineRouterComponent.ts | ||
| function s(s, { fallback: c, ...l } = {}) { | ||
| function s(s, { fallback: c, invalid: l, ...u } = {}) { | ||
| return t((t) => { | ||
| let l = r(), u = e(null, { compare: o }), d; | ||
| n([l], (e) => { | ||
| let u = r(), d = e(null, { compare: o }), f; | ||
| n([u], (e) => { | ||
| let t = new URL(e), n = a(t.pathname); | ||
| for (let [e, { component: r, attributes: i, matcher: a }] of s.entries()) { | ||
| for (let [e, { component: r, config: i, matcher: a }] of s.entries()) { | ||
| let o = a(n); | ||
| if (o) { | ||
| u.value = { | ||
| pattern: e, | ||
| component: r, | ||
| attributes: i(o, i.length >= 2 ? new URLSearchParams(t.search) : void 0) | ||
| }; | ||
| return; | ||
| let n = i.length >= 2 ? new URLSearchParams(t.search) : void 0; | ||
| try { | ||
| d.value = { | ||
| pattern: e, | ||
| component: r, | ||
| config: i(o, n), | ||
| children: [] | ||
| }; | ||
| return; | ||
| } catch (e) { | ||
| if (l) { | ||
| d.value = { | ||
| component: l, | ||
| pattern: "invalid", | ||
| config: { | ||
| "data-error-name": e instanceof Error ? e.name : null, | ||
| "data-error-code": typeof e?.code == "string" ? e.code : null | ||
| }, | ||
| children: [e instanceof Error ? e.message : String(e)] | ||
| }; | ||
| return; | ||
| } | ||
| console.warn(e); | ||
| } | ||
| } | ||
| } | ||
| u.value = c ? { | ||
| d.value = c ? { | ||
| component: c, | ||
| pattern: "fallback", | ||
| attributes: {} | ||
| config: {}, | ||
| children: [] | ||
| } : null; | ||
| }), n([u], (e) => { | ||
| }), n([d], (e) => { | ||
| if (!e) { | ||
@@ -36,8 +55,8 @@ i(t, []); | ||
| } | ||
| if (d?.component === e.component && d.pattern === e.pattern) { | ||
| i(d.element, e.attributes); | ||
| if (f?.component === e.component && f.pattern === e.pattern) { | ||
| i(f.element, e.config); | ||
| return; | ||
| } | ||
| let n = i(e.component, e.attributes); | ||
| i(t, [n]), d = { | ||
| let n = i(e.component, e.config); | ||
| i(t, [n]), f = { | ||
| component: e.component, | ||
@@ -49,3 +68,3 @@ pattern: e.pattern, | ||
| }, { | ||
| ...l, | ||
| ...u, | ||
| styles: [":host{display:contents;}"] | ||
@@ -52,0 +71,0 @@ }); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"defineRouterComponent.js","names":[],"sources":["../../src/router/defineRouterComponent.ts"],"sourcesContent":["import { type ComponentConstructor, defineComponent } from '../defineComponent.ts';\nimport { useEffect } from '../hooks/useEffect.ts';\nimport { useLocationHref } from '../hooks/useLocationHref.ts';\nimport { useRef } from '../hooks/useRef.ts';\nimport { html } from '../html.ts';\nimport { compareRouteMatches } from './compareRouteMatches.ts';\nimport type { RouterOptions } from './defineRouter.ts';\nimport { parsePath } from './parsePath.ts';\n\ninterface Route {\n readonly component: CustomElementConstructor;\n readonly attributes: (pathParams: Record<string, string>, searchParams?: URLSearchParams) => Record<string, string>;\n readonly matcher: (segments: readonly string[]) => Record<string, string> | null;\n}\n\nexport interface RouteMatch {\n readonly component: CustomElementConstructor;\n readonly attributes: Record<string, string>;\n readonly pattern: string;\n}\n\nexport function defineRouterComponent(\n routeEntries: ReadonlyMap<`/${string}`, Route>,\n { fallback, ...options }: RouterOptions = {},\n): ComponentConstructor<{}> {\n return defineComponent(\n (shadow) => {\n const href = useLocationHref();\n const routeMatch = useRef<RouteMatch | null>(null, { compare: compareRouteMatches });\n\n let prev:\n | { readonly component: CustomElementConstructor; readonly pattern: string; readonly element: HTMLElement }\n | undefined;\n\n useEffect([href], (href) => {\n const url = new URL(href);\n const segments = parsePath(url.pathname);\n\n for (const [pattern, { component, attributes, matcher }] of routeEntries.entries()) {\n const pathParams = matcher(segments);\n\n if (pathParams) {\n const searchParams = attributes.length >= 2 ? new URLSearchParams(url.search) : undefined;\n const resolvedAttributes = attributes(pathParams, searchParams);\n routeMatch.value = { pattern, component, attributes: resolvedAttributes };\n return;\n }\n }\n\n routeMatch.value = fallback ? { component: fallback, pattern: 'fallback', attributes: {} } : null;\n });\n\n useEffect([routeMatch], (routeMatch) => {\n if (!routeMatch) {\n html(shadow, []);\n return;\n }\n\n if (prev?.component === routeMatch.component && prev.pattern === routeMatch.pattern) {\n html(prev.element, routeMatch.attributes);\n return;\n }\n\n const element = html(routeMatch.component, routeMatch.attributes);\n html(shadow, [element]);\n prev = { component: routeMatch.component, pattern: routeMatch.pattern, element };\n });\n },\n { ...options, styles: [':host{display:contents;}'] },\n );\n}\n"],"mappings":";;;;;;;;AAqBA,SAAgB,EACd,GACA,EAAE,aAAU,GAAG,MAA2B,EAAE,EAClB;AAC1B,QAAO,GACJ,MAAW;EACV,IAAM,IAAO,GAAiB,EACxB,IAAa,EAA0B,MAAM,EAAE,SAAS,GAAqB,CAAC,EAEhF;AAsBJ,EAlBA,EAAU,CAAC,EAAK,GAAG,MAAS;GAC1B,IAAM,IAAM,IAAI,IAAI,EAAK,EACnB,IAAW,EAAU,EAAI,SAAS;AAExC,QAAK,IAAM,CAAC,GAAS,EAAE,cAAW,eAAY,iBAAc,EAAa,SAAS,EAAE;IAClF,IAAM,IAAa,EAAQ,EAAS;AAEpC,QAAI,GAAY;AAGd,OAAW,QAAQ;MAAE;MAAS;MAAW,YADd,EAAW,GADjB,EAAW,UAAU,IAAI,IAAI,gBAAgB,EAAI,OAAO,GAAG,KAAA,EAE3B;MAAoB;AACzE;;;AAIJ,KAAW,QAAQ,IAAW;IAAE,WAAW;IAAU,SAAS;IAAY,YAAY,EAAE;IAAE,GAAG;IAC7F,EAEF,EAAU,CAAC,EAAW,GAAG,MAAe;AACtC,OAAI,CAAC,GAAY;AACf,MAAK,GAAQ,EAAE,CAAC;AAChB;;AAGF,OAAI,GAAM,cAAc,EAAW,aAAa,EAAK,YAAY,EAAW,SAAS;AACnF,MAAK,EAAK,SAAS,EAAW,WAAW;AACzC;;GAGF,IAAM,IAAU,EAAK,EAAW,WAAW,EAAW,WAAW;AAEjE,GADA,EAAK,GAAQ,CAAC,EAAQ,CAAC,EACvB,IAAO;IAAE,WAAW,EAAW;IAAW,SAAS,EAAW;IAAS;IAAS;IAChF;IAEJ;EAAE,GAAG;EAAS,QAAQ,CAAC,2BAA2B;EAAE,CACrD"} | ||
| {"version":3,"file":"defineRouterComponent.js","names":[],"sources":["../../src/router/defineRouterComponent.ts"],"sourcesContent":["import { type ComponentConstructor, defineComponent } from '../defineComponent.ts';\nimport { useEffect } from '../hooks/useEffect.ts';\nimport { useLocationHref } from '../hooks/useLocationHref.ts';\nimport { useRef } from '../hooks/useRef.ts';\nimport { type ChildValue, html, type HtmlConfig } from '../html.ts';\nimport { compareRouteMatches } from './compareRouteMatches.ts';\nimport type { RouterOptions } from './defineRouter.ts';\nimport { parsePath } from './parsePath.ts';\n\ninterface Route {\n readonly component: CustomElementConstructor;\n readonly config: (pathParams: Record<string, string>, searchParams?: URLSearchParams) => HtmlConfig<HTMLElement>;\n readonly matcher: (segments: readonly string[]) => Record<string, string> | null;\n}\n\nexport interface RouteMatch {\n readonly component: CustomElementConstructor;\n readonly config: HtmlConfig<HTMLElement>;\n readonly children: readonly ChildValue[];\n readonly pattern: string;\n}\n\nexport function defineRouterComponent(\n routeEntries: ReadonlyMap<`/${string}`, Route>,\n { fallback, invalid, ...options }: RouterOptions = {},\n): ComponentConstructor<{}> {\n return defineComponent(\n (shadow) => {\n const href = useLocationHref();\n const routeMatch = useRef<RouteMatch | null>(null, { compare: compareRouteMatches });\n\n let prev:\n | { readonly component: CustomElementConstructor; readonly pattern: string; readonly element: HTMLElement }\n | undefined;\n\n useEffect([href], (href) => {\n const url = new URL(href);\n const segments = parsePath(url.pathname);\n\n for (const [pattern, { component, config, matcher }] of routeEntries.entries()) {\n const pathParams = matcher(segments);\n\n if (pathParams) {\n const searchParams = config.length >= 2 ? new URLSearchParams(url.search) : undefined;\n try {\n const resolvedConfig = config(pathParams, searchParams);\n routeMatch.value = { pattern, component, config: resolvedConfig, children: [] };\n return;\n } catch (error) {\n if (invalid) {\n routeMatch.value = {\n component: invalid,\n pattern: 'invalid',\n config: {\n 'data-error-name': error instanceof Error ? error.name : null,\n 'data-error-code': typeof (error as any)?.code === 'string' ? (error as any).code : null,\n },\n children: [error instanceof Error ? error.message : String(error)],\n };\n\n return;\n }\n\n console.warn(error);\n }\n }\n }\n\n routeMatch.value = fallback ? { component: fallback, pattern: 'fallback', config: {}, children: [] } : null;\n });\n\n useEffect([routeMatch], (routeMatch) => {\n if (!routeMatch) {\n html(shadow, []);\n return;\n }\n\n if (prev?.component === routeMatch.component && prev.pattern === routeMatch.pattern) {\n html(prev.element, routeMatch.config);\n return;\n }\n\n const element = html(routeMatch.component, routeMatch.config);\n html(shadow, [element]);\n prev = { component: routeMatch.component, pattern: routeMatch.pattern, element };\n });\n },\n { ...options, styles: [':host{display:contents;}'] },\n );\n}\n"],"mappings":";;;;;;;;AAsBA,SAAgB,EACd,GACA,EAAE,aAAU,YAAS,GAAG,MAA2B,EAAE,EAC3B;AAC1B,QAAO,GACJ,MAAW;EACV,IAAM,IAAO,GAAiB,EACxB,IAAa,EAA0B,MAAM,EAAE,SAAS,GAAqB,CAAC,EAEhF;AAwCJ,EApCA,EAAU,CAAC,EAAK,GAAG,MAAS;GAC1B,IAAM,IAAM,IAAI,IAAI,EAAK,EACnB,IAAW,EAAU,EAAI,SAAS;AAExC,QAAK,IAAM,CAAC,GAAS,EAAE,cAAW,WAAQ,iBAAc,EAAa,SAAS,EAAE;IAC9E,IAAM,IAAa,EAAQ,EAAS;AAEpC,QAAI,GAAY;KACd,IAAM,IAAe,EAAO,UAAU,IAAI,IAAI,gBAAgB,EAAI,OAAO,GAAG,KAAA;AAC5E,SAAI;AAEF,QAAW,QAAQ;OAAE;OAAS;OAAW,QADlB,EAAO,GAAY,EACO;OAAgB,UAAU,EAAE;OAAE;AAC/E;cACO,GAAO;AACd,UAAI,GAAS;AACX,SAAW,QAAQ;QACjB,WAAW;QACX,SAAS;QACT,QAAQ;SACN,mBAAmB,aAAiB,QAAQ,EAAM,OAAO;SACzD,mBAAmB,OAAQ,GAAe,QAAS,WAAY,EAAc,OAAO;SACrF;QACD,UAAU,CAAC,aAAiB,QAAQ,EAAM,UAAU,OAAO,EAAM,CAAC;QACnE;AAED;;AAGF,cAAQ,KAAK,EAAM;;;;AAKzB,KAAW,QAAQ,IAAW;IAAE,WAAW;IAAU,SAAS;IAAY,QAAQ,EAAE;IAAE,UAAU,EAAE;IAAE,GAAG;IACvG,EAEF,EAAU,CAAC,EAAW,GAAG,MAAe;AACtC,OAAI,CAAC,GAAY;AACf,MAAK,GAAQ,EAAE,CAAC;AAChB;;AAGF,OAAI,GAAM,cAAc,EAAW,aAAa,EAAK,YAAY,EAAW,SAAS;AACnF,MAAK,EAAK,SAAS,EAAW,OAAO;AACrC;;GAGF,IAAM,IAAU,EAAK,EAAW,WAAW,EAAW,OAAO;AAE7D,GADA,EAAK,GAAQ,CAAC,EAAQ,CAAC,EACvB,IAAO;IAAE,WAAW,EAAW;IAAW,SAAS,EAAW;IAAS;IAAS;IAChF;IAEJ;EAAE,GAAG;EAAS,QAAQ,CAAC,2BAA2B;EAAE,CACrD"} |
+1
-1
@@ -28,3 +28,3 @@ { | ||
| }, | ||
| "version": "0.8.1" | ||
| "version": "0.8.2" | ||
| } |
112191
2.78%1000
3.31%