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 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.0/docs/logo-dark.svg">
4
- <img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.1.0-alpha.0/docs/logo.svg" height="120">
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
- You can find a 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). This example serves as a great starting point for your own projects. Try it live [here](https://rasti.js.org/example/todo/index.html).
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 inside `Component.create\`...\`` template literals (`${({ model }) => ...}`, `onClick=${function() { this.x }}`) cannot be inferred from the surrounding template. To type them, annotate explicitly: `function(this: MyComponent, ev) { ... }` or `({ model }: MyComponent) => ...`.
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
- ## Version History
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
- - **[v4.0.0](https://github.com/8tentaculos/rasti/releases/tag/v4.0.0)**
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rasti",
3
- "version": "4.1.0-alpha.0",
3
+ "version": "4.1.0-alpha.2",
4
4
  "description": "Modern MVC for building user interfaces",
5
5
  "type": "module",
6
6
  "main": "lib/index.cjs",
@@ -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 Partial {
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> = C extends Component<any, any, infer M> ? M : never;
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: object | ((proto: InstanceType<T>) => 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 `Partial` that preserves structure for position-based recycling.
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[]): Partial;
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: object, ...args: any[]): void;
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;
@@ -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<any>, type: string, listener: (...args: any[]) => void): () => void;
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<any>, type: string, listener: (...args: any[]) => void): () => void;
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(emitter?: Emitter<any>, type?: string, listener?: (...args: any[]) => void): void;
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: boolean;
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: View): View;
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
@@ -6,10 +6,12 @@ export {
6
6
  ComponentOptions,
7
7
  ComponentReservedOptions,
8
8
  SafeHTML,
9
- Partial,
9
+ ComponentPartial,
10
10
  EventHandler,
11
11
  RenderExpression,
12
12
  Props,
13
13
  State,
14
14
  ComponentModel,
15
+ ComponentLifecycle,
16
+ ExtendedComponent,
15
17
  } from './Component.js';