rasti 4.1.0-alpha.0 → 4.1.0-alpha.2
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/README.md +109 -11
- package/package.json +1 -1
- package/types/Component.d.ts +56 -8
- package/types/Emitter.d.ts +4 -3
- package/types/View.d.ts +11 -3
- package/types/index.d.ts +3 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.1.0-alpha.
|
|
4
|
-
<img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.1.0-alpha.
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.1.0-alpha.2/docs/logo-dark.svg">
|
|
4
|
+
<img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.1.0-alpha.2/docs/logo.svg" height="120">
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
@@ -184,9 +184,35 @@ Counter.mount({ model }, document.body);
|
|
|
184
184
|
- **Just the Right Abstraction**
|
|
185
185
|
Keeps you close to the DOM with no over-engineering. Fully hackable — if you're curious about how something works, just check the source code.
|
|
186
186
|
|
|
187
|
+
## Scaffolding a New Project
|
|
188
|
+
|
|
189
|
+
The fastest way to start a real-world **Rasti** project is [`create-rasti`](https://github.com/8tentaculos/create-rasti), the official scaffolding tool. It generates a ready-to-use **Rasti** + **Vite** setup with optional server-side rendering, routing, styling, and icon components.
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# Interactive setup
|
|
193
|
+
npm create rasti
|
|
194
|
+
|
|
195
|
+
# Non-interactive single-page app
|
|
196
|
+
npm create rasti my-app
|
|
197
|
+
|
|
198
|
+
# Server-side rendering with routing and Tailwind CSS
|
|
199
|
+
npm create rasti my-app --ssr --router --tailwind
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Available options include:
|
|
203
|
+
|
|
204
|
+
- **Rendering** — Single-page app (default), server-side rendering (`--ssr`), or static pre-rendering (`--static`).
|
|
205
|
+
- **Styling** — Plain CSS (default), Tailwind CSS (`--tailwind`), or CSSFUN with light/dark theme support (`--cssfun`).
|
|
206
|
+
- **Routing** (`--router`) — A small universal router built on `path-to-regexp`.
|
|
207
|
+
- **Icons** (`--icons`) — Generate **Rasti** components from popular SVG icon sets (heroicons, akar-icons, feathericon, pixelarticons, and more).
|
|
208
|
+
|
|
209
|
+
See the [`create-rasti` repository](https://github.com/8tentaculos/create-rasti) for the full list of templates and options.
|
|
210
|
+
|
|
187
211
|
## Example
|
|
188
212
|
|
|
189
|
-
|
|
213
|
+
To see how **Rasti**'s API and architecture come together in a small app, explore the sample **TODO application** in the [example folder](https://github.com/8tentaculos/rasti/tree/master/example/todo) of the **Rasti** [GitHub repository](https://github.com/8tentaculos/rasti). It's a concise, self-contained reference for understanding how models, views, and components fit together in a simple application. Try it live [here](https://rasti.js.org/example/todo/index.html).
|
|
214
|
+
|
|
215
|
+
To scaffold a real-world project, use [`create-rasti`](#scaffolding-a-new-project).
|
|
190
216
|
|
|
191
217
|
## API Documentation
|
|
192
218
|
|
|
@@ -219,6 +245,47 @@ const Plain = Component.create`<div></div>`;
|
|
|
219
245
|
new Plain({ anything: 'goes' }); // ✅
|
|
220
246
|
```
|
|
221
247
|
|
|
248
|
+
When a component is used with inner content (`<${Card}>...</${Card}>`), rasti injects a `renderChildren` function into its props at runtime. Declare it in `P` to use it:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
const Card = Component.create<{ title: string; renderChildren?: () => any }>`
|
|
252
|
+
<div class="card">
|
|
253
|
+
<h2>${({ props }) => props.title}</h2>
|
|
254
|
+
${({ props }) => props.renderChildren?.()}
|
|
255
|
+
</div>
|
|
256
|
+
`;
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`Component.extend` adds the object members to the instance type. Inside its methods, `this` is the extended component, and lifecycle overrides get their parameters typed automatically:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
const Counter = Component.create<{ initial: number }>`<div>...</div>`.extend({
|
|
263
|
+
onCreate() {
|
|
264
|
+
this.state = new Model({ count: this.props.initial }); // `this` is typed
|
|
265
|
+
},
|
|
266
|
+
onChange(model, changed) { // parameters typed automatically
|
|
267
|
+
if ('count' in changed) this.render();
|
|
268
|
+
},
|
|
269
|
+
increment() { this.state.count++; },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
Counter.mount({ initial: 0 }, document.body).increment(); // ✅ increment is typed
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
To read attributes off a typed `state` (or `model`) directly, define it as a named `Model` subclass with declaration merging and pass it as the `S` (or `M`) generic — then there are no casts anywhere:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
class ScoreState extends Model<{ points: number }> {}
|
|
279
|
+
interface ScoreState { points: number } // exposes this.points
|
|
280
|
+
|
|
281
|
+
class Scoreboard extends Component<{}, ScoreState> {
|
|
282
|
+
onCreate() {
|
|
283
|
+
this.state = new ScoreState({ points: 0 });
|
|
284
|
+
this.state.points++; // ✅ typed, no cast
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
222
289
|
### Models
|
|
223
290
|
|
|
224
291
|
Type the attributes with `Model<Attrs>`. Use **declaration merging** to surface the auto-generated getters/setters as instance properties:
|
|
@@ -267,25 +334,56 @@ type P = Props<Counter>; // CounterProps
|
|
|
267
334
|
type S = State<Counter>; // CounterState
|
|
268
335
|
```
|
|
269
336
|
|
|
337
|
+
### Typing template interpolations
|
|
338
|
+
|
|
339
|
+
Functions inside a template are `any` — rasti can't infer them from the surrounding string. Typing is **opt-in**: annotate where you want safety, leave the rest as `any`. Which type to use depends on how rasti treats the function (quoted attribute or content → run on render; unquoted attribute → passed as-is):
|
|
340
|
+
|
|
341
|
+
| Interpolation | What it is | Type to use |
|
|
342
|
+
|---|---|---|
|
|
343
|
+
| Content `${fn}` or quoted attr `attr="${fn}"` | Run on render; `this` and the argument are the component | `RenderExpression<C>` |
|
|
344
|
+
| Unquoted `onX=${fn}` | DOM handler, called `(event, component, matched)` | `EventHandler<C, E>` |
|
|
345
|
+
| Function passed to a child (`handler=${fn}`) | Becomes the child's prop; typed by the child, not this component | the child's prop signature |
|
|
346
|
+
|
|
347
|
+
Three ways to apply them:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
// 1. Named const — cleanest for non-trivial handlers
|
|
351
|
+
const onClick: EventHandler<Home, MouseEvent> = function(ev, self) {
|
|
352
|
+
ev.preventDefault();
|
|
353
|
+
self.close();
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// 2. Inline with `satisfies` — checks + types the params without widening
|
|
357
|
+
${(({ state }) => state.location) satisfies RenderExpression<Home>}
|
|
358
|
+
|
|
359
|
+
// 3. Bare annotation — lightest, just types the argument
|
|
360
|
+
${({ state }: Home) => state.location}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
For a function passed to a child, neither helper fits — its type comes from the child's prop. Type it against that prop's declared type (rasti can't connect the attribute to the child, since both live inside the template string):
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
// where the child was created with Component.create<ToggleAllProps>`...`
|
|
367
|
+
handleChange=${((checked) => model.toggleAll(checked)) satisfies ToggleAllProps['handleChange']}
|
|
368
|
+
```
|
|
369
|
+
|
|
270
370
|
### Known limitations
|
|
271
371
|
|
|
272
|
-
- **Template interpolation callbacks are `any`**. Functions
|
|
372
|
+
- **Template interpolation callbacks are `any`**. Functions in `Component.create\`...\`` templates can't be inferred from the surrounding string — type them opt-in (see [Typing template interpolations](#typing-template-interpolations)).
|
|
273
373
|
- **`Model<A>` instance keys require declaration merging**. TypeScript can't add `A`'s keys to a `class extends Model<A>` automatically — see the `interface Todo extends TodoAttrs {}` pattern above.
|
|
274
374
|
- **`this.$()` can return `null`**. It mirrors `querySelector`, so handle the empty case (`?.`) and pass a type argument to narrow the element: `this.$<HTMLInputElement>('input.edit')?.focus()`. `this.$$()` returns a `NodeListOf<HTMLElement>` (also narrowable).
|
|
275
375
|
- **`this.model` / `this.state` are optional**. Both are `undefined` unless provided, so guard (`this.model?.foo`) or assert (`this.model!`) when you know one was passed. Both accept a Rasti `Model` or a model from another library (e.g. Backbone); Components subscribe to `change` events automatically when the object exposes `on`/`off`.
|
|
376
|
+
- **`state` / `model` are raw generics, `props` is not**. `this.props` is *always* a `Model` built by rasti, so it's typed `Model<P> & P` (direct access to `P`'s keys). But `state` and `model` can be anything you provide — a Rasti `Model`, a Backbone model, a store, or a plain object — so they stay the raw generic. To read a typed `Model` state/model directly, define it as a named subclass with declaration merging and pass it as the `S`/`M` generic (see the `Scoreboard` example above) — no casts needed.
|
|
377
|
+
- **Weak-type error on narrow props**. If a component's `P` has no required keys and you pass only options not declared in it, TypeScript reports *"has no properties in common"* (weak-type check). Fix: declare those options in `P` — non-reserved options become props at runtime.
|
|
378
|
+
- **Instance fields set in `.extend` hooks need predeclaration**. `.extend` infers the instance type from the object's members only, so a field first assigned in `onCreate` (`this.router = ...`) isn't known. Predeclare it in the object: `router: null as unknown as Router`. For components with many instance fields, `class MyComponent extends Component<P, S>` is usually cleaner than `.extend`.
|
|
276
379
|
|
|
277
380
|
## Working with LLMs
|
|
278
381
|
|
|
279
382
|
For those working with LLMs, there is an [AI Agents reference guide](/docs/AGENTS.md) that provides API patterns, lifecycle methods, and best practices, optimized for LLM context. You can share this guide with AI assistants to help them understand **Rasti**'s architecture and component APIs.
|
|
280
383
|
|
|
281
|
-
##
|
|
282
|
-
|
|
283
|
-
We strive to minimize breaking changes between major versions. However, if you're migrating between major versions, please refer to the release notes below for details on any breaking changes and migration tips.
|
|
384
|
+
## Changelog
|
|
284
385
|
|
|
285
|
-
|
|
286
|
-
- **[v3.0.0](https://github.com/8tentaculos/rasti/releases/tag/v3.0.0)**
|
|
287
|
-
- **[v2.0.0](https://github.com/8tentaculos/rasti/releases/tag/v2.0.0)**
|
|
288
|
-
- **[v1.0.0](https://github.com/8tentaculos/rasti/releases/tag/v1.0.0)**
|
|
386
|
+
Release history and migration notes for major versions are in [CHANGELOG.md](CHANGELOG.md).
|
|
289
387
|
|
|
290
388
|
## License
|
|
291
389
|
|
package/package.json
CHANGED
package/types/Component.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface SafeHTML {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** A partial template produced by `this.partial` — preserves structure for position-based recycling. */
|
|
41
|
-
export interface
|
|
41
|
+
export interface ComponentPartial {
|
|
42
42
|
strings: TemplateStringsArray;
|
|
43
43
|
expressions: any[];
|
|
44
44
|
}
|
|
@@ -75,7 +75,40 @@ export type Props<C> = C extends Component<infer P, any, any> ? P : never;
|
|
|
75
75
|
/** Extracts the state type `S` from a Component subclass. */
|
|
76
76
|
export type State<C> = C extends Component<any, infer S, any> ? S : never;
|
|
77
77
|
/** Extracts the model type `M` from a Component / View subclass. */
|
|
78
|
-
export type ComponentModel<C> =
|
|
78
|
+
export type ComponentModel<C> =
|
|
79
|
+
C extends Component<any, any, infer M> ? M :
|
|
80
|
+
C extends View<infer M> ? M :
|
|
81
|
+
never;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Lifecycle methods, with their real signatures, made available for contextual typing
|
|
85
|
+
* inside `Component.extend({ ... })` so overrides don't need parameter annotations.
|
|
86
|
+
*/
|
|
87
|
+
export interface ComponentLifecycle {
|
|
88
|
+
/** Lifecycle. Called at the end of the constructor. Runs on both client and server. */
|
|
89
|
+
onCreate?(...args: any[]): void;
|
|
90
|
+
/** Lifecycle. Called when model/state/props emit `change`. Default: triggers `render`. */
|
|
91
|
+
onChange?(model: object, changed: Record<string, any>, ...args: any[]): void;
|
|
92
|
+
/** Lifecycle. Called after the first render hydrates the DOM. Client only. */
|
|
93
|
+
onHydrate?(): void;
|
|
94
|
+
/** Lifecycle. Called at the start of `recycle`, before any recycling happens. */
|
|
95
|
+
onBeforeRecycle?(): void;
|
|
96
|
+
/** Lifecycle. Called when the component is recycled and its props are updated. */
|
|
97
|
+
onRecycle?(): void;
|
|
98
|
+
/** Lifecycle. Called at the start of `render` on update. */
|
|
99
|
+
onBeforeUpdate?(): void;
|
|
100
|
+
/** Lifecycle. Called at the end of `render` on update. */
|
|
101
|
+
onUpdate?(): void;
|
|
102
|
+
/** Lifecycle. Called when the component is destroyed. */
|
|
103
|
+
onDestroy?(...args: any[]): void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Class returned by `Component.extend`: keeps the statics and constructor signature of the
|
|
108
|
+
* parent class `T`, and adds the members of `O` to the instance type.
|
|
109
|
+
*/
|
|
110
|
+
export type ExtendedComponent<T extends new (...args: any[]) => any, O> =
|
|
111
|
+
Omit<T, never> & (new (...args: ConstructorParameters<T>) => InstanceType<T> & O);
|
|
79
112
|
|
|
80
113
|
/**
|
|
81
114
|
* Components are a special kind of `View` designed to be easily composable. Unlike views,
|
|
@@ -104,14 +137,23 @@ declare class Component<P = {}, S = any, M = any> extends View<M> {
|
|
|
104
137
|
|
|
105
138
|
/**
|
|
106
139
|
* Helper method used to extend a `Component`, creating a subclass.
|
|
140
|
+
*
|
|
141
|
+
* The members of `object` are added to the resulting instance type, and `this` inside
|
|
142
|
+
* its methods is typed as the extended component. Lifecycle overrides (`onCreate`,
|
|
143
|
+
* `onChange`, ...) get their parameters typed automatically.
|
|
144
|
+
*
|
|
107
145
|
* @param object Object with methods to add to the subclass, or a function that receives
|
|
108
146
|
* the parent prototype and returns such an object.
|
|
109
147
|
* @return The newly created Component subclass.
|
|
110
148
|
*/
|
|
111
|
-
static extend<T extends new (...args: any[]) => Component<any, any, any
|
|
149
|
+
static extend<T extends new (...args: any[]) => Component<any, any, any>, O extends object>(
|
|
150
|
+
this: T,
|
|
151
|
+
object: (proto: InstanceType<T>) => O & ThisType<InstanceType<T> & O>,
|
|
152
|
+
): ExtendedComponent<T, O>;
|
|
153
|
+
static extend<T extends new (...args: any[]) => Component<any, any, any>, O extends object>(
|
|
112
154
|
this: T,
|
|
113
|
-
object:
|
|
114
|
-
): T
|
|
155
|
+
object: O & ComponentLifecycle & ThisType<InstanceType<T> & O>,
|
|
156
|
+
): ExtendedComponent<T, O>;
|
|
115
157
|
|
|
116
158
|
/**
|
|
117
159
|
* Mount the component into the DOM.
|
|
@@ -157,6 +199,12 @@ declare class Component<P = {}, S = any, M = any> extends View<M> {
|
|
|
157
199
|
...expressions: any[]
|
|
158
200
|
): typeof Component<P, S, M>;
|
|
159
201
|
|
|
202
|
+
/**
|
|
203
|
+
* A unique key to identify the component, merged from options.
|
|
204
|
+
* Components with keys are recycled when the same key is found in the previous render.
|
|
205
|
+
*/
|
|
206
|
+
key?: string;
|
|
207
|
+
|
|
160
208
|
/**
|
|
161
209
|
* Props passed from the parent component, stored as a `Model` for reactive updates.
|
|
162
210
|
* Accessible directly (`this.props.foo`) or via `Model` API (`this.props.get('foo')`).
|
|
@@ -184,7 +232,7 @@ declare class Component<P = {}, S = any, M = any> extends View<M> {
|
|
|
184
232
|
|
|
185
233
|
/**
|
|
186
234
|
* Tagged template helper bound to the component instance.
|
|
187
|
-
* Returns a `
|
|
235
|
+
* Returns a `ComponentPartial` that preserves structure for position-based recycling.
|
|
188
236
|
* String literals are marked as safe HTML automatically.
|
|
189
237
|
*
|
|
190
238
|
* @example
|
|
@@ -192,7 +240,7 @@ declare class Component<P = {}, S = any, M = any> extends View<M> {
|
|
|
192
240
|
* return this.partial`<header><${Title}>${this.model.title}</${Title}></header>`;
|
|
193
241
|
* }
|
|
194
242
|
*/
|
|
195
|
-
partial(strings: TemplateStringsArray, ...expressions: any[]):
|
|
243
|
+
partial(strings: TemplateStringsArray, ...expressions: any[]): ComponentPartial;
|
|
196
244
|
|
|
197
245
|
/**
|
|
198
246
|
* Subscribes to a `change` event on a model or emitter and invokes `onChange`.
|
|
@@ -215,7 +263,7 @@ declare class Component<P = {}, S = any, M = any> extends View<M> {
|
|
|
215
263
|
onCreate(...args: any[]): void;
|
|
216
264
|
|
|
217
265
|
/** Lifecycle. Called when model/state/props emit `change`. Default: triggers `render`. */
|
|
218
|
-
onChange(model: object, changed:
|
|
266
|
+
onChange(model: object, changed: Record<string, any>, ...args: any[]): void;
|
|
219
267
|
|
|
220
268
|
/** Lifecycle. Called after the first render hydrates the DOM. Client only. */
|
|
221
269
|
onHydrate(): void;
|
package/types/Emitter.d.ts
CHANGED
|
@@ -87,7 +87,7 @@ export default class Emitter<E extends EventMap = EventMap> {
|
|
|
87
87
|
* @example
|
|
88
88
|
* this.listenTo(otherModel, 'change', this.render.bind(this));
|
|
89
89
|
*/
|
|
90
|
-
listenTo(emitter: Emitter<
|
|
90
|
+
listenTo<E2 extends EventMap, K extends keyof E2>(emitter: Emitter<E2>, type: K, listener: E2[K]): () => void;
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* Listen to an event of another emitter and remove the listener after it is called.
|
|
@@ -97,7 +97,7 @@ export default class Emitter<E extends EventMap = EventMap> {
|
|
|
97
97
|
* @param listener The listener to call when the event is emitted.
|
|
98
98
|
* @return A function to stop listening to the event.
|
|
99
99
|
*/
|
|
100
|
-
listenToOnce(emitter: Emitter<
|
|
100
|
+
listenToOnce<E2 extends EventMap, K extends keyof E2>(emitter: Emitter<E2>, type: K, listener: E2[K]): () => void;
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* Stop listening to events from other emitters.
|
|
@@ -107,5 +107,6 @@ export default class Emitter<E extends EventMap = EventMap> {
|
|
|
107
107
|
* - `stopListening(emitter, type)` - Stops listening to the specified event type from the specified emitter
|
|
108
108
|
* - `stopListening(emitter, type, listener)` - Stops the specific listener for the specific event
|
|
109
109
|
*/
|
|
110
|
-
stopListening(
|
|
110
|
+
stopListening(): void;
|
|
111
|
+
stopListening<E2 extends EventMap, K extends keyof E2>(emitter: Emitter<E2>, type?: K, listener?: E2[K]): void;
|
|
111
112
|
}
|
package/types/View.d.ts
CHANGED
|
@@ -37,6 +37,14 @@ export interface ViewOptions<M = any> {
|
|
|
37
37
|
* }
|
|
38
38
|
*/
|
|
39
39
|
export default class View<M = any> extends Emitter {
|
|
40
|
+
/**
|
|
41
|
+
* Counter for generating unique IDs for view instances.
|
|
42
|
+
* For server-side rendering, reset it to `0` on every request (see `resetUid`) so the
|
|
43
|
+
* generated IDs match those on the client, enabling seamless hydration of components.
|
|
44
|
+
* @default 0
|
|
45
|
+
*/
|
|
46
|
+
static uid: number;
|
|
47
|
+
|
|
40
48
|
/**
|
|
41
49
|
* Escape HTML entities in a string.
|
|
42
50
|
* Use this method to sanitize user-generated content before inserting it into the DOM.
|
|
@@ -74,8 +82,8 @@ export default class View<M = any> extends Emitter {
|
|
|
74
82
|
/** Child views. Destroyed automatically when the parent is destroyed. */
|
|
75
83
|
children: View[];
|
|
76
84
|
|
|
77
|
-
/** Whether `destroy()` has been called on this view. */
|
|
78
|
-
destroyed
|
|
85
|
+
/** Whether `destroy()` has been called on this view. `undefined` until then. */
|
|
86
|
+
destroyed?: boolean;
|
|
79
87
|
|
|
80
88
|
/**
|
|
81
89
|
* Functions run on `destroy()`. Push cleanup callbacks here for external subscriptions
|
|
@@ -124,7 +132,7 @@ export default class View<M = any> extends Emitter {
|
|
|
124
132
|
* Add a view as a child. Children are stored in `this.children` and destroyed when the parent is destroyed.
|
|
125
133
|
* @return The child view for chaining.
|
|
126
134
|
*/
|
|
127
|
-
addChild(child:
|
|
135
|
+
addChild<C extends View>(child: C): C;
|
|
128
136
|
|
|
129
137
|
/** Call `destroy()` on children views. */
|
|
130
138
|
destroyChildren(): void;
|
package/types/index.d.ts
CHANGED