@rettangoli/fe 0.0.6 โ†’ 0.0.7-rc2

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.
Files changed (3) hide show
  1. package/README.md +312 -192
  2. package/package.json +1 -1
  3. package/src/cli/watch.js +17 -6
package/README.md CHANGED
@@ -1,164 +1,267 @@
1
-
2
1
  # Rettangoli Frontend
3
2
 
4
- ## Development
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
- Bundle the code under `example` folder using `@rettangoli/fe`
5
+ ## Features
7
6
 
8
- ```bash
9
- bun run ../rettangoli-cli/cli.js fe build
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
- Visit the example project that shows the components in action
15
+ ## Quick Start
13
16
 
17
+ **Production usage** (when rtgl is installed globally):
14
18
  ```bash
15
- bunx serve ./viz/static
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
- Instead of running `fe build` each time, we can also watch for file changes and bundle the code automatically.
24
+ ## Architecture
19
25
 
20
- ```bash
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
- Note: rettangoli-vt is not setup for this project yet. We just use static files under `viz/static` folder.
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
- ## Introduction
41
+ ## Development
29
42
 
30
- A frontend framework with just 3 type of files to scale from a single page to a full fledged complex application.
43
+ ### Prerequisites
31
44
 
32
- Implemented using:
45
+ - Node.js 18+ or Bun
46
+ - A `rettangoli.config.yaml` file in your project root
33
47
 
34
- Browser native:
35
- * web components for components
48
+ ### Setup
36
49
 
37
- Runtime:
38
- * [snabbdom](https://github.com/snabbdom/snabbdom) for virtual dom
39
- * [immer](https://github.com/immerjs/immer) for data immutability for state management
40
- * [json-e](https://github.com/json-e/json-e) for templating using json
41
- * [rxjs](https://github.com/ReactiveX/rxjs) for reactive programming
50
+ 1. **Install dependencies**:
51
+ ```bash
52
+ bun install
53
+ ```
42
54
 
43
- Build & Development:
44
- * [esbuild](https://esbuild.github.io/) for bundling
45
- * [vite](https://vite.dev/) for development
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
- ## View Layer
61
+ 3. **Start development**:
62
+ ```bash
63
+ # Build once
64
+ node ../rettangoli-cli/cli.js fe build
48
65
 
49
- The view layer is unique in that it uses yaml.
66
+ # Watch for changes (recommended)
67
+ node ../rettangoli-cli/cli.js fe watch
68
+ ```
50
69
 
51
- * The yaml will be converted into json at build time. The json will then be consumed by snabbdom to be transformed into html through a virtual dom.
70
+ ### Project Structure
52
71
 
53
- ### Yaml to write html
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
- Standard html can be totally written in yaml.
87
+ # Usage
56
88
 
57
- All child element are arrays. Except for things like actual content of text
89
+ ## Component Structure
58
90
 
59
- Use `#` and `.` selectors to represent `id` and `class`.
91
+ Each component consists of three files:
60
92
 
61
- `div#myid.class1.class2 custorm-attribute=abcd`
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
- `<div id="myid" class="class1 class2" custom-attribute="abcd"></div>`
101
+ ## View Layer (.view.yaml)
66
102
 
103
+ Views are written in YAML and compiled to virtual DOM at build time.
67
104
 
68
- ### Templating using json-e
105
+ ### Basic HTML Structure
69
106
 
70
- `json-e` templating language allows us to write conditions and loops in our yaml. For example:
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
- Loop
122
+ ### Component Definition
74
123
 
75
124
  ```yaml
125
+ elementName: my-custom-component
126
+
76
127
  template:
77
- - rtgl-view w=f g=m:
78
- - $map: { $eval: projects }
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
- Conditional. We usually use `$switch` more as it tends to be more flexible than `$if`.
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
- - rtgl-view d=h w=f h=f:
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
- `json-e` has many more features but we want to keep it simple and in most cases loops and conditionals are enough.
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
- The actual data used in the template is passed in as `viewData` from the state store which we will cover later.
146
+ ### Variable Expressions
103
147
 
104
- ### Define a elementName
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
- elementName: custom-projects
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
- This will be the web component name that will be used for the component.
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
- Styles can also be completely written in yaml.
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
- TODO better support nesting and issue with some global selectors.
128
-
129
- ### Event listeners
189
+ ### Event Handling
130
190
 
131
191
  ```yaml
132
192
  refs:
133
- createButton:
134
- eventListeners:
135
- click:
136
- handler: handleCreateButtonClick
137
- project-*:
193
+ submitButton:
138
194
  eventListeners:
139
195
  click:
