rasti 4.0.0 → 4.1.0-alpha.0
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 +92 -8
- package/dist/rasti.js +48 -44
- package/dist/rasti.js.map +1 -1
- package/dist/rasti.min.js +1 -1
- package/dist/rasti.min.js.map +1 -1
- package/es/Component.js +15 -18
- package/es/Component.js.map +1 -1
- package/es/Model.js +3 -1
- package/es/Model.js.map +1 -1
- package/es/View.js +33 -28
- package/es/View.js.map +1 -1
- package/lib/Component.cjs +15 -18
- package/lib/Component.cjs.map +1 -1
- package/lib/Model.cjs +3 -1
- package/lib/Model.cjs.map +1 -1
- package/lib/View.cjs +33 -28
- package/lib/View.cjs.map +1 -1
- package/package.json +36 -26
- package/src/Component.js +13 -16
- package/src/Model.js +3 -1
- package/src/View.js +33 -25
- package/types/Component.d.ts +238 -0
- package/types/Emitter.d.ts +111 -0
- package/types/Model.d.ts +123 -0
- package/types/View.d.ts +173 -0
- package/types/index.d.ts +15 -0
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.0.0/docs/logo-dark.svg">
|
|
4
|
-
<img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.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.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">
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
It provides declarative, composable **components** for building state-driven UIs.
|
|
14
14
|
Its low-level MVC core, inspired by **Backbone.js**’s architecture, provides **models**, **views** and **event emitters** as the fundamental building blocks.
|
|
15
15
|
|
|
16
|
-
[](https://github.com/8tentaculos/rasti/actions/workflows/ci.yml)
|
|
17
17
|
[](https://www.npmjs.com/package/rasti)
|
|
18
18
|
[](https://unpkg.com/rasti/dist/rasti.min.js)
|
|
19
19
|
[](https://www.npmjs.com/package/rasti)
|
|
20
20
|
[](https://www.jsdelivr.com/package/npm/rasti)
|
|
21
|
+
[](https://github.com/8tentaculos/rasti/blob/master/LICENSE)
|
|
21
22
|
|
|
22
23
|
## Key Features
|
|
23
24
|
|
|
@@ -35,6 +36,8 @@ Its low-level MVC core, inspired by **Backbone.js**’s architecture, provides *
|
|
|
35
36
|
Seamlessly integrates into existing **Backbone.js** legacy projects.
|
|
36
37
|
- **Standards-Based** 📐
|
|
37
38
|
Built on modern web standards, no tooling required.
|
|
39
|
+
- **TypeScript Support** 🧩
|
|
40
|
+
Ships with type definitions for strict typing of models, views, components, props, and events.
|
|
38
41
|
|
|
39
42
|
## Getting Started
|
|
40
43
|
|
|
@@ -58,8 +61,8 @@ import { Model, Component } from 'https://esm.run/rasti';
|
|
|
58
61
|
|
|
59
62
|
Include **Rasti** directly in your HTML using a CDN. Available UMD builds:
|
|
60
63
|
|
|
61
|
-
- [
|
|
62
|
-
- [
|
|
64
|
+
- [rasti.js](https://cdn.jsdelivr.net/npm/rasti/dist/rasti.js)
|
|
65
|
+
- [rasti.min.js](https://cdn.jsdelivr.net/npm/rasti/dist/rasti.min.js)
|
|
63
66
|
|
|
64
67
|
```html
|
|
65
68
|
<script src="https://cdn.jsdelivr.net/npm/rasti"></script>
|
|
@@ -123,7 +126,7 @@ const Navigation = Component.create`
|
|
|
123
126
|
// Create a Main component that includes the Navigation and displays the current route's label as the title.
|
|
124
127
|
const Main = Component.create`
|
|
125
128
|
<main>
|
|
126
|
-
<${Navigation} routes
|
|
129
|
+
<${Navigation} routes="${({ props }) => props.routes}" />
|
|
127
130
|
<section>
|
|
128
131
|
<h1>
|
|
129
132
|
${({ model, props }) => props.routes.find(
|
|
@@ -179,7 +182,7 @@ Counter.mount({ model }, document.body);
|
|
|
179
182
|
- **Lightweight and Efficient**
|
|
180
183
|
Minimal footprint with optimized performance, ensuring smooth updates.
|
|
181
184
|
- **Just the Right Abstraction**
|
|
182
|
-
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.
|
|
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.
|
|
183
186
|
|
|
184
187
|
## Example
|
|
185
188
|
|
|
@@ -189,6 +192,88 @@ You can find a sample **TODO application** in the [example folder](https://githu
|
|
|
189
192
|
|
|
190
193
|
For detailed information on how to use **Rasti**, refer to the [API documentation](/docs/api.md).
|
|
191
194
|
|
|
195
|
+
## TypeScript
|
|
196
|
+
|
|
197
|
+
**Rasti** ships with TypeScript declarations out of the box. The types are bundled in the package and resolved automatically.
|
|
198
|
+
|
|
199
|
+
### Components
|
|
200
|
+
|
|
201
|
+
Pass generics explicitly to type the resulting class:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const Header = Component.create<{ handleAddTodo: (title: string) => void }>`
|
|
205
|
+
<header>...</header>
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
new Header({ handleAddTodo: (t) => console.log(t) }); // ✅
|
|
209
|
+
|
|
210
|
+
// With a typed model:
|
|
211
|
+
const App = Component.create<{}, any, AppModel>`<main>...</main>`;
|
|
212
|
+
App.mount({ model: new AppModel() }, document.body);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Without generics, `Component.create` stays permissive (parity with JS):
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const Plain = Component.create`<div></div>`;
|
|
219
|
+
new Plain({ anything: 'goes' }); // ✅
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Models
|
|
223
|
+
|
|
224
|
+
Type the attributes with `Model<Attrs>`. Use **declaration merging** to surface the auto-generated getters/setters as instance properties:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { Model } from 'rasti';
|
|
228
|
+
|
|
229
|
+
interface TodoAttrs { title: string; completed: boolean; }
|
|
230
|
+
|
|
231
|
+
class Todo extends Model<TodoAttrs> {
|
|
232
|
+
preinitialize() {
|
|
233
|
+
this.defaults = { title: '', completed: false };
|
|
234
|
+
}
|
|
235
|
+
toggle() { this.completed = !this.completed; }
|
|
236
|
+
}
|
|
237
|
+
interface Todo extends TodoAttrs {} // Exposes this.title, this.completed
|
|
238
|
+
|
|
239
|
+
const t = new Todo({ title: 'x' });
|
|
240
|
+
t.title.toUpperCase(); // ✅
|
|
241
|
+
t.on('change:completed', (m, value) => value && /* boolean */ console.log('done'));
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Helper types
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import {
|
|
248
|
+
EventHandler,
|
|
249
|
+
RenderExpression,
|
|
250
|
+
Attrs,
|
|
251
|
+
Props,
|
|
252
|
+
State,
|
|
253
|
+
ComponentModel,
|
|
254
|
+
} from 'rasti';
|
|
255
|
+
|
|
256
|
+
// Typed event handler with `this` bound to the component
|
|
257
|
+
const onClick: EventHandler<Counter, MouseEvent> = function(ev) {
|
|
258
|
+
this.props.label;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Typed render expression (`(component) => any`)
|
|
262
|
+
const renderLabel: RenderExpression<Counter> = ({ props }) => props.label;
|
|
263
|
+
|
|
264
|
+
// Extract types from existing classes
|
|
265
|
+
type T = Attrs<Todo>; // TodoAttrs
|
|
266
|
+
type P = Props<Counter>; // CounterProps
|
|
267
|
+
type S = State<Counter>; // CounterState
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Known limitations
|
|
271
|
+
|
|
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) => ...`.
|
|
273
|
+
- **`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
|
+
- **`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
|
+
- **`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`.
|
|
276
|
+
|
|
192
277
|
## Working with LLMs
|
|
193
278
|
|
|
194
279
|
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.
|
|
@@ -209,4 +294,3 @@ We strive to minimize breaking changes between major versions. However, if you'r
|
|
|
209
294
|
## Contributing
|
|
210
295
|
|
|
211
296
|
Contributions are welcome! Share feature ideas or report bugs on our [GitHub Issues page](https://github.com/8tentaculos/rasti/issues).
|
|
212
|
-
|
package/dist/rasti.js
CHANGED
|
@@ -419,7 +419,6 @@
|
|
|
419
419
|
* @param {...*} [args] Additional arguments passed to `preinitialize` and `parse` methods
|
|
420
420
|
* @property {object|Function} defaults Default attributes for the model. If a function, it's called bound to the model instance to get defaults.
|
|
421
421
|
* @property {object} previous Object containing previous attributes when a change occurs.
|
|
422
|
-
* @property {string} attributePrefix Static property that defines a prefix for generated getters/setters. Defaults to empty string.
|
|
423
422
|
* @example
|
|
424
423
|
* import { Model } from 'rasti';
|
|
425
424
|
*
|
|
@@ -733,6 +732,9 @@
|
|
|
733
732
|
* Static property that defines a prefix for generated getters/setters.
|
|
734
733
|
* When set, all attribute properties will be prefixed (e.g., 'attr_name' instead of 'name').
|
|
735
734
|
* Useful for avoiding naming conflicts or creating a consistent property naming convention.
|
|
735
|
+
* @static
|
|
736
|
+
* @memberof module:Model
|
|
737
|
+
* @name attributePrefix
|
|
736
738
|
* @type {string}
|
|
737
739
|
* @default ''
|
|
738
740
|
* @example
|
|
@@ -778,7 +780,7 @@
|
|
|
778
780
|
* @property {object|Function} events Object in the format `{'event selector' : 'listener'}`. It will be used to bind delegated event listeners to the root element. If it is a function, it will be called to get the events object, bound to the view instance. See {@link module_view_delegateevents View.delegateEvents}.
|
|
779
781
|
* @property {object} model A model or any object containing data and business logic.
|
|
780
782
|
* @property {Function} template A function that returns a string with the view's inner HTML. See {@link module_view__render View.render}.
|
|
781
|
-
* @property {
|
|
783
|
+
* @property {string} uid Unique identifier for the view instance. This can be used to generate unique IDs for elements within the view. It is automatically generated and should not be set manually.
|
|
782
784
|
* @example
|
|
783
785
|
* import { View, Model } from 'rasti';
|
|
784
786
|
*
|
|
@@ -1031,12 +1033,17 @@
|
|
|
1031
1033
|
if (this.delegatedEventListeners.length) this.undelegateEvents();
|
|
1032
1034
|
|
|
1033
1035
|
// Store events by type i.e.: "click", "submit", etc.
|
|
1034
|
-
|
|
1036
|
+
const eventTypes = {};
|
|
1035
1037
|
|
|
1036
1038
|
Object.keys(events).forEach(key => {
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1039
|
+
const match = key.match(/^(\w+)(?:\s+(.+))*$/);
|
|
1040
|
+
|
|
1041
|
+
if (!match) {
|
|
1042
|
+
const message = `Invalid event format: ${key}`;
|
|
1043
|
+
throw new Error(createDevelopmentErrorMessage(message) );
|
|
1044
|
+
}
|
|
1045
|
+
// Extract type and selector from the event key.
|
|
1046
|
+
const [,type, selector] = match;
|
|
1040
1047
|
|
|
1041
1048
|
let listener = events[key];
|
|
1042
1049
|
// Listener may be a string representing a method name on the view, or a function.
|
|
@@ -1046,32 +1053,30 @@
|
|
|
1046
1053
|
|
|
1047
1054
|
if (!eventTypes[type]) eventTypes[type] = [];
|
|
1048
1055
|
|
|
1049
|
-
eventTypes[type].push(
|
|
1056
|
+
eventTypes[type].push([selector, listener]);
|
|
1050
1057
|
});
|
|
1051
1058
|
|
|
1052
1059
|
Object.keys(eventTypes).forEach(type => {
|
|
1053
1060
|
// Listener for the type of event.
|
|
1054
1061
|
const typeListener = (event) => {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
if (
|
|
1059
|
-
listener
|
|
1060
|
-
|
|
1062
|
+
let node = event.target;
|
|
1063
|
+
// Traverse ancestors until reaching the view root (`this.el`).
|
|
1064
|
+
while (node) {
|
|
1065
|
+
if (node.matches) {
|
|
1066
|
+
// Iterate and run every individual listener if the selector matches.
|
|
1067
|
+
eventTypes[type].forEach(([selector, listener]) => {
|
|
1068
|
+
if ((node === this.el && !selector) || (node !== this.el && node.matches(selector))) {
|
|
1069
|
+
listener.call(this, event, this, node);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1061
1072
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
while (node && node !== this.el) {
|
|
1066
|
-
if (node.matches && node.matches(selector)) {
|
|
1067
|
-
listener.call(this, event, this, node);
|
|
1068
|
-
}
|
|
1069
|
-
node = node.parentElement;
|
|
1070
|
-
}
|
|
1071
|
-
});
|
|
1073
|
+
// Continue traversing ancestors until reaching the view root (`this.el`) or stopping propagation.
|
|
1074
|
+
node = node === this.el || event.cancelBubble ? null : node.parentElement;
|
|
1075
|
+
}
|
|
1072
1076
|
};
|
|
1073
|
-
|
|
1074
|
-
this.delegatedEventListeners.push(
|
|
1077
|
+
// Store the type and listener in the delegated event listeners array.
|
|
1078
|
+
this.delegatedEventListeners.push([type, typeListener]);
|
|
1079
|
+
// Add the event listener to the element.
|
|
1075
1080
|
this.el.addEventListener(type, typeListener);
|
|
1076
1081
|
});
|
|
1077
1082
|
// Return `this` for chaining.
|
|
@@ -1085,7 +1090,7 @@
|
|
|
1085
1090
|
* @return {View} Return `this` for chaining.
|
|
1086
1091
|
*/
|
|
1087
1092
|
undelegateEvents() {
|
|
1088
|
-
this.delegatedEventListeners.forEach((
|
|
1093
|
+
this.delegatedEventListeners.forEach(([type, listener]) => {
|
|
1089
1094
|
this.el.removeEventListener(type, listener);
|
|
1090
1095
|
});
|
|
1091
1096
|
|
|
@@ -1160,6 +1165,8 @@
|
|
|
1160
1165
|
* For server-side rendering, this counter should be reset to `0` on every request to ensure that the generated
|
|
1161
1166
|
* unique IDs match those on the client, enabling seamless hydration of components.
|
|
1162
1167
|
* @static
|
|
1168
|
+
* @memberof module:View
|
|
1169
|
+
* @name uid
|
|
1163
1170
|
* @type {number}
|
|
1164
1171
|
* @default 0
|
|
1165
1172
|
*/
|
|
@@ -1944,15 +1951,12 @@
|
|
|
1944
1951
|
const isComponent = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT] && el.dataset[Component.DATASET_ELEMENT].endsWith('-1'));
|
|
1945
1952
|
|
|
1946
1953
|
/**
|
|
1947
|
-
* Check if an element contains a
|
|
1948
|
-
* It checks if the element is a component root element or if it contains a component.
|
|
1954
|
+
* Check if an element contains (or is) a dynamic element.
|
|
1949
1955
|
* @param {Element} el The element to check.
|
|
1950
|
-
* @return {boolean} True if the element contains a
|
|
1956
|
+
* @return {boolean} True if the element contains (or is) a dynamic element.
|
|
1951
1957
|
* @private
|
|
1952
1958
|
*/
|
|
1953
|
-
const containsElement = (el) => !!(
|
|
1954
|
-
el && ((el.dataset && el.dataset[Component.DATASET_ELEMENT]) || (el.querySelector && el.querySelector(`[${Component.ATTRIBUTE_ELEMENT}]`)))
|
|
1955
|
-
);
|
|
1959
|
+
const containsElement = (el) => !!(el && ((el.dataset && el.dataset[Component.DATASET_ELEMENT]) || (el.querySelector && el.querySelector(`[${Component.ATTRIBUTE_ELEMENT}]`))));
|
|
1956
1960
|
|
|
1957
1961
|
/**
|
|
1958
1962
|
* Generate string with placeholders for interpolated expressions.
|
|
@@ -2525,12 +2529,12 @@
|
|
|
2525
2529
|
element.hydrate(this.el);
|
|
2526
2530
|
}
|
|
2527
2531
|
});
|
|
2528
|
-
// Delegate events.
|
|
2529
|
-
this.delegateEvents();
|
|
2530
2532
|
// Get references for interpolation marker comments
|
|
2531
2533
|
this.template.interpolations.forEach(interpolation => interpolation.hydrate(this.el));
|
|
2532
2534
|
this.children.forEach(child => child.hydrate(this.el));
|
|
2533
2535
|
}
|
|
2536
|
+
// Delegate events.
|
|
2537
|
+
this.delegateEvents();
|
|
2534
2538
|
// Call `onHydrate` lifecycle method.
|
|
2535
2539
|
this.onHydrate.call(this);
|
|
2536
2540
|
// Return `this` for chaining.
|
|
@@ -2693,7 +2697,7 @@
|
|
|
2693
2697
|
|
|
2694
2698
|
/**
|
|
2695
2699
|
* Render the component as a string.
|
|
2696
|
-
* Used internally on the render process.
|
|
2700
|
+
* Used internally on the render process.
|
|
2697
2701
|
* Use it for server-side rendering or static site generation.
|
|
2698
2702
|
* @return {string} The rendered component.
|
|
2699
2703
|
* @example
|
|
@@ -2710,10 +2714,10 @@
|
|
|
2710
2714
|
* const app = new App();
|
|
2711
2715
|
*
|
|
2712
2716
|
* console.log(app.toString());
|
|
2713
|
-
* // <div data-
|
|
2717
|
+
* // <div data-rst-el="r1-1"><!--rst-s-r1-1--><button class="button" data-rst-el="r2-1">Click me</button><!--rst-e-r1-1--></div>
|
|
2714
2718
|
*
|
|
2715
2719
|
* console.log(`${app}`);
|
|
2716
|
-
* // <div data-
|
|
2720
|
+
* // <div data-rst-el="r1-1"><!--rst-s-r1-1--><button class="button" data-rst-el="r2-1">Click me</button><!--rst-e-r1-1--></div>
|
|
2717
2721
|
*/
|
|
2718
2722
|
toString() {
|
|
2719
2723
|
// Normally there won't be any children, but if there are, destroy them.
|
|
@@ -2803,7 +2807,7 @@
|
|
|
2803
2807
|
// In this case, where the component updates, it handles children recycling.
|
|
2804
2808
|
const addChild = (component) => {
|
|
2805
2809
|
let out = component;
|
|
2806
|
-
let found
|
|
2810
|
+
let found;
|
|
2807
2811
|
// Check if child already exists by key.
|
|
2808
2812
|
if (component.key) {
|
|
2809
2813
|
found = previousChildren.find(prev => prev.key === component.key);
|
|
@@ -2875,10 +2879,10 @@
|
|
|
2875
2879
|
} else {
|
|
2876
2880
|
// Update elements attributes.
|
|
2877
2881
|
this.template.elements.forEach(element => element.update());
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
+
}
|
|
2883
|
+
// If there are pending event types, delegate events again.
|
|
2884
|
+
if (this.eventsManager.hasPendingTypes()) {
|
|
2885
|
+
this.delegateEvents();
|
|
2882
2886
|
}
|
|
2883
2887
|
// Call onUpdate lifecycle method.
|
|
2884
2888
|
this.onUpdate.call(this);
|
|
@@ -3283,9 +3287,9 @@
|
|
|
3283
3287
|
* // Increment `model.seconds` every second.
|
|
3284
3288
|
* setInterval(() => model.seconds++, 1000);
|
|
3285
3289
|
*/
|
|
3286
|
-
var
|
|
3290
|
+
var Component_default = Component.create`<div></div>`;
|
|
3287
3291
|
|
|
3288
|
-
exports.Component =
|
|
3292
|
+
exports.Component = Component_default;
|
|
3289
3293
|
exports.Emitter = Emitter;
|
|
3290
3294
|
exports.Model = Model;
|
|
3291
3295
|
exports.View = View;
|