///
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;