seqflow-js
Advanced tools
Comparing version 0.0.1-beta.12 to 0.0.1-beta.13
@@ -10,7 +10,10 @@ import { BrowserRouter, InMemoryRouter, NavigationEvent, Router } from './router'; | ||
export interface Log { | ||
type: "info" | "debug" | "error"; | ||
message: string; | ||
data?: unknown; | ||
} | ||
export type LogFunction = (log: Log) => void; | ||
export interface LogFunction { | ||
info: (l: Log) => void; | ||
error: (l: Log) => void; | ||
debug: (l: Log) => void; | ||
} | ||
export interface SeqflowAppContext { | ||
@@ -61,3 +64,3 @@ log: LogFunction; | ||
preventDefault?: boolean; | ||
}) => EventAsyncGenerator<HTMLElementEventMap[K]>; | ||
} | string) => EventAsyncGenerator<HTMLElementEventMap[K]>; | ||
/** | ||
@@ -74,2 +77,16 @@ * Wait for a domain event to happen | ||
/** | ||
* Get a child component by its key | ||
* | ||
* @param key the key of the child to get | ||
* @returns the child component | ||
*/ | ||
getChild(key: string): HTMLElement; | ||
/** | ||
* Get a child component by its key | ||
* | ||
* @param key the key of the child to get | ||
* @returns the child component or null if not found | ||
*/ | ||
findChild(key: string): HTMLElement | null; | ||
/** | ||
* Replace a child component with a new one | ||
@@ -81,3 +98,3 @@ * | ||
*/ | ||
replaceChild: (key: string, newChild: () => JSX.Element | Promise<JSX.Element>) => void; | ||
replaceChild: (key: string, newChild: () => JSX.Element | Promise<JSX.Element>) => void | Promise<void>; | ||
/** | ||
@@ -115,3 +132,5 @@ * The DOM element where this component is mounted | ||
children?: ChildenType[]; | ||
}>(root: HTMLElement, firstComponent: Component, componentOption: FirstComponentData | undefined, seqflowConfiguration: Partial<SeqflowConfiguration>): AbortController; | ||
}>(root: HTMLElement, firstComponent: Component, componentOption: FirstComponentData | undefined, seqflowConfiguration: Partial<Omit<SeqflowConfiguration, "log"> & { | ||
log?: Partial<LogFunction>; | ||
}>): AbortController; | ||
type ChildenType = Element | Element[]; | ||
@@ -130,2 +149,4 @@ declare global { | ||
style?: Partial<CSSStyleDeclaration> | string; | ||
onClick?: (ev: MouseEvent) => void; | ||
key?: string; | ||
}>; | ||
@@ -137,2 +158,3 @@ }; | ||
key?: string; | ||
onClick?: (ev: MouseEvent) => void; | ||
wrapperClass?: string; | ||
@@ -139,0 +161,0 @@ } |
{ | ||
"name": "seqflow-js", | ||
"version": "0.0.1-beta.12", | ||
"version": "0.0.1-beta.13", | ||
"description": "SeqFlow is a modern web framework that is designed to be simple and easy to use. It optimizes the development process by providing a simple and easy-to-understand API. It is a good choice for those who want to create web applications without the complexity of other frameworks.", | ||
@@ -5,0 +5,0 @@ "main": "./dist/index.mjs", |
@@ -39,3 +39,3 @@ # SeqFlow JS | ||
// Perform an async operation | ||
const quote = quote = await getRandomQuote(); | ||
const quote = await getRandomQuote(); | ||
@@ -42,0 +42,0 @@ // Replace loading message with quote |
243
src/index.ts
@@ -20,7 +20,10 @@ import * as DomainsPackage from "./domains"; | ||
export interface Log { | ||
type: "info" | "debug" | "error"; | ||
message: string; | ||
data?: unknown; | ||
} | ||
export type LogFunction = (log: Log) => void; | ||
export interface LogFunction { | ||
info: (l: Log) => void; | ||
error: (l: Log) => void; | ||
debug: (l: Log) => void; | ||
} | ||
@@ -76,6 +79,8 @@ export interface SeqflowAppContext { | ||
eventType: K, | ||
options: { | ||
el: HTMLElement; | ||
preventDefault?: boolean; | ||
}, | ||
options: | ||
| { | ||
el: HTMLElement; | ||
preventDefault?: boolean; | ||
} | ||
| string, | ||
) => EventAsyncGenerator<HTMLElementEventMap[K]>; | ||
@@ -95,2 +100,16 @@ /** | ||
/** | ||
* Get a child component by its key | ||
* | ||
* @param key the key of the child to get | ||
* @returns the child component | ||
*/ | ||
getChild(key: string): HTMLElement; | ||
/** | ||
* Get a child component by its key | ||
* | ||
* @param key the key of the child to get | ||
* @returns the child component or null if not found | ||
*/ | ||
findChild(key: string): HTMLElement | null; | ||
/** | ||
* Replace a child component with a new one | ||
@@ -105,3 +124,3 @@ * | ||
newChild: () => JSX.Element | Promise<JSX.Element>, | ||
) => void; | ||
) => void | Promise<void>; | ||
/** | ||
@@ -124,3 +143,5 @@ * The DOM element where this component is mounted | ||
children, | ||
}: { children?: ChildenType[] }): DocumentFragment; | ||
}: { | ||
children?: ChildenType[]; | ||
}): DocumentFragment; | ||
} | ||
@@ -157,4 +178,3 @@ export type SeqflowFunction<T> = ( | ||
const componentName = component.name; | ||
parentContext.app.log({ | ||
type: "debug", | ||
parentContext.app.log.debug({ | ||
message: "startComponent", | ||
@@ -215,2 +235,20 @@ data: { componentOption, componentName }, | ||
}, | ||
getChild(this: SeqflowFunctionContext, key: string): HTMLElement { | ||
const child = this.findChild(key); | ||
if (!child) { | ||
this.app.log.error({ | ||
message: "getChild: wrapper not found", | ||
data: { key }, | ||
}); | ||
throw new Error("getChild: wrapper not found"); | ||
} | ||
return child; | ||
}, | ||
findChild(this: SeqflowFunctionContext, key: string): HTMLElement | null { | ||
const child = componentChildren.find((c) => c.key === key); | ||
if (child) { | ||
return child.el; | ||
} | ||
return null; | ||
}, | ||
replaceChild( | ||
@@ -220,7 +258,6 @@ this: SeqflowFunctionContext, | ||
newChild: () => JSX.Element | Promise<JSX.Element>, | ||
) { | ||
): void | Promise<void> { | ||
const oldChildIndex = componentChildren.findIndex((c) => c.key === key); | ||
if (oldChildIndex < 0) { | ||
this.app.log({ | ||
type: "error", | ||
this.app.log.error({ | ||
message: "replaceChild: wrapper not found", | ||
@@ -243,8 +280,21 @@ data: { key, newChild }, | ||
for (const otherChild of componentChildren) { | ||
// If we replace a child which contains another child, | ||
// we have to abort the other child also | ||
if (oldChild.el.contains(otherChild.el)) { | ||
this.app.log.debug({ | ||
message: "replaceChild: wrapper contains other child", | ||
data: { parent: key, child: otherChild.key }, | ||
}); | ||
this.abortController.signal.dispatchEvent( | ||
new Event(`abort-component-${otherChild.key}`), | ||
); | ||
} | ||
} | ||
const a = newChild(); | ||
if (a instanceof Promise) { | ||
a.then((child) => { | ||
return a.then((child) => { | ||
wrapper.replaceWith(child as Node); | ||
}); | ||
return; | ||
} | ||
@@ -257,10 +307,20 @@ | ||
eventType: K, | ||
options: { | ||
el: HTMLElement; | ||
preventDefault?: boolean; | ||
}, | ||
options: | ||
| { | ||
el: HTMLElement; | ||
preventDefault?: boolean; | ||
} | ||
| string, | ||
): EventAsyncGenerator<HTMLElementEventMap[K]> { | ||
let el: HTMLElement; | ||
let preventDefault = false; | ||
if (typeof options === "string") { | ||
el = this.getChild(options); | ||
} else { | ||
el = options.el; | ||
preventDefault = options.preventDefault ?? false; | ||
} | ||
return domEvent(eventType, { | ||
el: options.el, | ||
preventDefault: options.preventDefault ?? false, | ||
el, | ||
preventDefault, | ||
}); | ||
@@ -277,4 +337,3 @@ }, | ||
navigationEvent(): EventAsyncGenerator<NavigationEvent> { | ||
this.app.log({ | ||
type: "debug", | ||
this.app.log.debug({ | ||
message: "navigationEvent", | ||
@@ -297,4 +356,3 @@ data: { | ||
): DocumentFragment { | ||
this.app.log({ | ||
type: "debug", | ||
this.app.log.debug({ | ||
message: "createDOMFragment", | ||
@@ -328,4 +386,3 @@ data: { children }, | ||
this.app.log({ | ||
type: "error", | ||
this.app.log.error({ | ||
message: "Unsupported child type. Implement me", | ||
@@ -342,7 +399,6 @@ data: { child, children }, | ||
tagName: string, | ||
options?: { [key: string]: string }, | ||
options?: { [key: string]: unknown }, | ||
...children: ChildenType[] | ||
): Node { | ||
this.app.log({ | ||
type: "debug", | ||
this.app.log.debug({ | ||
message: "createDOMElement", | ||
@@ -356,9 +412,28 @@ data: { tagName, options, children }, | ||
if (key === "className") { | ||
el.setAttribute("class", options[key]); | ||
el.setAttribute("class", options[key] as string); | ||
continue; | ||
} | ||
if (key === "htmlFor") { | ||
el.setAttribute("for", options[key]); | ||
el.setAttribute("for", options[key] as string); | ||
continue; | ||
} | ||
if (key === "onClick") { | ||
const k = (options.key as string) ?? Math.random().toString(); | ||
const elAbortController = new AbortController(); | ||
this.abortController.signal.addEventListener( | ||
`abort-component-${k}`, | ||
() => { | ||
elAbortController.abort(); | ||
}, | ||
{ once: true }, | ||
); | ||
componentChildren.push({ | ||
key: k, | ||
el, | ||
}); | ||
el.addEventListener("click", options[key] as EventListener, { | ||
signal: elAbortController.signal, | ||
}); | ||
continue; | ||
} | ||
if (key === "style") { | ||
@@ -378,3 +453,12 @@ const style = options[key] as string | Partial<CSSStyleDeclaration>; | ||
} | ||
el.setAttribute(key, options[key]); | ||
// keep the key for the componentChildren array | ||
if (key === "key") { | ||
componentChildren.push({ | ||
key: options[key] as string, | ||
el, | ||
}); | ||
} | ||
el.setAttribute(key, options[key] as string); | ||
} | ||
@@ -400,6 +484,11 @@ | ||
this.app.log({ | ||
type: "error", | ||
this.app.log.error({ | ||
message: "Unsupported child type. Implement me", | ||
data: { child, children, tagName, options, el }, | ||
data: { | ||
child, | ||
children, | ||
tagName, | ||
options, | ||
el, | ||
}, | ||
}); | ||
@@ -415,14 +504,26 @@ throw new Error("Unsupported child type"); | ||
const opt = options || {}; | ||
if (!opt?.key) { | ||
opt.key = Math.random().toString(); | ||
} | ||
const wrapper = document.createElement("div"); | ||
if (options?.key) { | ||
componentChildren.push({ | ||
key: options.key, | ||
el: wrapper, | ||
componentChildren.push({ | ||
key: opt.key as string, | ||
el: wrapper, | ||
}); | ||
if (opt.wrapperClass) { | ||
wrapper.classList.add(opt.wrapperClass as string); | ||
} | ||
if (typeof opt.onClick === "function") { | ||
wrapper.addEventListener("click", opt.onClick as EventListener, { | ||
signal: childContext.abortController.signal, | ||
}); | ||
} | ||
if (options?.wrapperClass) { | ||
wrapper.classList.add(options.wrapperClass); | ||
} | ||
startComponent(childContext, wrapper, tagName, { ...options, children }); | ||
startComponent(childContext, wrapper, tagName, { | ||
...opt, | ||
children, | ||
}); | ||
return wrapper; | ||
@@ -444,11 +545,12 @@ }, | ||
() => { | ||
parentContext.app.log({ | ||
type: "debug", | ||
parentContext.app.log.debug({ | ||
message: "Component rendering ended", | ||
data: { componentOption, componentName }, | ||
data: { | ||
componentOption, | ||
componentName, | ||
}, | ||
}); | ||
}, | ||
(e) => { | ||
parentContext.app.log({ | ||
type: "error", | ||
parentContext.app.log.error({ | ||
message: "Component throws an error", | ||
@@ -471,3 +573,5 @@ data: { | ||
// biome-ignore lint/suspicious/noExplicitAny: We don't care about the component properties | ||
FirstComponentData extends Record<string, any> & { children?: ChildenType[] }, | ||
FirstComponentData extends Record<string, any> & { | ||
children?: ChildenType[]; | ||
}, | ||
>( | ||
@@ -477,3 +581,5 @@ root: HTMLElement, | ||
componentOption: FirstComponentData | undefined, | ||
seqflowConfiguration: Partial<SeqflowConfiguration>, | ||
seqflowConfiguration: Partial< | ||
Omit<SeqflowConfiguration, "log"> & { log?: Partial<LogFunction> } | ||
>, | ||
): AbortController { | ||
@@ -496,6 +602,8 @@ const seqflowConfig = applyDefaults(seqflowConfiguration); | ||
seqflowConfig.router.getEventTarget().addEventListener("navigation", (ev) => { | ||
appContext.log({ | ||
type: "info", | ||
appContext.log.info({ | ||
message: "navigate", | ||
data: { path: (ev as NavigationEvent).path, event: ev }, | ||
data: { | ||
path: (ev as NavigationEvent).path, | ||
event: ev, | ||
}, | ||
}); | ||
@@ -513,2 +621,8 @@ }); | ||
}, | ||
getChild: () => { | ||
throw new Error("getChild is not supported in the main context"); | ||
}, | ||
findChild: () => { | ||
throw new Error("findChild is not supported in the main context"); | ||
}, | ||
replaceChild: () => { | ||
@@ -567,3 +681,5 @@ throw new Error("replaceChild is not supported in the main context"); | ||
function applyDefaults( | ||
seqflowConfiguration: Partial<SeqflowConfiguration>, | ||
seqflowConfiguration: Partial< | ||
Omit<SeqflowConfiguration, "log"> & { log?: Partial<LogFunction> } | ||
>, | ||
): SeqflowConfiguration { | ||
@@ -576,6 +692,18 @@ function noop() {} | ||
if (seqflowConfiguration.log === undefined) { | ||
seqflowConfiguration.log = {}; | ||
} | ||
if (seqflowConfiguration.log.info === undefined) { | ||
seqflowConfiguration.log.info = noop; | ||
} | ||
if (seqflowConfiguration.log.error === undefined) { | ||
seqflowConfiguration.log.error = noop; | ||
} | ||
if (seqflowConfiguration.log.debug === undefined) { | ||
seqflowConfiguration.log.debug = noop; | ||
} | ||
return Object.assign( | ||
{}, | ||
{ | ||
log: noop, | ||
config: {}, | ||
@@ -653,2 +781,4 @@ domains: {}, | ||
style?: Partial<CSSStyleDeclaration> | string; | ||
onClick?: (ev: MouseEvent) => void; | ||
key?: string; | ||
}>; | ||
@@ -660,2 +790,3 @@ }; | ||
key?: string; | ||
onClick?: (ev: MouseEvent) => void; | ||
wrapperClass?: string; | ||
@@ -662,0 +793,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
313170
40
3285