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 +87 -12
- package/package.json +1 -1
- package/types/Component.d.ts +3 -3
- package/types/Emitter.d.ts +4 -3
- package/types/Model.d.ts +1 -1
- package/types/index.d.ts +3 -3
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.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<
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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.
|
|
329
|
+
this.props.initial;
|
|
301
330
|
};
|
|
302
331
|
|
|
303
332
|
// Typed render expression (`(component) => any`)
|
|
304
|
-
const renderLabel: RenderExpression<Counter> = ({ props }) => props.
|
|
333
|
+
const renderLabel: RenderExpression<Counter> = ({ props }) => props.initial;
|
|
305
334
|
|
|
306
335
|
// Extract types from existing classes
|
|
307
|
-
type
|
|
308
|
-
type P =
|
|
309
|
-
type S =
|
|
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
|
|
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
package/types/Component.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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;
|
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/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
|
|
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,
|
|
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
|
-
|
|
13
|
-
|
|
12
|
+
ComponentProps,
|
|
13
|
+
ComponentState,
|
|
14
14
|
ComponentModel,
|
|
15
15
|
ComponentLifecycle,
|
|
16
16
|
ExtendedComponent,
|