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 +64 -3
- package/package.json +1 -1
- package/types/Component.d.ts +1 -1
- package/types/Emitter.d.ts +4 -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.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
|
|
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
package/types/Component.d.ts
CHANGED
|
@@ -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
|
}
|