rasti 4.1.0-alpha.1 → 4.1.0-alpha.3

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.3/docs/logo-dark.svg">
4
+ <img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.1.0-alpha.3/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,9 +272,23 @@ 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
- Type the attributes with `Model<Attrs>`. Use **declaration merging** to surface the auto-generated getters/setters as instance properties:
291
+ Type the attributes with `Model<YourAttrs>`. Use **declaration merging** to surface the auto-generated getters/setters as instance properties:
267
292
 
268
293
  ```ts
269
294
  import { Model } from 'rasti';
@@ -289,32 +314,82 @@ t.on('change:completed', (m, value) => value && /* boolean */ console.log('done'
289
314
  import {
290
315
  EventHandler,
291
316
  RenderExpression,
292
- Attrs,
293
- Props,
294
- State,
317
+ ModelAttrs,
318
+ ComponentProps,
319
+ ComponentState,
295
320
  ComponentModel,
296
321
  } from 'rasti';
297
322
 
323
+ // `Counter` (defined above with Component.create) is a *value*. To use the name in
324
+ // type position, alias it once — now `Counter` is both a value and a type:
325
+ type Counter = InstanceType<typeof Counter>;
326
+
298
327
  // Typed event handler with `this` bound to the component
299
328
  const onClick: EventHandler<Counter, MouseEvent> = function(ev) {
300
- this.props.label;
329
+ this.props.initial;
301
330
  };
302
331
 
303
332
  // Typed render expression (`(component) => any`)
304
- const renderLabel: RenderExpression<Counter> = ({ props }) => props.label;
333
+ const renderLabel: RenderExpression<Counter> = ({ props }) => props.initial;
305
334
 
306
335
  // Extract types from existing classes
307
- type T = Attrs<Todo>; // TodoAttrs
308
- type P = Props<Counter>; // CounterProps
309
- type S = State<Counter>; // CounterState
336
+ type A = ModelAttrs<Todo>; // Todo extends Model → already a type, no alias
337
+ type P = ComponentProps<Counter>; // pass the instance; `ComponentProps<typeof Counter>` is `never`
338
+ type S = ComponentState<Counter>;
339
+ ```
340
+
341
+ > Components made with `Component.create` are **values**, not types. To use one as a type — as with `Counter` above — add `type X = InstanceType<typeof X>` next to the definition, or write `InstanceType<typeof X>` inline. A `Model` subclass needs no alias, since `class` already declares both a value and a type.
342
+
343
+ ### Typing template interpolations
344
+
345
+ Functions inside a template are `any` — rasti can't infer them from the surrounding string. Which type to use depends on how rasti treats the function (quoted attribute or content → run on render; unquoted attribute → passed as-is):
346
+
347
+ > Under `strict` / `noImplicitAny`, every interpolation callback **must** be annotated — an untyped parameter is an error (TS7031/TS7006), not a silent `any`. In non-strict mode typing is opt-in: annotate where you want safety and leave trivial ones as `any`.
348
+
349
+ | Interpolation | What it is | Type to use |
350
+ |---|---|---|
351
+ | Content `${fn}` or quoted attr `attr="${fn}"` | Run on render; `this` and the argument are the component | `RenderExpression<C>` |
352
+ | Unquoted `onX=${fn}` | DOM handler, called `(event, component, matched)` | `EventHandler<C, E>` |
353
+ | Function passed to a child (`handler=${fn}`) | Becomes the child's prop; typed by the child, not this component | the child's prop signature |
354
+
355
+ Three ways to apply them:
356
+
357
+ ```ts
358
+ // `Home` is a value (made with Component.create), so alias it to use the name as a type:
359
+ const Home = Component.create<{}, { location: string }>`<div></div>`.extend({
360
+ close() { /* ... */ },
361
+ });
362
+ type Home = InstanceType<typeof Home>;
363
+
364
+ // 1. Named const — cleanest for non-trivial handlers
365
+ const onClick: EventHandler<Home, MouseEvent> = function(ev, self) {
366
+ ev.preventDefault();
367
+ self.close();
368
+ };
369
+
370
+ // 2. Inline with `satisfies` — checks + types the params without widening
371
+ ${(({ state }) => state?.location) satisfies RenderExpression<Home>}
372
+
373
+ // 3. Bare annotation — lightest, just types the argument
374
+ ${({ state }: Home) => state?.location}
375
+ ```
376
+
377
+ 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):
378
+
379
+ ```ts
380
+ // where the child was created with Component.create<ToggleAllProps>`...`
381
+ handleChange=${((checked) => model.toggleAll(checked)) satisfies ToggleAllProps['handleChange']}
310
382
  ```
311
383
 
312
384
  ### Known limitations
313
385
 
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) => ...`.
386
+ - **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
387
  - **`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
388
  - **`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
389
  - **`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`.
390
+ - **`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.
391
+ - **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.
392
+ - **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
393
 
319
394
  ## Working with LLMs
320
395
 
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.3",
4
4
  "description": "Modern MVC for building user interfaces",
5
5
  "type": "module",
6
6
  "main": "lib/index.cjs",
@@ -71,9 +71,9 @@ export type EventHandler<C, E extends Event = Event> = (
71
71
  export type RenderExpression<C> = (this: C, component: C) => any;
72
72
 
73
73
  /** Extracts the props type `P` from a Component subclass. */
74
- export type Props<C> = C extends Component<infer P, any, any> ? P : never;
74
+ export type ComponentProps<C> = C extends Component<infer P, any, any> ? P : never;
75
75
  /** Extracts the state type `S` from a Component subclass. */
76
- export type State<C> = C extends Component<any, infer S, any> ? S : never;
76
+ export type ComponentState<C> = C extends Component<any, infer S, any> ? S : never;
77
77
  /** Extracts the model type `M` from a Component / View subclass. */
78
78
  export type ComponentModel<C> =
79
79
  C extends Component<any, any, infer M> ? M :
@@ -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
  }
package/types/Model.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import Emitter from './Emitter.js';
2
2
 
3
3
  /** Extracts the attribute type `A` from a Model subclass. */
4
- export type Attrs<M> = M extends Model<infer A> ? A : never;
4
+ export type ModelAttrs<M> = M extends Model<infer A> ? A : never;
5
5
 
6
6
  export type ModelEvents<A> =
7
7
  & {
package/types/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { default as Emitter, EventMap } from './Emitter.js';
2
- export { default as Model, ModelEvents, Attrs } from './Model.js';
2
+ export { default as Model, ModelEvents, ModelAttrs } from './Model.js';
3
3
  export { default as View, ViewOptions } from './View.js';
4
4
  export {
5
5
  default as Component,
@@ -9,8 +9,8 @@ export {
9
9
  ComponentPartial,
10
10
  EventHandler,
11
11
  RenderExpression,
12
- Props,
13
- State,
12
+ ComponentProps,
13
+ ComponentState,
14
14
  ComponentModel,
15
15
  ComponentLifecycle,
16
16
  ExtendedComponent,