rasti 4.1.0-alpha.1 → 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.1/docs/logo-dark.svg">
4
- <img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.1.0-alpha.1/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
 
@@ -245,6 +245,17 @@ const Plain = Component.create`<div></div>`;
245
245
  new Plain({ anything: 'goes' }); // ✅
246
246
  ```
247
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
+
248
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:
249
260
 
250
261
  ```ts
@@ -261,6 +272,20 @@ const Counter = Component.create<{ initial: number }>`<div>...</div>`.extend({
261
272
  Counter.mount({ initial: 0 }, document.body).increment(); // ✅ increment is typed
262
273
  ```
263
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
+
264
289
  ### Models
265
290
 
266
291
  Type the attributes with `Model<Attrs>`. Use **declaration merging** to surface the auto-generated getters/setters as instance properties:
@@ -309,12 +334,48 @@ type P = Props<Counter>; // CounterProps
309
334
  type S = State<Counter>; // CounterState
310
335
  ```
311
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
+
312
370
  ### Known limitations
313
371
 
314
- - **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)).
315
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.
316
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).
317
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`.
318
379
 
319
380
  ## Working with LLMs
320
381
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rasti",
3
- "version": "4.1.0-alpha.1",
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",
@@ -263,7 +263,7 @@ declare class Component<P = {}, S = any, M = any> extends View<M> {
263
263
  onCreate(...args: any[]): void;
264
264
 
265
265
  /** Lifecycle. Called when model/state/props emit `change`. Default: triggers `render`. */
266
- onChange(model: object, changed: object, ...args: any[]): void;
266
+ onChange(model: object, changed: Record<string, any>, ...args: any[]): void;
267
267
 
268
268
  /** Lifecycle. Called after the first render hydrates the DOM. Client only. */
269
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
  }