Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
33 changes: 19 additions & 14 deletions docs/common/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A styled button component matching the regular `button` API. Pass a handler to t
type InputSchema = { count: Cell<number> };
type OutputSchema = { count: Cell<number> };

const MyRecipe = recipe<InputSchema, OutputSchema>("MyRecipe", ({ count }) => {
const MyRecipe = recipe<InputSchema, OutputSchema>(({ count }) => {
const handleClick = handler<unknown, { count: Cell<number> }>((_event, { count }) => {
count.set(count.get() + 1);
});
Expand All @@ -22,7 +22,7 @@ Notice how handlers are bound to the cell from the input schema _in_ the VDOM de

(For even more detail, see `HANDLERS.md`)

# Bidirectional Binding with $ Prefix
## Bidirectional Binding with $ Prefix

Many CommonTools components support **bidirectional binding** through the `$` prefix. This powerful feature automatically updates cells when users interact with components, eliminating the need for explicit onChange handlers in most cases.

Expand Down Expand Up @@ -96,6 +96,7 @@ const toggle = handler<{detail: {checked: boolean}}, {item: Cell<Item>}>(
```

The bidirectional binding version is:

- **Simpler**: No handler definition needed
- **Less code**: One line instead of five
- **More maintainable**: Less surface area for bugs
Expand Down Expand Up @@ -123,7 +124,7 @@ const toggle = handler(
one-directional binding.

Note that the actual name for the `onChange` handler may be different depending
on the component being used. For example, <ct-checkbox> uses `onct-change`.
on the component being used. For example, `<ct-checkbox>` uses `onct-change`.
Consult the component for details.

### Validation Example
Expand Down Expand Up @@ -152,7 +153,7 @@ const validatedValue = derive(rawInput, (value) => {

This approach separates concerns: bidirectional binding handles the UI sync, while derive handles validation logic.

# Styling: String vs Object Syntax
## Styling: String vs Object Syntax

Different element types accept different style syntax in CommonTools JSX. This is a common source of TypeScript errors.

Expand Down Expand Up @@ -242,15 +243,15 @@ CommonTools custom elements (`common-hstack`, `common-vstack`, `ct-card`, etc.)
<common-hstack style="padding: 1rem;">
```

# ct-input
## ct-input

The `ct-input` component demonstrates bidirectional binding perfectly:

```tsx
type InputSchema = { value: Cell<string> };
type OutputSchema = { value: Cell<string> };

const MyRecipe = recipe<InputSchema, OutputSchema>("MyRecipe", ({ value }) => {
const MyRecipe = recipe(({ value }: InputSchema) => {
// Option 1: Bidirectional binding (simplest)
const simpleInput = <ct-input $value={value} />;

Expand All @@ -275,7 +276,7 @@ const MyRecipe = recipe<InputSchema, OutputSchema>("MyRecipe", ({ value }) => {

Both inputs update the cell, but the second one logs changes. Use the simple bidirectional binding unless you need the extra logic.

# ct-select
## ct-select

The `ct-select` component creates a dropdown selector. **Important:** It uses an `items` attribute with an array of `{ label, value }` objects, **not** HTML `<option>` elements.

Expand All @@ -284,7 +285,7 @@ type CategoryInput = {
category: Default<string, "Other">;
};

const MyRecipe = recipe<CategoryInput, CategoryInput>("MyRecipe", ({ category }) => {
const MyRecipe = recipe(({ category }: CategoryInput) => {
return {
[UI]: (
<ct-select
Expand Down Expand Up @@ -363,7 +364,7 @@ The `value` property doesn't have to be a string - it can be any type:

Like other components, `ct-select` supports bidirectional binding with the `$value` prefix, automatically updating the cell when the user selects an option.

# ct-list
## ct-list

The `ct-list` component provides a convenient way to display and manage lists, but it has **specific schema requirements**.

Expand All @@ -380,7 +381,7 @@ interface CtListItem {

type ListSchema = { items: Cell<CtListItem[]> };

const MyRecipe = recipe<ListSchema, ListSchema>("MyRecipe", ({ items }) => {
const MyRecipe = recipe(({ items }: ListSchema) => {
return {
[UI]: <div>
<ct-list $items={items} editable={false} />
Expand Down Expand Up @@ -418,16 +419,18 @@ interface ShoppingItem {
## Trade-offs

**Use ct-list when:**

- Your items only need `title` and optionally `done`
- You want a quick, pre-styled list component
- You don't need custom rendering

**Use manual rendering when:**

- You have custom fields
- You need custom styling or layout
- You need fine-grained control over interactions

# ct-message-input
## ct-message-input

This component bundles an input and a button to 'send a message' or 'add an item to a list' which is a common pattern. You don't need to worry about the value until submission time.

Expand All @@ -449,7 +452,7 @@ const addItem = handler<
/>
````

# ct-outliner
## ct-outliner

`ct-outliner` is conceptually similar to `ct-list` except it works on a tree data structure. Below is a demonstration of the minimal use, see `page.tsx` for a more complete example.

Expand Down Expand Up @@ -510,7 +513,7 @@ export default recipe<PageInput, PageResult>(
);
```

# ct-render
## ct-render

The `ct-render` component displays pattern instances within another pattern. Use this for **pattern composition** - combining multiple patterns together in a single recipe.

Expand Down Expand Up @@ -571,7 +574,7 @@ interface Input {
items: Default<Item[], []>;
}

export default recipe<Input, Input>("Multi-View", ({ items }) => {
export default recipe(({ items }: Input) => {
// Both patterns receive the same items cell
const listView = ListView({ items });
const gridView = GridView({ items });
Expand All @@ -596,6 +599,7 @@ export default recipe<Input, Input>("Multi-View", ({ items }) => {
```

**What happens:**

- Both patterns receive the same `items` cell reference
- Changes in ListView automatically appear in GridView
- Changes in GridView automatically appear in ListView
Expand All @@ -619,6 +623,7 @@ const counter = Counter({ value: state.value });
```

**When to use each:**

- **Direct interpolation** (`{counter}`): Simple cases, most concise
- **JSX component syntax** (`<Counter />`): When you want it to look like a component
- **ct-render** (`<ct-render $cell={counter} />`): When the pattern wasn't instantiated from within this pattern but was passed in or was stored in a list.
Expand Down
18 changes: 16 additions & 2 deletions docs/common/HANDLERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const updateTitle = handler<
## Common TypeScript Issues & Fixes

### Handler Function Parameters

- **First parameter**: Event data (from UI interactions)
- **Second parameter**: State data (destructured from state schema)
- **Parameter naming**: Use descriptive names or `_` prefix for unused parameters
Expand Down Expand Up @@ -273,7 +274,7 @@ const removeItem = handler<
}
});

export default recipe<Input, Input>("Shopping List", ({ items }) => {
export default recipe<Input, Input>(({ items }) => {
// items here is OpaqueRef<ShoppingItem[]>

return {
Expand Down Expand Up @@ -307,7 +308,7 @@ export default recipe<Input, Input>("Shopping List", ({ items }) => {

**Rule of thumb:** In handler type signatures, use `Cell<T[]>` for array parameters. The Cell wraps the entire array, not individual elements.

#### Event Parameter Patterns
### Event Parameter Patterns

You can handle the event parameter in different ways depending on your needs:

Expand Down Expand Up @@ -343,6 +344,7 @@ const simpleIncrement = handler<Record<string, never>, { count: Cell<number> }>(
## Handler Invocation Patterns

### State Passing

When invoking handlers in UI, pass an object that matches your state schema:

```typescript
Expand All @@ -357,18 +359,21 @@ onClick={myHandler({ items: itemsArray, currentPage: currentPageString })}
```

### Event vs Props Confusion

- **Event type**: Describes data coming FROM the UI event
- **State type**: Describes data the handler needs to ACCESS
- **Invocation object**: Must match the state type, NOT the event type

## Debugging Handler Issues

### Type Mismatches

1. Check that handler invocation object matches state type
2. Verify event type matches actual UI event structure
3. Ensure destructuring in handler function matches types

### Runtime Issues

1. Use `console.log` in handler functions to debug
2. Check that state objects have expected properties
3. Verify UI events are firing correctly
Expand All @@ -384,13 +389,15 @@ onClick={myHandler({ items: itemsArray, currentPage: currentPageString })}
## Examples by Use Case

### Counter/Simple State

```typescript
const increment = handler<Record<string, never>, { count: Cell<number> }>(
(_, { count }) => count.set(count.get() + 1)
);
```

### Form/Input Updates

```typescript
const updateField = handler<
{ detail: { value: string } },
Expand All @@ -401,6 +408,7 @@ const updateField = handler<
```

### List/Array Operations

```typescript
const addItem = handler<
{ message: string },
Expand All @@ -412,6 +420,7 @@ const addItem = handler<
```

### Complex State Updates

```typescript
const updatePageContent = handler<
{ detail: { value: string } },
Expand All @@ -426,6 +435,7 @@ const updatePageContent = handler<
## Advanced Patterns

### Handler Composition

```typescript
// Base handlers for reuse
const createTimestamp = () => Date.now();
Expand Down Expand Up @@ -491,6 +501,7 @@ const handleFormSubmit = handler<
## Common Pitfalls

### 1. Type Mismatch

```typescript
// ❌ Wrong: State type doesn't match invocation
const handler = handler<Record<string, never>, { count: number }>(
Expand All @@ -502,6 +513,7 @@ onClick={handler({ value: 5 })} // Should be { count: 5 }
```

### 2. Event Type Over-specification

```typescript
// ❌ Wrong: Over-complicated event type for simple clicks
const handler = handler<{
Expand All @@ -518,6 +530,7 @@ const handler = handler<Record<string, never>, any>(
```

### 3. Mutation vs Immutability

```typescript
// ❌ Wrong: Direct assignment to non-cell state
(_, { title }) => {
Expand All @@ -538,6 +551,7 @@ const handler = handler<Record<string, never>, any>(
## Debugging Handlers

### Console-based Debugging

```typescript
// In recipes, use console logging for debugging
const addItem = handler<
Expand Down
Loading
Loading