/// import { action, computed, Default, handler, NAME, pattern, Stream, UI, type VNode, Writable, } from "commontools"; // ===== Types ===== interface CounterInput { value?: Writable>; } interface CounterOutput { [NAME]: string; [UI]: VNode; value: number; increment: Stream; decrement: Stream; } // ===== Module-scope handler ===== // Use module-scope handlers when the same handler needs to be reused across // multiple pattern instances or bound to different values. The handler is // defined once and can be bound to different contexts. // // handler - Event is what .send() receives, Context is bound state const increment = handler }>( (_, { value }) => { value.set(value.get() + 1); }, ); // ===== Helper functions ===== function ordinal(n: number): string { const num = n ?? 0; if (num % 10 === 1 && num % 100 !== 11) return `${num}st`; if (num % 10 === 2 && num % 100 !== 12) return `${num}nd`; if (num % 10 === 3 && num % 100 !== 13) return `${num}rd`; return `${num}th`; } // ===== Pattern ===== const Counter = pattern(({ value }) => { // Bind the module-scope handler with its required context const boundIncrement = increment({ value }); // Pattern-body action (PREFERRED approach for single-use handlers) // When an action only needs to work with this pattern's state, use action() // which closes over the pattern's values directly. This is simpler and clearer // than defining a reusable handler when you don't need reusability. const decrement = action(() => { value.set(value.get() - 1); }); // Computed values const displayName = computed(() => `Counter: ${value.get()}`); const ordinalDisplay = computed(() => ordinal(value.get())); return { [NAME]: displayName, [UI]: ( Simple Counter
{value}
Counter is the {ordinalDisplay} number
{/* onClick can take a Stream directly - runtime calls .send() */} - Decrement {/* onClick can also take a function that calls .send() explicitly */} boundIncrement.send()} > + Increment
), value, // Both approaches can be exported and tested via the `ct` CLI // and with automated pattern tests. See counter.test.tsx. increment: boundIncrement, // Module-scope handler, bound in pattern decrement, // Pattern-body action, closes over value directly }; }); // ===== Pattern as JSX Element ===== // Patterns can be rendered as JSX elements directly. This is useful when // composing patterns or creating wrapper views. Since value is optional with // a Default, we don't need to pass it. const _CounterView = pattern(() => { return { [UI]: , }; }); export default Counter;