140
- handler: handleProjectsClick
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-button#createButton: Create Project
144
- - rtgl-view w=f g=m:
145
- - $map: { $eval: projects }
146
- each(v,k):
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
- The above example, will attach event listenrs to `#createButton` and all `#project-*` (wild card support) elements. And bind them to the handlers `handleCreateButtonClick` and `handleProjectsClick`.
240
+ **Conditionals:**
241
+ ```yaml
242
+ template:
243
+ - rtgl-view:
244
+ $if isLoggedIn:
245
+ - user-dashboard: []
246
+ $else:
247
+ - login-form: []
153
248
 
154
- ### Defining data schema
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
- Component have a few types of data that can be defined using a JSON schema:
260
+ For more advanced templating features, see the [Jempl documentation](https://github.com/yuusoft-org/jempl).
157
261
 
158
- * `viewDataSchema` - The data that will used for the template.
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: Projects
170
- createButtonText:
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
- properties:
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 Store
292
+ ## State Management (.store.js)
193
293
 
194
- * Define `initial state`
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: "Projects",
202
- createButtonText: "Create Project",
203
- projects: [
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
- Note that this is just a dump store, it is not reactive. Components will need to call `deps.render()` from handlers to re-render the component.
231
-
232
- ## Handlers
304
+ ### View Data Transformation
233
305
 
234
- `handleOnMount` is a special handler that is called when the component is mounted. It returns a promise that resolves when the component is mounted.
235
-
236
- All other exported functions will automatically become handlers and can be used in the view layers's `eventListeners`
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
- A special object called `deps` is injected into all handlers. It has the following properties:
316
+ ### Selectors
239
317
 
240
- * `deps.render()` will re-render the component. We call this function each time we have chaged state and want to re-render the component.
241
- * `deps.store` is the store instance. Use selectors to select state and actions to mutate state.
242
- * `deps.transformedHandlers` can be used to call other handlers.
243
- * `deps.attrs` is the html attributes that are passed to the component.
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 handleOnMount = (deps) => {
249
- () => {
250
- // unsubscribe
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
- export const handleCreateButtonClick = async (e, deps) => {
255
- const { store, deps, render } = deps;
256
- const formIsVisible = store.selectFormIsVisible();
342
+ ## Event Handlers (.handlers.js)
257
343
 
258
- if (!formIsVisible) {
259
- store.setFormIsVisible(true);
260
- }
261
- deps.render();
262
- }
344
+ ### Special Handlers
263
345
 
264
- export const handleProjectsClick = (e, deps) => {
265
- const id = e.target.id
266
- console.log('handleProjectsClick', id);
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 handleProjectsClick = (e, deps) => {
276
- deps.dispatchEvent(new CustomEvent('project-clicked', {
277
- projectId: '1',
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
- ### Adding additional dependencies
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
- This is a simple yet powerful way to do dependency injection. Those are all global singleton dependencies. Technically anything can be injected and be made accessible to all components.
392
+ ### Dependency Injection
286
393
 
287
394
  ```js
395
+ // In your setup.js file
288
396
  const componentDependencies = {
289
- }
290
-
291
- const pageDependencies = {
292
- }
397
+ apiClient: new ApiClient(),
398
+ router: new Router()
399
+ };
293
400
 
294
401
  export const deps = {
295
402
  components: componentDependencies,
296
- pages: pageDependencies,
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
- ## Testing
302
-
303
- This framework is written with testability in mind.
304
-
305
-
306
- ## View
416
+ ## Configuration
307
417
 
308
- Visual testing `rettangoli-vt`
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
- ## State Store
431
+ ## Testing
312
432
 
313
- Those are all pure functions and it is straighforward to test them. Actions can be turned into pure functions using immer produce.
433
+ ### View Components
314
434
 
315
- Example
435
+ Use visual testing with `rtgl vt`:
316
436
 
317
- ```yaml
318
- ...
437
+ ```bash
438
+ rtgl vt generate
439
+ rtgl vt report
319
440
  ```
320
441
 
321
- ## Handlers
442
+ ## Examples
322
443
 
323
- Test them as normal functions.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/fe",
3
- "version": "0.0.6",
3
+ "version": "0.0.7-rc2",
4
4
  "description": "Frontend framework for building reactive web components",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
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({ dirs: [directory] });
24
+ await buildRettangoliFrontend(options);
25
25
  } catch (error) {
26
26
  console.error(`Error processing ${filename}:`, error);
27
27
  // Keep the watcher running
@@ -32,7 +32,13 @@ const setupWatcher = (directory) => {
32
32
  };
33
33
 
34
34
  async function startViteServer(options) {
35
- const { port = 3001, root = './viz/static' } = options;
35
+ const { port = 3001, outfile = "./vt/static/main.js" } = options;
36
+
37
+ // Extract the directory from outfile path
38
+ const outDir = path.dirname(outfile);
39
+ // Go up one level from the JS file directory to serve the site root
40
+ const root = path.dirname(outDir);
41
+ console.log('watch root dir:', root)
36
42
  try {
37
43
  const server = await createServer({
38
44
  // any valid user config options, plus `mode` and `configFile`
@@ -53,14 +59,19 @@ async function startViteServer(options) {
53
59
  }
54
60
 
55
61
 
56
- const startWatching = (options) => {
62
+ const startWatching = async (options) => {
57
63
  const { dirs = ['src'], port = 3001 } = options;
58
64
 
65
+ // Do initial build with all directories
66
+ console.log('Starting initial build...');
67
+ await buildRettangoliFrontend(options);
68
+ console.log('Initial build complete');
69
+
59
70
  dirs.forEach(dir => {
60
- setupWatcher(dir);
71
+ setupWatcher(dir, options);
61
72
  });
62
73
 
63
- startViteServer({ port });
74
+ startViteServer({ port, outfile: options.outfile });
64
75
  }
65
76
 
66
77
  export default startWatching;