@rettangoli/fe 0.0.5 โ 0.0.7-rc1
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 +312 -192
- package/package.json +1 -1
- package/src/cli/build.js +3 -3
- package/src/cli/watch.js +9 -4
package/README.md
CHANGED
|
@@ -1,164 +1,267 @@
|
|
|
1
|
-
|
|
2
1
|
# Rettangoli Frontend
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
A modern frontend framework that uses YAML for view definitions, web components for composition, and Immer for state management. Build reactive applications with minimal complexity using just 3 types of files.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
## Features
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
- **๐๏ธ Three-File Architecture** - `.view.yaml`, `.store.js`, `.handlers.js` files scale from single page to complex applications
|
|
8
|
+
- **๐ YAML Views** - Declarative UI definitions that compile to virtual DOM
|
|
9
|
+
- **๐งฉ Web Components** - Standards-based component architecture
|
|
10
|
+
- **๐ Reactive State** - Immer-powered immutable state management
|
|
11
|
+
- **โก Fast Development** - Hot reload with Vite integration
|
|
12
|
+
- **๐ฏ Template System** - Jempl templating for dynamic content
|
|
13
|
+
- **๐งช Testing Ready** - Pure functions and dependency injection for easy testing
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
## Quick Start
|
|
13
16
|
|
|
17
|
+
**Production usage** (when rtgl is installed globally):
|
|
14
18
|
```bash
|
|
15
|
-
|
|
19
|
+
rtgl fe build # Build components
|
|
20
|
+
rtgl fe watch # Start dev server
|
|
21
|
+
rtgl fe scaffold # Create new component
|
|
16
22
|
```
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
## Architecture
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
bun run ../rettangoli-cli/cli.js fe watch
|
|
22
|
-
```
|
|
26
|
+
### Technology Stack
|
|
23
27
|
|
|
28
|
+
**Runtime:**
|
|
29
|
+
- [Snabbdom](https://github.com/snabbdom/snabbdom) - Virtual DOM
|
|
30
|
+
- [Immer](https://github.com/immerjs/immer) - Immutable state management
|
|
31
|
+
- [Jempl](https://github.com/yuusoft-org/jempl) - Template engine
|
|
32
|
+
- [RxJS](https://github.com/ReactiveX/rxjs) - Reactive programming
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
**Build & Development:**
|
|
35
|
+
- [ESBuild](https://esbuild.github.io/) - Fast bundling
|
|
36
|
+
- [Vite](https://vite.dev/) - Development server with hot reload
|
|
26
37
|
|
|
38
|
+
**Browser Native:**
|
|
39
|
+
- Web Components - Component encapsulation
|
|
27
40
|
|
|
28
|
-
##
|
|
41
|
+
## Development
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
### Prerequisites
|
|
31
44
|
|
|
32
|
-
|
|
45
|
+
- Node.js 18+ or Bun
|
|
46
|
+
- A `rettangoli.config.yaml` file in your project root
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
* web components for components
|
|
48
|
+
### Setup
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* [rxjs](https://github.com/ReactiveX/rxjs) for reactive programming
|
|
50
|
+
1. **Install dependencies**:
|
|
51
|
+
```bash
|
|
52
|
+
bun install
|
|
53
|
+
```
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
2. **Create project structure**:
|
|
56
|
+
```bash
|
|
57
|
+
# Scaffold a new component
|
|
58
|
+
node ../rettangoli-cli/cli.js fe scaffold --category components --name MyButton
|
|
59
|
+
```
|
|
46
60
|
|
|
47
|
-
|
|
61
|
+
3. **Start development**:
|
|
62
|
+
```bash
|
|
63
|
+
# Build once
|
|
64
|
+
node ../rettangoli-cli/cli.js fe build
|
|
48
65
|
|
|
49
|
-
|
|
66
|
+
# Watch for changes (recommended)
|
|
67
|
+
node ../rettangoli-cli/cli.js fe watch
|
|
68
|
+
```
|
|
50
69
|
|
|
51
|
-
|
|
70
|
+
### Project Structure
|
|
52
71
|
|
|
53
|
-
|
|
72
|
+
```
|
|
73
|
+
src/
|
|
74
|
+
โโโ cli/
|
|
75
|
+
โ โโโ build.js # Build component bundles
|
|
76
|
+
โ โโโ watch.js # Development server with hot reload
|
|
77
|
+
โ โโโ scaffold.js # Component scaffolding
|
|
78
|
+
โ โโโ examples.js # Generate examples for testing
|
|
79
|
+
โ โโโ blank/ # Component templates
|
|
80
|
+
โโโ createComponent.js # Component factory
|
|
81
|
+
โโโ createWebPatch.js # Virtual DOM patching
|
|
82
|
+
โโโ parser.js # YAML to JSON converter
|
|
83
|
+
โโโ common.js # Shared utilities
|
|
84
|
+
โโโ index.js # Main exports
|
|
85
|
+
```
|
|
54
86
|
|
|
55
|
-
|
|
87
|
+
# Usage
|
|
56
88
|
|
|
57
|
-
|
|
89
|
+
## Component Structure
|
|
58
90
|
|
|
59
|
-
|
|
91
|
+
Each component consists of three files:
|
|
60
92
|
|
|
61
|
-
|
|
93
|
+
```
|
|
94
|
+
component-name/
|
|
95
|
+
โโโ component-name.handlers.js # Event handlers
|
|
96
|
+
โโโ component-name.store.js # State management
|
|
97
|
+
โโโ component-name.view.yaml # UI structure and styling
|
|
98
|
+
```
|
|
62
99
|
|
|
63
|
-
will become
|
|
64
100
|
|
|
65
|
-
|
|
101
|
+
## View Layer (.view.yaml)
|
|
66
102
|
|
|
103
|
+
Views are written in YAML and compiled to virtual DOM at build time.
|
|
67
104
|
|
|
68
|
-
###
|
|
105
|
+
### Basic HTML Structure
|
|
69
106
|
|
|
70
|
-
|
|
107
|
+
```yaml
|
|
108
|
+
template:
|
|
109
|
+
- div#myid.class1.class2 custom-attribute=abcd:
|
|
110
|
+
- rtgl-text: "Hello World"
|
|
111
|
+
- rtgl-button: "Click Me"
|
|
112
|
+
```
|
|
71
113
|
|
|
114
|
+
Compiles to:
|
|
115
|
+
```html
|
|
116
|
+
<div id="myid" class="class1 class2" custom-attribute="abcd">
|
|
117
|
+
<rtgl-text>Hello World</rtgl-text>
|
|
118
|
+
<rtgl-button>Click Me</rtgl-button>
|
|
119
|
+
</div>
|
|
120
|
+
```
|
|
72
121
|
|
|
73
|
-
|
|
122
|
+
### Component Definition
|
|
74
123
|
|
|
75
124
|
```yaml
|
|
125
|
+
elementName: my-custom-component
|
|
126
|
+
|
|
76
127
|
template:
|
|
77
|
-
- rtgl-view
|
|
78
|
-
-
|
|
79
|
-
each(v,k):
|
|
80
|
-
- rtgl-view#project-${v.id} h=64 w=f bw=xs p=m cur=p:
|
|
81
|
-
- rtgl-text s=lg: "${v.name}"
|
|
82
|
-
- rtgl-text s=sm: "${v.description}"
|
|
128
|
+
- rtgl-view:
|
|
129
|
+
- rtgl-text: "My Component"
|
|
83
130
|
```
|
|
84
131
|
|
|
85
|
-
|
|
132
|
+
### Attributes vs Props
|
|
133
|
+
|
|
134
|
+
When passing data to components, there's an important distinction:
|
|
86
135
|
|
|
87
136
|
```yaml
|
|
88
137
|
template:
|
|
89
|
-
-
|
|
90
|
-
- $switch:
|
|
91
|
-
'showSidebar':
|
|
92
|
-
- sidebar-component: []
|
|
93
|
-
- rtgl-view w=f h=f:
|
|
94
|
-
- $switch:
|
|
95
|
-
'currentRoute== "/projects"':
|
|
96
|
-
- projects-component: []
|
|
97
|
-
'currentRoute== "/profile"':
|
|
138
|
+
- custom-component title=Hello .items=items
|
|
98
139
|
```
|
|
99
140
|
|
|
100
|
-
|
|
141
|
+
- **Attributes** (`title=Hello`): Always string values, passed as HTML attributes
|
|
142
|
+
- **Props** (`.items=items`): JavaScript values from viewData, passed as component properties
|
|
143
|
+
|
|
144
|
+
Attributes become HTML attributes, while props are JavaScript objects/arrays/functions passed directly to the component.
|
|
101
145
|
|
|
102
|
-
|
|
146
|
+
### Variable Expressions
|
|
103
147
|
|
|
104
|
-
|
|
148
|
+
Views do not support complex variable expressions like `${myValue || 4}`. All values must be pre-computed in the `toViewData` store function:
|
|
105
149
|
|
|
150
|
+
โ **Don't do this:**
|
|
106
151
|
```yaml
|
|
107
|
-
|
|
152
|
+
template:
|
|
153
|
+
- rtgl-text: "${user.name || 'Guest'}"
|
|
154
|
+
- rtgl-view class="${isActive ? 'active' : 'inactive'}"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
โ
**Do this instead:**
|
|
158
|
+
```js
|
|
159
|
+
// In your .store.js file
|
|
160
|
+
export const toViewData = ({ state, props, attrs }) => {
|
|
161
|
+
return {
|
|
162
|
+
...state,
|
|
163
|
+
displayName: state.user.name || 'Guest',
|
|
164
|
+
statusClass: state.isActive ? 'active' : 'inactive'
|
|
165
|
+
};
|
|
166
|
+
};
|
|
108
167
|
```
|
|
109
168
|
|
|
110
|
-
|
|
169
|
+
```yaml
|
|
170
|
+
template:
|
|
171
|
+
- rtgl-text: "${displayName}"
|
|
172
|
+
- rtgl-view class="${statusClass}"
|
|
173
|
+
```
|
|
111
174
|
|
|
112
|
-
The component can later be used as `<custom-projects></custom-projects>`.
|
|
113
175
|
|
|
114
|
-
### Styles
|
|
115
176
|
|
|
116
|
-
|
|
177
|
+
### Styling
|
|
117
178
|
|
|
118
179
|
```yaml
|
|
119
180
|
styles:
|
|
120
181
|
'#title':
|
|
121
182
|
font-size: 24px
|
|
183
|
+
color: blue
|
|
122
184
|
'@media (min-width: 768px)':
|
|
123
185
|
'#title':
|
|
124
186
|
font-size: 32px
|
|
125
187
|
```
|
|
126
188
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
### Event listeners
|
|
189
|
+
### Event Handling
|
|
130
190
|
|
|
131
191
|
```yaml
|
|
132
192
|
refs:
|
|
133
|
-
|
|
134
|
-
eventListeners:
|
|
135
|
-
click:
|
|
136
|
-
handler: handleCreateButtonClick
|
|
137
|
-
project-*:
|
|
193
|
+
submitButton:
|
|
138
194
|
eventListeners:
|
|
139
195
|
click:
|
|
140
|
-
handler:
|
|
196
|
+
handler: handleSubmit
|
|
197
|
+
|
|
198
|
+
template:
|
|
199
|
+
- rtgl-button#submitButton: "Submit"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Templating with Jempl
|
|
203
|
+
|
|
204
|
+
**Loops:**
|
|
205
|
+
```yaml
|
|
206
|
+
template:
|
|
207
|
+
- rtgl-view:
|
|
208
|
+
projects:
|
|
209
|
+
$for project, index in projects:
|
|
210
|
+
- rtgl-view#project-${project.id}:
|
|
211
|
+
- rtgl-text: "${project.name}"
|
|
212
|
+
- rtgl-text: "${project.description}"
|
|
213
|
+
- rtgl-text: "Item ${index}"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
#### Props caveats
|
|
218
|
+
|
|
219
|
+
โ This will not work. Prop references can only be taken from viewDate, not from loop variables
|
|
220
|
+
|
|
221
|
+
```yaml
|
|
222
|
+
|
|
223
|
+
template:
|
|
224
|
+
- rtgl-view:
|
|
225
|
+
- $for project, index in projects:
|
|
226
|
+
- rtgl-view#project-${project.id}:
|
|
227
|
+
- custom-component .item=project:
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
โ
This is the workaround
|
|
141
231
|
|
|
232
|
+
```yaml
|
|
142
233
|
template:
|
|
143
|
-
- rtgl-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
- rtgl-view#project-${v.id} h=64 w=f bw=xs p=m cur=p:
|
|
148
|
-
- rtgl-text s=lg: "${v.name}"
|
|
149
|
-
- rtgl-text s=sm: "${v.description}"
|
|
234
|
+
- rtgl-view:
|
|
235
|
+
- $for project, index in projects:
|
|
236
|
+
- rtgl-view#project-${project.id}:
|
|
237
|
+
- custom-component .item=projects[${index}]:
|
|
150
238
|
```
|
|
151
239
|
|
|
152
|
-
|
|
240
|
+
**Conditionals:**
|
|
241
|
+
```yaml
|
|
242
|
+
template:
|
|
243
|
+
- rtgl-view:
|
|
244
|
+
$if isLoggedIn:
|
|
245
|
+
- user-dashboard: []
|
|
246
|
+
$else:
|
|
247
|
+
- login-form: []
|
|
153
248
|
|
|
154
|
-
|
|
249
|
+
# Multiple conditions with logical operators
|
|
250
|
+
template:
|
|
251
|
+
- rtgl-view:
|
|
252
|
+
$if user.age >= 18 && user.verified:
|
|
253
|
+
- admin-panel: []
|
|
254
|
+
$elif user.age >= 13:
|
|
255
|
+
- teen-dashboard: []
|
|
256
|
+
$else:
|
|
257
|
+
- kid-dashboard: []
|
|
258
|
+
```
|
|
155
259
|
|
|
156
|
-
|
|
260
|
+
For more advanced templating features, see the [Jempl documentation](https://github.com/yuusoft-org/jempl).
|
|
157
261
|
|
|
158
|
-
|
|
159
|
-
* `propsSchema` - The data that will be passed to the component via javascript, those can be objects.
|
|
160
|
-
* `attrsSchema` - The data that will be passed to the component via html attributes, this is raw strings.
|
|
262
|
+
### Data Schemas
|
|
161
263
|
|
|
264
|
+
Define component interfaces with JSON Schema:
|
|
162
265
|
|
|
163
266
|
```yaml
|
|
164
267
|
viewDataSchema:
|
|
@@ -166,159 +269,176 @@ viewDataSchema:
|
|
|
166
269
|
properties:
|
|
167
270
|
title:
|
|
168
271
|
type: string
|
|
169
|
-
default:
|
|
170
|
-
|
|
171
|
-
type: string
|
|
172
|
-
default: Create Project
|
|
173
|
-
projects:
|
|
272
|
+
default: "My Component"
|
|
273
|
+
items:
|
|
174
274
|
type: array
|
|
175
275
|
items:
|
|
176
276
|
type: object
|
|
177
|
-
|
|
178
|
-
id:
|
|
179
|
-
type: string
|
|
180
|
-
name:
|
|
181
|
-
type: string
|
|
182
|
-
default: Project 1
|
|
183
|
-
description:
|
|
184
|
-
type: string
|
|
185
|
-
default: Project 1 description
|
|
277
|
+
|
|
186
278
|
propsSchema:
|
|
187
279
|
type: object
|
|
188
|
-
properties:
|
|
189
|
-
|
|
280
|
+
properties:
|
|
281
|
+
onSelect:
|
|
282
|
+
type: function
|
|
190
283
|
|
|
284
|
+
attrsSchema:
|
|
285
|
+
type: object
|
|
286
|
+
properties:
|
|
287
|
+
variant:
|
|
288
|
+
type: string
|
|
289
|
+
enum: [primary, secondary]
|
|
290
|
+
```
|
|
191
291
|
|
|
192
|
-
## State
|
|
292
|
+
## State Management (.store.js)
|
|
193
293
|
|
|
194
|
-
|
|
195
|
-
* `toViewData` will take current `state`, `props` and `attrs` and return the `viewData` to be used by the view template
|
|
196
|
-
* Any exported function that starts with `select` will beceme selectors and are used by handlers to access state data
|
|
197
|
-
* `actions` are all other exported functions that are used to mutate the state.
|
|
294
|
+
### Initial State
|
|
198
295
|
|
|
199
296
|
```js
|
|
200
297
|
export const INITIAL_STATE = Object.freeze({
|
|
201
|
-
title: "
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
{
|
|
205
|
-
id: "1",
|
|
206
|
-
name: "Project 1",
|
|
207
|
-
description: "Project 1 description",
|
|
208
|
-
},
|
|
209
|
-
{
|
|
210
|
-
id: '2',
|
|
211
|
-
name: 'Project 2',
|
|
212
|
-
description: 'Project 2 description'
|
|
213
|
-
}
|
|
214
|
-
],
|
|
298
|
+
title: "My App",
|
|
299
|
+
items: [],
|
|
300
|
+
loading: false
|
|
215
301
|
});
|
|
216
|
-
|
|
217
|
-
export const toViewData = ({ state, props }, payload) => {
|
|
218
|
-
return state;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export const selectProjects = (state, props, payload) => {
|
|
222
|
-
return state.projects;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export const setProjects = (state, payload) => {
|
|
226
|
-
|
|
227
|
-
}
|
|
228
302
|
```
|
|
229
303
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
## Handlers
|
|
304
|
+
### View Data Transformation
|
|
233
305
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
306
|
+
```js
|
|
307
|
+
export const toViewData = ({ state, props, attrs }) => {
|
|
308
|
+
return {
|
|
309
|
+
...state,
|
|
310
|
+
itemCount: state.items.length,
|
|
311
|
+
hasItems: state.items.length > 0
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
```
|
|
237
315
|
|
|
238
|
-
|
|
316
|
+
### Selectors
|
|
239
317
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
* `deps.props` is the javascript properties that are passed to the component.
|
|
318
|
+
```js
|
|
319
|
+
export const selectItems = (state) => state.items;
|
|
320
|
+
export const selectIsLoading = (state) => state.loading;
|
|
321
|
+
```
|
|
245
322
|
|
|
323
|
+
### Actions
|
|
246
324
|
|
|
247
325
|
```js
|
|
248
|
-
export const
|
|
249
|
-
|
|
250
|
-
|
|
326
|
+
export const setLoading = (state, isLoading) => {
|
|
327
|
+
state.loading = isLoading; // Immer makes this immutable
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export const addItem = (state, item) => {
|
|
331
|
+
state.items.push(item);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export const removeItem = (state, itemId) => {
|
|
335
|
+
const index = state.items.findIndex(item => item.id === itemId);
|
|
336
|
+
if (index !== -1) {
|
|
337
|
+
state.items.splice(index, 1);
|
|
251
338
|
}
|
|
252
|
-
}
|
|
339
|
+
};
|
|
340
|
+
```
|
|
253
341
|
|
|
254
|
-
|
|
255
|
-
const { store, deps, render } = deps;
|
|
256
|
-
const formIsVisible = store.selectFormIsVisible();
|
|
342
|
+
## Event Handlers (.handlers.js)
|
|
257
343
|
|
|
258
|
-
|
|
259
|
-
store.setFormIsVisible(true);
|
|
260
|
-
}
|
|
261
|
-
deps.render();
|
|
262
|
-
}
|
|
344
|
+
### Special Handlers
|
|
263
345
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
346
|
+
```js
|
|
347
|
+
// Called when component mounts
|
|
348
|
+
export const handleOnMount = (deps) => {
|
|
349
|
+
const { store, render } = deps;
|
|
350
|
+
|
|
351
|
+
// Load initial data
|
|
352
|
+
store.setLoading(true);
|
|
353
|
+
loadData().then(data => {
|
|
354
|
+
store.setItems(data);
|
|
355
|
+
store.setLoading(false);
|
|
356
|
+
render();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Return cleanup function
|
|
360
|
+
return () => {
|
|
361
|
+
// Cleanup code here
|
|
362
|
+
};
|
|
363
|
+
};
|
|
268
364
|
```
|
|
269
365
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
* `deps.dispatchEvent` can be used to dispatch custom dom events.
|
|
366
|
+
### Event Handlers
|
|
273
367
|
|
|
274
368
|
```js
|
|
275
|
-
export const
|
|
276
|
-
|
|
277
|
-
|
|
369
|
+
export const handleSubmit = async (event, deps) => {
|
|
370
|
+
const { store, render, attrs, props } = deps;
|
|
371
|
+
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
|
|
374
|
+
const formData = new FormData(event.target);
|
|
375
|
+
const newItem = Object.fromEntries(formData);
|
|
376
|
+
|
|
377
|
+
store.addItem(newItem);
|
|
378
|
+
render();
|
|
379
|
+
|
|
380
|
+
// Dispatch custom event
|
|
381
|
+
deps.dispatchEvent(new CustomEvent('item-added', {
|
|
382
|
+
detail: { item: newItem }
|
|
278
383
|
}));
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
384
|
+
};
|
|
282
385
|
|
|
283
|
-
|
|
386
|
+
export const handleItemClick = (event, deps) => {
|
|
387
|
+
const itemId = event.target.id.replace('item-', '');
|
|
388
|
+
console.log('Item clicked:', itemId);
|
|
389
|
+
};
|
|
390
|
+
```
|
|
284
391
|
|
|
285
|
-
|
|
392
|
+
### Dependency Injection
|
|
286
393
|
|
|
287
394
|
```js
|
|
395
|
+
// In your setup.js file
|
|
288
396
|
const componentDependencies = {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
397
|
+
apiClient: new ApiClient(),
|
|
398
|
+
router: new Router()
|
|
399
|
+
};
|
|
293
400
|
|
|
294
401
|
export const deps = {
|
|
295
402
|
components: componentDependencies,
|
|
296
|
-
pages:
|
|
297
|
-
}
|
|
403
|
+
pages: {}
|
|
404
|
+
};
|
|
298
405
|
```
|
|
299
406
|
|
|
407
|
+
Access in handlers:
|
|
408
|
+
```js
|
|
409
|
+
export const handleLoadData = async (event, deps) => {
|
|
410
|
+
const { apiClient } = deps.components;
|
|
411
|
+
const data = await apiClient.fetchItems();
|
|
412
|
+
// ... handle data
|
|
413
|
+
};
|
|
414
|
+
```
|
|
300
415
|
|
|
301
|
-
##
|
|
302
|
-
|
|
303
|
-
This framework is written with testability in mind.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
## View
|
|
416
|
+
## Configuration
|
|
307
417
|
|
|
308
|
-
|
|
418
|
+
Create a `rettangoli.config.yaml` file in your project root:
|
|
309
419
|
|
|
420
|
+
```yaml
|
|
421
|
+
fe:
|
|
422
|
+
dirs:
|
|
423
|
+
- "./src/components"
|
|
424
|
+
- "./src/pages"
|
|
425
|
+
setup: "setup.js"
|
|
426
|
+
outfile: "./dist/bundle.js"
|
|
427
|
+
examples:
|
|
428
|
+
outputDir: "./vt/specs/examples"
|
|
429
|
+
```
|
|
310
430
|
|
|
311
|
-
##
|
|
431
|
+
## Testing
|
|
312
432
|
|
|
313
|
-
|
|
433
|
+
### View Components
|
|
314
434
|
|
|
315
|
-
|
|
435
|
+
Use visual testing with `rtgl vt`:
|
|
316
436
|
|
|
317
|
-
```
|
|
318
|
-
|
|
437
|
+
```bash
|
|
438
|
+
rtgl vt generate
|
|
439
|
+
rtgl vt report
|
|
319
440
|
```
|
|
320
441
|
|
|
321
|
-
##
|
|
442
|
+
## Examples
|
|
322
443
|
|
|
323
|
-
|
|
324
|
-
They are not always pure per se due to calling of dependencies.
|
|
444
|
+
For a complete working example, see the todos app in `examples/example1/`.
|
package/package.json
CHANGED
package/src/cli/build.js
CHANGED
|
@@ -30,7 +30,7 @@ export const writeViewFile = (view, category, component) => {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
export const bundleFile = async (options) => {
|
|
33
|
-
const { outfile = "./
|
|
33
|
+
const { outfile = "./vt/static/main.js" } = options;
|
|
34
34
|
await esbuild.build({
|
|
35
35
|
entryPoints: ["./.temp/dynamicImport.js"],
|
|
36
36
|
bundle: true,
|
|
@@ -47,7 +47,7 @@ export const bundleFile = async (options) => {
|
|
|
47
47
|
const buildRettangoliFrontend = async (options) => {
|
|
48
48
|
console.log("running build with options", options);
|
|
49
49
|
|
|
50
|
-
const { dirs = ["./example"], outfile = "./
|
|
50
|
+
const { dirs = ["./example"], outfile = "./vt/static/main.js", setup = "setup.js" } = options;
|
|
51
51
|
|
|
52
52
|
const allFiles = getAllFiles(dirs).filter((filePath) => {
|
|
53
53
|
return (
|
|
@@ -106,7 +106,7 @@ const buildRettangoliFrontend = async (options) => {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
output += `
|
|
109
|
-
import { createComponent } from 'rettangoli
|
|
109
|
+
import { createComponent } from '@rettangoli/fe';
|
|
110
110
|
import { deps, patch, h } from '../${setup}';
|
|
111
111
|
const imports = ${JSON.stringify(imports, null, 2)};
|
|
112
112
|
|
package/src/cli/watch.js
CHANGED
|
@@ -8,7 +8,7 @@ import buildRettangoliFrontend from './build.js';
|
|
|
8
8
|
import { extractCategoryAndComponent } from '../common.js';
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
const setupWatcher = (directory) => {
|
|
11
|
+
const setupWatcher = (directory, options) => {
|
|
12
12
|
watch(
|
|
13
13
|
directory,
|
|
14
14
|
{ recursive: true },
|
|
@@ -21,7 +21,7 @@ const setupWatcher = (directory) => {
|
|
|
21
21
|
const { category, component } = extractCategoryAndComponent(filename);
|
|
22
22
|
await writeViewFile(view, category, component);
|
|
23
23
|
}
|
|
24
|
-
await buildRettangoliFrontend(
|
|
24
|
+
await buildRettangoliFrontend(options);
|
|
25
25
|
} catch (error) {
|
|
26
26
|
console.error(`Error processing ${filename}:`, error);
|
|
27
27
|
// Keep the watcher running
|
|
@@ -53,11 +53,16 @@ async function startViteServer(options) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
const startWatching = (options) => {
|
|
56
|
+
const startWatching = async (options) => {
|
|
57
57
|
const { dirs = ['src'], port = 3001 } = options;
|
|
58
58
|
|
|
59
|
+
// Do initial build with all directories
|
|
60
|
+
console.log('Starting initial build...');
|
|
61
|
+
await buildRettangoliFrontend(options);
|
|
62
|
+
console.log('Initial build complete');
|
|
63
|
+
|
|
59
64
|
dirs.forEach(dir => {
|
|
60
|
-
setupWatcher(dir);
|
|
65
|
+
setupWatcher(dir, options);
|
|
61
66
|
});
|
|
62
67
|
|
|
63
68
|
startViteServer({ port });
|