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
110 changes: 75 additions & 35 deletions docs/common/COMPONENTS.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
# ct-button

A styled button component matching the regular `button` API. Pass a handler to the `onClick` prop to bind it.
A styled button component matching the regular `button` API. You can use inline handlers or the `handler()` function.

## Inline Handler (Preferred for Simple Cases)

```tsx
interface Input {
count: Cell<number>;
}

const MyRecipe = recipe<Input>(({ count }) => {
return {
[UI]: (
<ct-button onClick={() => count.set(count.get() + 1)}>
Increment
</ct-button>
),
count,
};
});
```

**Key point:** Declare `count` as `Cell<number>` in your input type to use it in inline handlers.

## Using handler() for Complex Logic

```tsx
type InputSchema = { count: Cell<number> };
type OutputSchema = { count: Cell<number> };
interface Input {
count: Cell<number>;
}

const MyRecipe = recipe<InputSchema, OutputSchema>(({ count }) => {
const handleClick = handler<unknown, { count: Cell<number> }>((_event, { count }) => {
count.set(count.get() + 1);
});
const handleClick = handler<unknown, { count: Cell<number> }>((_event, { count }) => {
const newValue = count.get() + 1;
count.set(newValue);
console.log("Count updated to:", newValue);
});

const MyRecipe = recipe<Input>(({ count }) => {
return {
[UI]: <ct-button onClick={handleClick({ count })} />,
[UI]: <ct-button onClick={handleClick({ count })}>Increment</ct-button>,
count,
};
});
Expand Down Expand Up @@ -129,16 +155,17 @@ Consult the component for details.

### Validation Example

For validation, consider using two cells: a raw input cell and a validated derived cell:
For validation, consider using two cells: a raw input cell and a validated computed cell:

```tsx
// Raw input with bidirectional binding
const rawInput = cell("");
const rawInput = Cell.of("");

<ct-input $value={rawInput} />

// Validated output using derive
const validatedValue = derive(rawInput, (value) => {
// Validated output using computed
const validatedValue = computed(() => {
const value = rawInput;
if (value.length < 3) return null;
if (!value.match(/^[a-z]+$/i)) return null;
return value;
Expand All @@ -151,7 +178,7 @@ const validatedValue = derive(rawInput, (value) => {
}
```

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

## Styling: String vs Object Syntax

Expand Down Expand Up @@ -245,36 +272,49 @@ CommonTools custom elements (`common-hstack`, `common-vstack`, `ct-card`, etc.)

## ct-input

The `ct-input` component demonstrates bidirectional binding perfectly:
The `ct-input` component demonstrates bidirectional binding and inline handlers:

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

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

// Option 2: With handler for additional logic
const handleChange = handler<
{ detail: { value: string } },
{ value: Cell<string> }
>((event, { value }) => {
value.set(event.detail.value);
console.log("Value changed:", event.detail.value);
});
const inputWithHandler = <ct-input value={value} onct-input={handleChange({ value })} />;
interface Input {
value: Cell<string>;
}

const MyRecipe = recipe<Input>(({ value }) => {
return {
[UI]: <div>
{simpleInput}
{inputWithHandler}
</div>,
[UI]: (
<div>
{/* Option 1: Bidirectional binding (simplest) */}
<ct-input $value={value} />

{/* Option 2: Inline handler for additional logic */}
<ct-input
value={value}
onct-input={(e) => {
value.set(e.detail.value);
console.log("Value changed:", e.detail.value);
}}
/>

{/* Option 3: handler() for complex/reusable logic */}
<ct-input value={value} onct-input={handleChange({ value })} />
</div>
),
value,
};
});

// If using handler() for complex logic
const handleChange = handler<
{ detail: { value: string } },
{ value: Cell<string> }
>((event, { value }) => {
value.set(event.detail.value);
console.log("Value changed:", event.detail.value);
// Additional complex logic...
});
```

Both inputs update the cell, but the second one logs changes. Use the simple bidirectional binding unless you need the extra logic.
**Recommendation:** Use bidirectional binding unless you need custom logic, then use inline handlers. Only use `handler()` for complex or reusable logic.

## ct-select

Expand Down
66 changes: 59 additions & 7 deletions docs/common/HANDLERS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
# CommonTools Handler Patterns Guide

## Inline Handlers (Preferred for Simple Cases)

**New in CommonTools**: You can now write event handlers directly inline without the `handler()` wrapper!

```typescript
interface Input {
count: Cell<number>;
title: Cell<string>;
}

export default recipe<Input>(({ count, title }) => {
return {
[UI]: (
<div>
{/* Simple click handler */}
<ct-button onClick={() => count.set(count.get() + 1)}>
Increment
</ct-button>

{/* Handler with event */}
<ct-input
value={title}
onct-input={(e) => title.set(e.detail.value)}
/>

{/* Multi-line handler */}
<ct-button onClick={() => {
const current = count.get();
if (current < 10) {
count.set(current + 1);
}
}}>
Increment (max 10)
</ct-button>
</div>
),
};
});
```

**Key points:**
- Declare cells as `Cell<T>` in your recipe input types
- Write arrow functions directly: `onClick={() => ...}` or `onClick={(e) => ...}`
- No `handler()` wrapper needed for simple cases
- Can still use `handler()` for complex/reusable logic

## When Do You Need Handlers?

**Important:** Many UI updates don't need handlers at all! CommonTools components support **bidirectional binding** with the `$` prefix, which automatically updates cells when users interact with components.
Expand All @@ -11,18 +57,24 @@
| Update checkbox | ✅ Bidirectional binding | `<ct-checkbox $checked={item.done} />` |
| Update text input | ✅ Bidirectional binding | `<ct-input $value={item.title} />` |
| Update dropdown | ✅ Bidirectional binding | `<ct-select $value={item.category} items={...} />` |
| Add item to list | ❌ Need handler | `addItem` handler with `items.set([...])` |
| Remove item from list | ❌ Need handler | `removeItem` handler with `toSpliced()` |
| Validate input | ❌ Need handler (or derive) | Handler with validation logic |
| Call API on change | ❌ Need handler | Handler with fetch/save logic |
| Log changes | ❌ Need handler | Handler with logging |

**Rule of thumb:** If you're just syncing UI ↔ cell with no additional logic, use bidirectional binding. If you need side effects, validation, or structural changes (add/remove), use handlers.
| Add item to list | ✅ Inline handler | `onClick={() => items.push({ title: "New" })}` |
| Remove item from list | ✅ Inline handler | `onClick={() => items.set(items.get().filter(i => i !== item))}` |
| Simple counter | ✅ Inline handler | `onClick={() => count.set(count.get() + 1)}` |
| Validate input | ✅ Inline handler or computed | Inline handler or `computed()` |
| Call API on change | ✅ Inline handler or `handler()` | For complex logic, use `handler()` |
| Complex reusable logic | ✅ `handler()` function | Module-level `handler()` |

**Rule of thumb:**
1. **Bidirectional binding** for simple UI ↔ cell sync (no logic needed)
2. **Inline handlers** for simple one-off operations
3. **`handler()` function** for complex or reusable logic

See `COMPONENTS.md` for detailed bidirectional binding examples.

## Handler Function Structure

**Note:** This section describes the `handler()` function for complex/reusable handlers. For simple cases, prefer inline handlers (see above).

Handlers in CommonTools follow a specific pattern that creates a factory function:

```typescript
Expand Down
Loading
Loading