Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
<script type="importmap">
{
"imports": {
"@std/assert/": "https://esm.sh/jsr/@std/assert/"
"@f-stack/functorial": "./packages/functorial/src/reactive.js",
"@f-stack/reflow": "./packages/reflow/src/html.js"
}
}
</script>
Expand Down
2 changes: 1 addition & 1 deletion packages/functorial/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ The library's single `.js` file can be load directly from a CDN
<script type="importmap">
{
"imports": {
"@f-stack/functorial": "https://esm.sh/jsr/@f-stack/functorial"
"@f-stack/functorial": "https://cdn.jsdelivr.net/gh/fcrozatier/f-stack@main/packages/functorial/src/reactive.js"
}
}
</script>
Expand Down
4 changes: 2 additions & 2 deletions packages/functorial/src/reactive.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ export function isPrimitive(value: unknown): value is Primitive;
* @template T
* @param {T} node
* @param {ReactiveEventCallback} callback
* @return {() => void} A cleanup function to remove the listener
* @return {Disposable} A disposable object to cleanup the listener
*/
export function listen<T>(node: T, callback: ReactiveEventCallback): () => void;
export function listen<T>(node: T, callback: ReactiveEventCallback): Disposable;

/**
* Creates a derived {@linkcode reactive} with a `value` getter
Expand Down
13 changes: 8 additions & 5 deletions packages/functorial/src/reactive.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,15 @@ export function reactive(object) {

/**
* @param {ReactiveEventCallback} callback
* @return {Disposable}
*/
function addListener(callback) {
callbacks.add(callback);
return () => callbacks.delete(callback);
return {
[Symbol.dispose]() {
callbacks.delete(callback);
},
};
}

/**
Expand Down Expand Up @@ -939,8 +944,6 @@ export function isPrimitive(value) {
["string", "number", "boolean", "undefined"].includes(typeof value);
}

function noop() {}

/**
* Listens to a {@linkcode reactive} graph and runs the provided callback whenever a change or call is detected
*
Expand All @@ -949,11 +952,11 @@ function noop() {}
* @template T
* @param {T} node
* @param {ReactiveEventCallback} callback
* @return {() => void} A cleanup function to remove the listener
* @return {Disposable} A cleanup function to remove the listener
*/
export function listen(node, callback) {
// doing the sanity check here to avoid spreading these checks all over the codebase
if (!isReactive(node)) return noop;
if (!isReactive(node)) return { [Symbol.dispose]() {} };
return getOwn(node, ns.ADD_LISTENER)(callback);
}

Expand Down
25 changes: 15 additions & 10 deletions packages/functorial/src/reactive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,23 +459,28 @@ Deno.test("nested listeners", () => {
const MSG2 = "even";
const MSG3 = "odd";

let cleanup: (() => void) | undefined;
let disposer: DisposableStack | undefined;

listen(r, (e) => {
cleanup?.();
disposer?.dispose();
events.push({ ...e, info: "root listener" });

if (e.type === "update") {
disposer = new DisposableStack();
if (e.newValue % 2 === 0) {
cleanup = listen(r, (e1) => {
events.push({ ...e1, info: "even listener" });
(e.newValue % 6 === 0) ? logs.push(MSG1) : logs.push(MSG2);
});
disposer.use(
listen(r, (e1) => {
events.push({ ...e1, info: "even listener" });
(e.newValue % 6 === 0) ? logs.push(MSG1) : logs.push(MSG2);
}),
);
} else {
cleanup = listen(r, (e2) => {
events.push({ ...e2, info: "odd listener" });
logs.push(MSG3);
});
disposer.use(
listen(r, (e2) => {
events.push({ ...e2, info: "odd listener" });
logs.push(MSG3);
}),
);
}
}
});
Expand Down
56 changes: 51 additions & 5 deletions packages/reflow/src/html.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Primitive, ReactiveLeaf } from "@f-stack/functorial";
import type { listen, Primitive, ReactiveLeaf } from "@f-stack/functorial";
import type { DOMAttributesTagNameMap } from "./elements.d.ts";

/**
Expand All @@ -11,7 +11,7 @@ import type { DOMAttributesTagNameMap } from "./elements.d.ts";
export type TemplateTag = (
strings: TemplateStringsArray,
...sinks: Sink[]
) => DocumentFragment;
) => TemplateSink;

/**
* A sink is either an {@linkcode ElementSink} or a {@linkcode FragmentSink}
Expand All @@ -36,6 +36,7 @@ export type FragmentSink =
| DerivedSink
| MapSink
| ShowSink
| TemplateSink
| TextSink
| UnsafeSink;

Expand Down Expand Up @@ -179,7 +180,7 @@ export function isClassSink(value: unknown): value is ClassListSink;
*/
export type MapSink<T = any> = {
values: T[];
mapper: (value: T, index: ReactiveLeaf<number>) => DocumentFragment;
mapper: (value: T, index: ReactiveLeaf<number>) => TemplateSink;
};

/**
Expand All @@ -204,7 +205,7 @@ export type MapSink<T = any> = {
*/
export function map<T>(
values: T[],
mapper: (value: T, index: ReactiveLeaf<number>) => DocumentFragment,
mapper: (value: T, index: ReactiveLeaf<number>) => TemplateSink,
): MapSink<T>;

/**
Expand Down Expand Up @@ -402,6 +403,28 @@ export function style(styles: StyleSink): StyleSink;
*/
export function isStyleSink(value: unknown): value is StyleSink;

// template

/**
* A `TemplateSink` is a type of sink that's `Disposable`
*
* @see {@linkcode TemplateTag}
*/
export interface TemplateSink extends Disposable {
/**
* The `DocumentFragment` template to mount into the DOM
*/
fragment: DocumentFragment;
}

/**
* Checks whether a sink is a template sink
*
* @param {unknown} value
* @returns {value is TemplateSink}
*/
export function isTemplateSink(value: unknown): value is TemplateSink;

// text

/**
Expand Down Expand Up @@ -493,4 +516,27 @@ export function isUnsafeHTML(value: unknown): value is UnsafeSink;
/**
* Return type of the {@linkcode derived} sink callback when inlined in the template
*/
export type DerivedSink = Primitive | ReactiveLeaf | DocumentFragment;
export type DerivedSink = Primitive | ReactiveLeaf | TemplateSink;

/**
* An `EffectScope` is a disposable scope used in components that provides a local {@linkcode listen} function.
*/
export interface EffectScope extends Disposable {
/**
* A `DisposableStack` that holds the component unmount logic
*/
disposer: DisposableStack;
/**
* The component local {@linkcode listen} function which is cleaned up automatically when the component is unmounted
*/
listen: typeof listen;
}

/**
* Creates a component
*
* Provides a local {@linkcode listen} function that's automatically cleaned up when the component unmounts
*/
export function component<T extends any[]>(
callback: (this: EffectScope, ...args: T) => TemplateSink,
): (...args: NoInfer<T>) => TemplateSink;
Loading