creo 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +156 -0
- package/CHANGELOG.md +138 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +130 -87
- package/dist/index.js.map +10 -10
- package/dist/internal/internal_view.d.ts +6 -1
- package/dist/public/primitive.d.ts +9 -10
- package/dist/public/primitives/primitives.d.ts +341 -111
- package/dist/public/view.d.ts +27 -8
- package/dist/render/html_render.d.ts +1 -0
- package/docs/create-app.md +110 -0
- package/docs/events.md +236 -0
- package/docs/getting-started.md +201 -0
- package/docs/how-to/data-fetching.md +155 -0
- package/docs/how-to/deploy-vercel.md +130 -0
- package/docs/how-to/router.md +111 -0
- package/docs/how-to/styles.md +124 -0
- package/docs/how-to/suspense.md +116 -0
- package/docs/index.md +66 -0
- package/docs/lifecycle.md +173 -0
- package/docs/primitives.md +195 -0
- package/docs/renderers.md +183 -0
- package/docs/state.md +131 -0
- package/docs/store.md +135 -0
- package/docs/view.md +205 -0
- package/package.json +5 -2
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Lifecycle
|
|
2
|
+
|
|
3
|
+
Creo provides explicit, named lifecycle hooks on the `ViewBody` object. There are no dependency arrays — each hook has a clear purpose and timing.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
| Hook | When it runs |
|
|
8
|
+
|------|-------------|
|
|
9
|
+
| `onMount` | After the view's first render (DOM/output exists) |
|
|
10
|
+
| `shouldUpdate(nextProps)` | Before a re-render, to decide whether it should proceed |
|
|
11
|
+
| `onUpdateBefore` | Before each re-render (not called on first render) |
|
|
12
|
+
| `onUpdateAfter` | After each re-render (not called on first render) |
|
|
13
|
+
|
|
14
|
+
All hooks are optional properties on the object returned from a view function.
|
|
15
|
+
|
|
16
|
+
## onMount
|
|
17
|
+
|
|
18
|
+
Runs after the view's first render and after all children have been rendered. The DOM/output exists at this point. Use it for post-mount side effects like focusing an element, starting timers, or fetching data:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const ItemList = view(({ use }) => {
|
|
22
|
+
const data = use<string[]>([]);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
onMount() {
|
|
26
|
+
fetch("/api/items")
|
|
27
|
+
.then(r => r.json())
|
|
28
|
+
.then(items => data.set(items));
|
|
29
|
+
},
|
|
30
|
+
render() {
|
|
31
|
+
for (const item of data.get()) {
|
|
32
|
+
li(_, item);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`onMount` callbacks are batched — all views that mounted in the same render loop have their `onMount` called after the entire tree is settled.
|
|
40
|
+
|
|
41
|
+
## shouldUpdate
|
|
42
|
+
|
|
43
|
+
A predicate that decides whether the view should re-render when it receives new props. Return `true` to allow the render, `false` to skip it. This is equivalent to `React.memo`'s comparison function:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const ExpensiveList = view<{ items: string[]; label: string }>(({ props }) => {
|
|
47
|
+
return {
|
|
48
|
+
shouldUpdate(nextProps) {
|
|
49
|
+
return nextProps.items !== props().items;
|
|
50
|
+
},
|
|
51
|
+
render() {
|
|
52
|
+
div(_, () => {
|
|
53
|
+
text(props().label);
|
|
54
|
+
for (const item of props().items) {
|
|
55
|
+
li(_, item);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If `shouldUpdate` is not defined, Creo compares props by **shallow equality**. The view re-renders when its own props shallow-differ, when a subscribed `use()` value changes, or when the slot's structure changes — a parent re-render alone doesn't force a child re-render.
|
|
64
|
+
|
|
65
|
+
## onUpdateBefore
|
|
66
|
+
|
|
67
|
+
Runs synchronously before each re-render (not on the first render). Use it for pre-render calculations or logging:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const Animated = view(({ use }) => {
|
|
71
|
+
const value = use(0);
|
|
72
|
+
let prevValue = 0;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
onUpdateBefore() {
|
|
76
|
+
prevValue = value.get();
|
|
77
|
+
},
|
|
78
|
+
render() {
|
|
79
|
+
div({ class: prevValue !== value.get() ? "changed" : "" }, () => {
|
|
80
|
+
text(String(value.get()));
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## onUpdateAfter
|
|
88
|
+
|
|
89
|
+
Runs synchronously after each re-render (not on the first render). The DOM/output reflects the new state:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const WordCount = view<{ text: string }>(({ props, use }) => {
|
|
93
|
+
const count = use(0);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
onUpdateAfter() {
|
|
97
|
+
// Props just changed; derive a value from them and push it somewhere.
|
|
98
|
+
count.set(props().text.trim().split(/\s+/).filter(Boolean).length);
|
|
99
|
+
console.log(`re-rendered with ${count.get()} words`);
|
|
100
|
+
},
|
|
101
|
+
render() {
|
|
102
|
+
div(_, () => {
|
|
103
|
+
p(_, props().text);
|
|
104
|
+
p({ class: "meta" }, `${count.get()} words`);
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`onUpdateAfter` is for observing the result of a render — reading measurements, pushing metrics, scheduling follow-up work. It fires after DOM mutations are applied, so any reads you do see the latest layout.
|
|
112
|
+
|
|
113
|
+
## Cleanup
|
|
114
|
+
|
|
115
|
+
When a view is removed from the tree (its parent no longer renders it), the view and all its descendants are disposed automatically during reconciliation. The engine calls the renderer's `unmount` to clean up output artifacts and removes the view from the dirty queue.
|
|
116
|
+
|
|
117
|
+
There is no dedicated unmount hook. For cleanup of resources (timers, subscriptions, event listeners) started in `onMount`, tie them to module-scoped stores or rely on `setInterval`/`setTimeout` completing on page unload. If you need guaranteed teardown, pair the resource with a reactive `store` and clear it from a parent view when the child is conditionally removed.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const Poller = view(({ use }) => {
|
|
121
|
+
const data = use("");
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
onMount() {
|
|
125
|
+
setInterval(() => {
|
|
126
|
+
fetch("/api/status")
|
|
127
|
+
.then(r => r.text())
|
|
128
|
+
.then(t => data.set(t));
|
|
129
|
+
}, 5000);
|
|
130
|
+
},
|
|
131
|
+
render() {
|
|
132
|
+
text(data.get());
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Complete example
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const Dashboard = view<{ userId: string }>(({ props, use }) => {
|
|
142
|
+
const profile = use<{ name: string } | null>(null);
|
|
143
|
+
let renderCount = 0;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
onMount() {
|
|
147
|
+
fetch(`/api/users/${props().userId}`)
|
|
148
|
+
.then(r => r.json())
|
|
149
|
+
.then(data => profile.set(data));
|
|
150
|
+
},
|
|
151
|
+
shouldUpdate(nextProps) {
|
|
152
|
+
return nextProps.userId !== props().userId;
|
|
153
|
+
},
|
|
154
|
+
onUpdateBefore() {
|
|
155
|
+
renderCount++;
|
|
156
|
+
console.log(`Re-render #${renderCount}`);
|
|
157
|
+
},
|
|
158
|
+
onUpdateAfter() {
|
|
159
|
+
console.log("Dashboard updated");
|
|
160
|
+
},
|
|
161
|
+
render() {
|
|
162
|
+
div({ class: "dashboard" }, () => {
|
|
163
|
+
const p = profile.get();
|
|
164
|
+
if (p) {
|
|
165
|
+
h1(_, p.name);
|
|
166
|
+
} else {
|
|
167
|
+
text("Loading...");
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
```
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Primitives
|
|
2
|
+
|
|
3
|
+
Primitives are the leaf-level building blocks in Creo. They correspond to HTML elements and are called as functions inside `render()`.
|
|
4
|
+
|
|
5
|
+
## Built-in HTML elements
|
|
6
|
+
|
|
7
|
+
Creo exports pre-defined primitives for all standard HTML elements. Import them directly:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { div, span, p, h1, button, input, text } from "creo";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Calling primitives
|
|
14
|
+
|
|
15
|
+
Primitives accept two optional arguments:
|
|
16
|
+
|
|
17
|
+
1. **Props** -- an object with HTML attributes and event handlers.
|
|
18
|
+
2. **Slot** -- a `() => void` callback for child content, or a `PendingView[]` for passthrough children.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// No props, no children
|
|
22
|
+
br();
|
|
23
|
+
hr();
|
|
24
|
+
|
|
25
|
+
// Props only
|
|
26
|
+
input({ type: "text", placeholder: "Enter name" });
|
|
27
|
+
img({ src: "/logo.png", alt: "Logo" });
|
|
28
|
+
|
|
29
|
+
// Props and children
|
|
30
|
+
div({ class: "card", id: "main" }, () => {
|
|
31
|
+
h1({}, () => { text("Title"); });
|
|
32
|
+
p({}, () => { text("Content goes here."); });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Children only (pass undefined or omit props)
|
|
36
|
+
div({}, () => {
|
|
37
|
+
text("Hello");
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### text()
|
|
42
|
+
|
|
43
|
+
`text()` renders a text node. It accepts a string or number:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
text("Hello, world");
|
|
47
|
+
text(42);
|
|
48
|
+
text(count.get());
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`text()` does not accept children.
|
|
52
|
+
|
|
53
|
+
## Available elements
|
|
54
|
+
|
|
55
|
+
### Layout / structural
|
|
56
|
+
|
|
57
|
+
`div`, `span`, `section`, `article`, `aside`, `nav`, `header`, `footer`, `main`
|
|
58
|
+
|
|
59
|
+
### Sectioning
|
|
60
|
+
|
|
61
|
+
`address`, `hgroup`, `search`
|
|
62
|
+
|
|
63
|
+
### Text / headings
|
|
64
|
+
|
|
65
|
+
`p`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `pre`, `code`, `em`, `strong`, `small`, `br`, `hr`, `blockquote`
|
|
66
|
+
|
|
67
|
+
### Links and labels
|
|
68
|
+
|
|
69
|
+
`a` (with `href`, `target` attrs), `label` (with `for` attr)
|
|
70
|
+
|
|
71
|
+
### Text semantics
|
|
72
|
+
|
|
73
|
+
`abbr`, `b`, `bdi`, `bdo`, `cite`, `data`, `dfn`, `i`, `kbd`, `mark`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `sub`, `sup`, `time`, `u`, `varEl`, `wbr`
|
|
74
|
+
|
|
75
|
+
### Lists
|
|
76
|
+
|
|
77
|
+
`ul`, `ol`, `li`, `dl`, `dt`, `dd`
|
|
78
|
+
|
|
79
|
+
### Tables
|
|
80
|
+
|
|
81
|
+
`table`, `thead`, `tbody`, `tfoot`, `tr`, `th` (with `colspan`, `rowspan`, `scope`), `td` (with `colspan`, `rowspan`), `caption`, `colgroup`, `col`
|
|
82
|
+
|
|
83
|
+
### Forms
|
|
84
|
+
|
|
85
|
+
`form`, `button`, `input`, `textarea`, `select`, `option`, `fieldset`, `legend`, `datalist`, `optgroup`, `output`, `progress`, `meter`
|
|
86
|
+
|
|
87
|
+
Form elements like `input`, `textarea`, and `select` support `FormEvents` (includes `onInput`, `onChange`). Other elements use `ContainerEvents`.
|
|
88
|
+
|
|
89
|
+
### Media
|
|
90
|
+
|
|
91
|
+
`img`, `video`, `audio`, `canvas`, `source`, `track`, `map`, `area`, `picture`
|
|
92
|
+
|
|
93
|
+
### Embedded
|
|
94
|
+
|
|
95
|
+
`iframe`, `embed`, `object`, `portal`, `svg`
|
|
96
|
+
|
|
97
|
+
### Interactive
|
|
98
|
+
|
|
99
|
+
`details` (with `open`), `summary`, `dialog` (with `open`), `menu`
|
|
100
|
+
|
|
101
|
+
### Figure
|
|
102
|
+
|
|
103
|
+
`figure`, `figcaption`
|
|
104
|
+
|
|
105
|
+
## HtmlAttrs
|
|
106
|
+
|
|
107
|
+
All built-in primitives share a common attribute base:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
type HtmlAttrs = {
|
|
111
|
+
class?: string;
|
|
112
|
+
id?: string;
|
|
113
|
+
style?: string;
|
|
114
|
+
title?: string;
|
|
115
|
+
tabindex?: number;
|
|
116
|
+
hidden?: boolean;
|
|
117
|
+
role?: string;
|
|
118
|
+
draggable?: boolean;
|
|
119
|
+
[attr: string]: unknown; // open index signature for any HTML attribute
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The open index signature means you can pass any attribute -- Creo will set it via `setAttribute`.
|
|
124
|
+
|
|
125
|
+
## The html() factory
|
|
126
|
+
|
|
127
|
+
`html(tag)` creates a primitive for any HTML tag at runtime. All built-in primitives are created this way:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { html } from "creo";
|
|
131
|
+
|
|
132
|
+
// Create a custom element primitive
|
|
133
|
+
const myWidget = html("my-widget");
|
|
134
|
+
|
|
135
|
+
// Use it in render
|
|
136
|
+
myWidget({ class: "fancy" }, () => {
|
|
137
|
+
text("Inside custom element");
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
You can specify custom attribute and event types via generics:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const video = html<
|
|
145
|
+
HtmlAttrs & { src?: string; controls?: boolean; autoplay?: boolean },
|
|
146
|
+
ContainerEvents
|
|
147
|
+
>("video");
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`html()` caches primitives by tag name -- calling `html("div")` twice returns the same primitive.
|
|
151
|
+
|
|
152
|
+
## The primitive() factory
|
|
153
|
+
|
|
154
|
+
For completely custom primitives (not backed by an HTML tag), use `primitive()`:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { primitive } from "creo";
|
|
158
|
+
import type { PrimitiveComponent } from "creo";
|
|
159
|
+
|
|
160
|
+
type CanvasAttrs = { width: number; height: number };
|
|
161
|
+
type CanvasEvents = { click: (e: PointerEventData) => void };
|
|
162
|
+
|
|
163
|
+
const myCanvas: PrimitiveComponent<CanvasAttrs, CanvasEvents> = primitive<CanvasAttrs, CanvasEvents>();
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Custom primitives need a render handler registered on the renderer to produce output. See [Renderers](./renderers.md) for details.
|
|
167
|
+
|
|
168
|
+
## Passing children
|
|
169
|
+
|
|
170
|
+
Primitives accept children in two forms:
|
|
171
|
+
|
|
172
|
+
### Slot callback
|
|
173
|
+
|
|
174
|
+
A `() => void` function called at the call site. The engine collects child calls made inside it:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
div({ class: "wrapper" }, () => {
|
|
178
|
+
p({}, () => { text("Hello"); });
|
|
179
|
+
span({}, () => { text("World"); });
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### PendingView array (passthrough)
|
|
184
|
+
|
|
185
|
+
When a view receives `ctx.children` from its parent, it can pass that array directly as the second argument:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const Card = view((ctx) => ({
|
|
189
|
+
render() {
|
|
190
|
+
div({ class: "card" }, ctx.children);
|
|
191
|
+
},
|
|
192
|
+
}));
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
This avoids re-collecting children and preserves the parent's pending views directly.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Renderers
|
|
2
|
+
|
|
3
|
+
Creo separates the component model from the output target. Renderers implement the `IRender` interface to translate the virtual DOM into a specific output format.
|
|
4
|
+
|
|
5
|
+
## Built-in renderers
|
|
6
|
+
|
|
7
|
+
### HtmlRender
|
|
8
|
+
|
|
9
|
+
Renders to the browser DOM. This is the primary renderer for web applications.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createApp, HtmlRender } from "creo";
|
|
13
|
+
|
|
14
|
+
const container = document.getElementById("app")!;
|
|
15
|
+
createApp(() => App(), new HtmlRender(container)).mount();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`HtmlRender` handles:
|
|
19
|
+
- Creating and updating DOM elements
|
|
20
|
+
- Attribute diffing (only changed attributes are touched)
|
|
21
|
+
- Event listener binding and cleanup
|
|
22
|
+
- DOM properties (`value`, `checked`, `selected`) set directly instead of via `setAttribute`
|
|
23
|
+
- Keyed reordering of child nodes
|
|
24
|
+
- Boolean attribute handling (`disabled`, `hidden`, etc.)
|
|
25
|
+
- `autofocus` support on mount
|
|
26
|
+
|
|
27
|
+
### JsonRender
|
|
28
|
+
|
|
29
|
+
Produces a JSON tree representation of the UI. Useful for testing and serialization.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createApp, JsonRender } from "creo";
|
|
33
|
+
import type { JsonNode } from "creo";
|
|
34
|
+
|
|
35
|
+
const renderer = new JsonRender();
|
|
36
|
+
createApp(() => App(), renderer).mount();
|
|
37
|
+
|
|
38
|
+
const tree: JsonNode | undefined = renderer.root;
|
|
39
|
+
// {
|
|
40
|
+
// type: "div",
|
|
41
|
+
// props: { class: "app" },
|
|
42
|
+
// children: [ ... ],
|
|
43
|
+
// key: undefined
|
|
44
|
+
// }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The `JsonNode` type:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
type JsonNode = {
|
|
51
|
+
type: string;
|
|
52
|
+
props: Record<string, unknown>;
|
|
53
|
+
children: JsonNode[];
|
|
54
|
+
key?: string | number;
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### StringRender
|
|
59
|
+
|
|
60
|
+
Produces an HTML string from the virtual DOM. Useful for server-side rendering.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { createApp, StringRender } from "creo";
|
|
64
|
+
|
|
65
|
+
const renderer = new StringRender();
|
|
66
|
+
createApp(() => App(), renderer).mount();
|
|
67
|
+
|
|
68
|
+
const html: string = renderer.renderToString();
|
|
69
|
+
// "<div><h1>Hello</h1><p>World</p></div>"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`StringRender` is pull-based -- `render()` and `unmount()` are essentially no-ops. Call `renderToString()` to walk the virtual DOM and build the HTML string on demand.
|
|
73
|
+
|
|
74
|
+
## The IRender interface
|
|
75
|
+
|
|
76
|
+
All renderers implement `IRender<Output>`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
interface IRender<Output> {
|
|
80
|
+
/** Create output if view is new, or update if existing. */
|
|
81
|
+
render(view: BaseView): void;
|
|
82
|
+
|
|
83
|
+
/** Remove a view's output artifacts. Called on disposal. */
|
|
84
|
+
unmount(view: BaseView): void;
|
|
85
|
+
|
|
86
|
+
/** Register render handlers for custom primitive components. */
|
|
87
|
+
registerPrimitive(
|
|
88
|
+
entries: [PrimitiveComponent<any, any>, PrimitiveRenderHandler<Output>][],
|
|
89
|
+
): void;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The `Output` type parameter describes what each primitive produces (e.g., `HTMLElement | Text` for HtmlRender, `JsonNode` for JsonRender, `string` for StringRender).
|
|
94
|
+
|
|
95
|
+
## Custom renderers
|
|
96
|
+
|
|
97
|
+
To create a custom renderer, implement `IRender<YourOutput>`:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import type { IRender, PrimitiveRenderHandler } from "creo";
|
|
101
|
+
import type { PrimitiveComponent } from "creo";
|
|
102
|
+
import type { BaseView } from "creo"; // available via internal types
|
|
103
|
+
|
|
104
|
+
class CanvasRender implements IRender<void> {
|
|
105
|
+
private ctx: CanvasRenderingContext2D;
|
|
106
|
+
|
|
107
|
+
constructor(canvas: HTMLCanvasElement) {
|
|
108
|
+
this.ctx = canvas.getContext("2d")!;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
render(view: BaseView): void {
|
|
112
|
+
// Draw or update based on view.props, view.renderRef, etc.
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
unmount(view: BaseView): void {
|
|
116
|
+
// Clean up any resources
|
|
117
|
+
view.renderRef = undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
registerPrimitive(
|
|
121
|
+
entries: [PrimitiveComponent<any, any>, PrimitiveRenderHandler<void>][],
|
|
122
|
+
): void {
|
|
123
|
+
// Store handlers for custom primitives
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Registering custom primitives
|
|
129
|
+
|
|
130
|
+
When you create a primitive with `primitive()` (not `html()`), you must register a render handler on the renderer so it knows how to produce output:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { primitive, createApp, HtmlRender } from "creo";
|
|
134
|
+
|
|
135
|
+
// Define a custom primitive
|
|
136
|
+
const sparkline = primitive<{ data: number[]; width: number; height: number }>();
|
|
137
|
+
|
|
138
|
+
// Create renderer and register the handler
|
|
139
|
+
const renderer = new HtmlRender(document.getElementById("app")!);
|
|
140
|
+
renderer.registerPrimitive([
|
|
141
|
+
[sparkline, {
|
|
142
|
+
render(view) {
|
|
143
|
+
const canvas = document.createElement("canvas");
|
|
144
|
+
canvas.width = view.props.width;
|
|
145
|
+
canvas.height = view.props.height;
|
|
146
|
+
// draw sparkline on canvas...
|
|
147
|
+
return canvas;
|
|
148
|
+
},
|
|
149
|
+
}],
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
// Now sparkline() can be used in render functions
|
|
153
|
+
createApp(() => App(), renderer).mount();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The `PrimitiveRenderHandler<Output>` interface:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
interface PrimitiveRenderHandler<Output> {
|
|
160
|
+
render(view: BaseView): Output;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Scheduler integration
|
|
165
|
+
|
|
166
|
+
The renderer is paired with a scheduler via `createApp` options. The scheduler controls when re-renders happen:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// Default: queueMicrotask (immediate, within same task)
|
|
170
|
+
createApp(() => App(), new HtmlRender(el)).mount();
|
|
171
|
+
|
|
172
|
+
// requestAnimationFrame (synced to display refresh)
|
|
173
|
+
createApp(() => App(), new HtmlRender(el), {
|
|
174
|
+
scheduler: requestAnimationFrame,
|
|
175
|
+
}).mount();
|
|
176
|
+
|
|
177
|
+
// Custom scheduler
|
|
178
|
+
createApp(() => App(), new HtmlRender(el), {
|
|
179
|
+
scheduler: (cb) => setTimeout(cb, 16),
|
|
180
|
+
}).mount();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The `Scheduler` type is `(callback: () => void) => void`.
|
package/docs/state.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# State
|
|
2
|
+
|
|
3
|
+
Creo's state system provides reactive values that trigger re-renders when changed.
|
|
4
|
+
|
|
5
|
+
## Creating state
|
|
6
|
+
|
|
7
|
+
Call `use(initial)` in the view function body (before `return`):
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { view, text } from "creo";
|
|
11
|
+
|
|
12
|
+
const Counter = view(({ use }) => {
|
|
13
|
+
const count = use(0);
|
|
14
|
+
const name = use("untitled");
|
|
15
|
+
const items = use<string[]>([]);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
render() {
|
|
19
|
+
text(String(count.get()));
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Rules
|
|
26
|
+
|
|
27
|
+
- Call `use()` **before** returning the ViewBody, never inside `render()`.
|
|
28
|
+
- Call order must be stable across re-renders (same as React hooks). Do not call `use()` inside conditionals or loops.
|
|
29
|
+
- On the first render, `use(initial)` creates a new `Reactive<T>` instance. On subsequent renders, it returns the existing instance at the same position (the `initial` argument is ignored).
|
|
30
|
+
|
|
31
|
+
## Reading state
|
|
32
|
+
|
|
33
|
+
Use `.get()` to read the current value:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const count = use(0);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
render() {
|
|
40
|
+
text(String(count.get())); // reads current value
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`.get()` always returns the latest committed value, including any changes made by `.set()` or `.update()` earlier in the same cycle.
|
|
46
|
+
|
|
47
|
+
## Setting state
|
|
48
|
+
|
|
49
|
+
### .set(value)
|
|
50
|
+
|
|
51
|
+
Replace the current value and schedule a re-render:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const name = use("Alice");
|
|
55
|
+
|
|
56
|
+
const rename = () => name.set("Bob");
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`.set()` applies the value **immediately** -- calling `.get()` right after `.set()` returns the new value. A re-render is then scheduled through the engine's scheduler.
|
|
60
|
+
|
|
61
|
+
### .update(fn)
|
|
62
|
+
|
|
63
|
+
Apply a function to the current value:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const count = use(0);
|
|
67
|
+
|
|
68
|
+
const increment = () => count.update(n => n + 1);
|
|
69
|
+
const decrement = () => count.update(n => n - 1);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Like `.set()`, the update is applied immediately and a re-render is scheduled.
|
|
73
|
+
|
|
74
|
+
### Async updates
|
|
75
|
+
|
|
76
|
+
`.update()` also accepts async functions. The value is applied and render is scheduled when the promise resolves:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const data = use<string[]>([]);
|
|
80
|
+
|
|
81
|
+
const fetchData = () => data.update(async current => {
|
|
82
|
+
const response = await fetch("/api/items");
|
|
83
|
+
const items = await response.json();
|
|
84
|
+
return items;
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## State vs plain variables
|
|
89
|
+
|
|
90
|
+
Use `use()` for values that should trigger re-renders when they change. For ephemeral values that do not affect the rendered output, a plain `let` variable is sufficient:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const MyComponent = view(({ use }) => {
|
|
94
|
+
const count = use(0); // reactive -- triggers re-render on change
|
|
95
|
+
let lastClickTime = 0; // not reactive -- no re-render needed
|
|
96
|
+
|
|
97
|
+
const handleClick = () => {
|
|
98
|
+
lastClickTime = Date.now();
|
|
99
|
+
count.update(n => n + 1);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
render() {
|
|
104
|
+
button({ on: { click: handleClick } }, () => {
|
|
105
|
+
text(String(count.get()));
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## The Reactive interface
|
|
113
|
+
|
|
114
|
+
Both `use(initial)` (local state) and `use(store)` (store binding) return `Reactive<T>`:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import type { Reactive } from "creo";
|
|
118
|
+
|
|
119
|
+
function doubleReactive(r: Reactive<number>): void {
|
|
120
|
+
r.update(n => n * 2);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Full API
|
|
125
|
+
|
|
126
|
+
| Method | Description |
|
|
127
|
+
|--------|-------------|
|
|
128
|
+
| `.get(): T` | Read the current value |
|
|
129
|
+
| `.set(value: T): void` | Set a new value immediately, schedule re-render |
|
|
130
|
+
| `.update(fn: (current: T) => T): void` | Apply a sync transform, schedule re-render |
|
|
131
|
+
| `.update(fn: (current: T) => Promise<T>): void` | Apply an async transform, schedule re-render on resolve |
|