@rettangoli/fe 0.0.7-rc5 → 0.0.7-rc6
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 +7 -331
- package/package.json +1 -1
- package/src/createComponent.js +17 -8
- package/src/parser.js +1 -1
package/README.md
CHANGED
|
@@ -14,13 +14,18 @@ A modern frontend framework that uses YAML for view definitions, web components
|
|
|
14
14
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
17
|
-
**Production usage** (when rtgl is installed globally):
|
|
18
17
|
```bash
|
|
19
18
|
rtgl fe build # Build components
|
|
20
19
|
rtgl fe watch # Start dev server
|
|
21
|
-
rtgl fe scaffold # Create new component
|
|
22
20
|
```
|
|
23
21
|
|
|
22
|
+
## Documentation
|
|
23
|
+
|
|
24
|
+
- **[Developer Quickstart](./docs/overview.md)** - Complete introduction and examples
|
|
25
|
+
- **[View System](./docs/view.md)** - Complete YAML syntax
|
|
26
|
+
- **[Store Management](./docs/store.md)** - State patterns
|
|
27
|
+
- **[Event Handlers](./docs/handlers.md)** - Event handling
|
|
28
|
+
|
|
24
29
|
## Architecture
|
|
25
30
|
|
|
26
31
|
### Technology Stack
|
|
@@ -84,335 +89,6 @@ src/
|
|
|
84
89
|
└── index.js # Main exports
|
|
85
90
|
```
|
|
86
91
|
|
|
87
|
-
# Usage
|
|
88
|
-
|
|
89
|
-
## Component Structure
|
|
90
|
-
|
|
91
|
-
Each component consists of three files:
|
|
92
|
-
|
|
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
|
-
```
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
## View Layer (.view.yaml)
|
|
102
|
-
|
|
103
|
-
Views are written in YAML and compiled to virtual DOM at build time.
|
|
104
|
-
|
|
105
|
-
### Basic HTML Structure
|
|
106
|
-
|
|
107
|
-
```yaml
|
|
108
|
-
template:
|
|
109
|
-
- div#myid.class1.class2 custom-attribute=abcd:
|
|
110
|
-
- rtgl-text: "Hello World"
|
|
111
|
-
- rtgl-button: "Click Me"
|
|
112
|
-
```
|
|
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
|
-
```
|
|
121
|
-
|
|
122
|
-
### Component Definition
|
|
123
|
-
|
|
124
|
-
```yaml
|
|
125
|
-
elementName: my-custom-component
|
|
126
|
-
|
|
127
|
-
template:
|
|
128
|
-
- rtgl-view:
|
|
129
|
-
- rtgl-text: "My Component"
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Attributes vs Props
|
|
133
|
-
|
|
134
|
-
When passing data to components, there's an important distinction:
|
|
135
|
-
|
|
136
|
-
```yaml
|
|
137
|
-
template:
|
|
138
|
-
- custom-component title=Hello .items=items
|
|
139
|
-
```
|
|
140
|
-
|
|
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.
|
|
145
|
-
|
|
146
|
-
### Variable Expressions
|
|
147
|
-
|
|
148
|
-
Views do not support complex variable expressions like `${myValue || 4}`. All values must be pre-computed in the `toViewData` store function:
|
|
149
|
-
|
|
150
|
-
❌ **Don't do this:**
|
|
151
|
-
```yaml
|
|
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
|
-
};
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
```yaml
|
|
170
|
-
template:
|
|
171
|
-
- rtgl-text: "${displayName}"
|
|
172
|
-
- rtgl-view class="${statusClass}"
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
### Styling
|
|
178
|
-
|
|
179
|
-
```yaml
|
|
180
|
-
styles:
|
|
181
|
-
'#title':
|
|
182
|
-
font-size: 24px
|
|
183
|
-
color: blue
|
|
184
|
-
'@media (min-width: 768px)':
|
|
185
|
-
'#title':
|
|
186
|
-
font-size: 32px
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### Event Handling
|
|
190
|
-
|
|
191
|
-
```yaml
|
|
192
|
-
refs:
|
|
193
|
-
submitButton:
|
|
194
|
-
eventListeners:
|
|
195
|
-
click:
|
|
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
|
|
231
|
-
|
|
232
|
-
```yaml
|
|
233
|
-
template:
|
|
234
|
-
- rtgl-view:
|
|
235
|
-
- $for project, index in projects:
|
|
236
|
-
- rtgl-view#project-${project.id}:
|
|
237
|
-
- custom-component .item=projects[${index}]:
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
**Conditionals:**
|
|
241
|
-
```yaml
|
|
242
|
-
template:
|
|
243
|
-
- rtgl-view:
|
|
244
|
-
$if isLoggedIn:
|
|
245
|
-
- user-dashboard: []
|
|
246
|
-
$else:
|
|
247
|
-
- login-form: []
|
|
248
|
-
|
|
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
|
-
```
|
|
259
|
-
|
|
260
|
-
For more advanced templating features, see the [Jempl documentation](https://github.com/yuusoft-org/jempl).
|
|
261
|
-
|
|
262
|
-
### Data Schemas
|
|
263
|
-
|
|
264
|
-
Define component interfaces with JSON Schema:
|
|
265
|
-
|
|
266
|
-
```yaml
|
|
267
|
-
viewDataSchema:
|
|
268
|
-
type: object
|
|
269
|
-
properties:
|
|
270
|
-
title:
|
|
271
|
-
type: string
|
|
272
|
-
default: "My Component"
|
|
273
|
-
items:
|
|
274
|
-
type: array
|
|
275
|
-
items:
|
|
276
|
-
type: object
|
|
277
|
-
|
|
278
|
-
propsSchema:
|
|
279
|
-
type: object
|
|
280
|
-
properties:
|
|
281
|
-
onSelect:
|
|
282
|
-
type: function
|
|
283
|
-
|
|
284
|
-
attrsSchema:
|
|
285
|
-
type: object
|
|
286
|
-
properties:
|
|
287
|
-
variant:
|
|
288
|
-
type: string
|
|
289
|
-
enum: [primary, secondary]
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
## State Management (.store.js)
|
|
293
|
-
|
|
294
|
-
### Initial State
|
|
295
|
-
|
|
296
|
-
```js
|
|
297
|
-
export const INITIAL_STATE = Object.freeze({
|
|
298
|
-
title: "My App",
|
|
299
|
-
items: [],
|
|
300
|
-
loading: false
|
|
301
|
-
});
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### View Data Transformation
|
|
305
|
-
|
|
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
|
-
```
|
|
315
|
-
|
|
316
|
-
### Selectors
|
|
317
|
-
|
|
318
|
-
```js
|
|
319
|
-
export const selectItems = (state) => state.items;
|
|
320
|
-
export const selectIsLoading = (state) => state.loading;
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
### Actions
|
|
324
|
-
|
|
325
|
-
```js
|
|
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);
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
## Event Handlers (.handlers.js)
|
|
343
|
-
|
|
344
|
-
### Special Handlers
|
|
345
|
-
|
|
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
|
-
};
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
### Event Handlers
|
|
367
|
-
|
|
368
|
-
```js
|
|
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 }
|
|
383
|
-
}));
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
export const handleItemClick = (event, deps) => {
|
|
387
|
-
const itemId = event.target.id.replace('item-', '');
|
|
388
|
-
console.log('Item clicked:', itemId);
|
|
389
|
-
};
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### Dependency Injection
|
|
393
|
-
|
|
394
|
-
```js
|
|
395
|
-
// In your setup.js file
|
|
396
|
-
const componentDependencies = {
|
|
397
|
-
apiClient: new ApiClient(),
|
|
398
|
-
router: new Router()
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
export const deps = {
|
|
402
|
-
components: componentDependencies,
|
|
403
|
-
pages: {}
|
|
404
|
-
};
|
|
405
|
-
```
|
|
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
|
-
```
|
|
415
|
-
|
|
416
92
|
## Configuration
|
|
417
93
|
|
|
418
94
|
Create a `rettangoli.config.yaml` file in your project root:
|
package/package.json
CHANGED
package/src/createComponent.js
CHANGED
|
@@ -4,7 +4,7 @@ import { parseView } from "./parser.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* covert this format of json into raw css strings
|
|
6
6
|
* notice if propoperty starts with \@, it will need to nest it
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
':host':
|
|
9
9
|
display: contents
|
|
10
10
|
'button':
|
|
@@ -26,8 +26,8 @@ import { parseView } from "./parser.js";
|
|
|
26
26
|
'@media (min-width: 768px)':
|
|
27
27
|
'button':
|
|
28
28
|
height: 40px
|
|
29
|
-
* @param {*} styleObject
|
|
30
|
-
* @returns
|
|
29
|
+
* @param {*} styleObject
|
|
30
|
+
* @returns
|
|
31
31
|
*/
|
|
32
32
|
const yamlToCss = (elementName, styleObject) => {
|
|
33
33
|
if (!styleObject || typeof styleObject !== "object") {
|
|
@@ -313,15 +313,24 @@ class BaseComponent extends HTMLElement {
|
|
|
313
313
|
};
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
-
if (this.handlers?.
|
|
317
|
-
this.
|
|
318
|
-
}
|
|
316
|
+
if (this.handlers?.handleBeforeMount) {
|
|
317
|
+
this._unmountCallback = this.handlers?.handleBeforeMount(deps);
|
|
319
318
|
|
|
320
|
-
|
|
321
|
-
this._unmountCallback
|
|
319
|
+
// Validate that handleBeforeMount doesn't return a Promise
|
|
320
|
+
if (this._unmountCallback && typeof this._unmountCallback.then === 'function') {
|
|
321
|
+
throw new Error('handleBeforeMount must be synchronous and cannot return a Promise.');
|
|
322
|
+
}
|
|
322
323
|
}
|
|
323
324
|
|
|
324
325
|
this.render();
|
|
326
|
+
|
|
327
|
+
if (this.handlers?.handleAfterMount) {
|
|
328
|
+
this.handlers?.handleAfterMount(deps);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (this.handlers?.subscriptions) {
|
|
332
|
+
this.unsubscribeAll = subscribeAll(this.handlers.subscriptions(deps));
|
|
333
|
+
}
|
|
325
334
|
}
|
|
326
335
|
|
|
327
336
|
disconnectedCallback() {
|
package/src/parser.js
CHANGED