@rettangoli/fe 0.0.6 โ†’ 0.0.7-rc10

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 CHANGED
@@ -1,324 +1,120 @@
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
 
14
17
  ```bash
15
- bunx serve ./viz/static
18
+ rtgl fe build # Build components
19
+ rtgl fe watch # Start dev server
16
20
  ```
17
21
 
18
- Instead of running `fe build` each time, we can also watch for file changes and bundle the code automatically.
19
-
20
- ```bash
21
- bun run ../rettangoli-cli/cli.js fe watch
22
- ```
23
-
24
-
25
- Note: rettangoli-vt is not setup for this project yet. We just use static files under `viz/static` folder.
26
-
27
-
28
- ## Introduction
22
+ ## Documentation
29
23
 
30
- A frontend framework with just 3 type of files to scale from a single page to a full fledged complex application.
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
31
28
 
32
- Implemented using:
29
+ ## Architecture
33
30
 
34
- Browser native:
35
- * web components for components
31
+ ### Technology Stack
36
32
 
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
33
+ **Runtime:**
34
+ - [Snabbdom](https://github.com/snabbdom/snabbdom) - Virtual DOM
35
+ - [Immer](https://github.com/immerjs/immer) - Immutable state management
36
+ - [Jempl](https://github.com/yuusoft-org/jempl) - Template engine
37
+ - [RxJS](https://github.com/ReactiveX/rxjs) - Reactive programming
42
38
 
43
- Build & Development:
44
- * [esbuild](https://esbuild.github.io/) for bundling
45
- * [vite](https://vite.dev/) for development
39
+ **Build & Development:**
40
+ - [ESBuild](https://esbuild.github.io/) - Fast bundling
41
+ - [Vite](https://vite.dev/) - Development server with hot reload
46
42
 
47
- ## View Layer
43
+ **Browser Native:**
44
+ - Web Components - Component encapsulation
48
45
 
49
- The view layer is unique in that it uses yaml.
50
-
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.
52
-
53
- ### Yaml to write html
54
-
55
- Standard html can be totally written in yaml.
56
-
57
- All child element are arrays. Except for things like actual content of text
58
-
59
- Use `#` and `.` selectors to represent `id` and `class`.
60
-
61
- `div#myid.class1.class2 custorm-attribute=abcd`
62
-
63
- will become
64
-
65
- `<div id="myid" class="class1 class2" custom-attribute="abcd"></div>`
66
-
67
-
68
- ### Templating using json-e
46
+ ## Development
69
47
 
70
- `json-e` templating language allows us to write conditions and loops in our yaml. For example:
48
+ ### Prerequisites
71
49
 
50
+ - Node.js 18+ or Bun
51
+ - A `rettangoli.config.yaml` file in your project root
72
52
 
73
- Loop
53
+ ### Setup
74
54
 
75
- ```yaml
76
- 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}"
55
+ 1. **Install dependencies**:
56
+ ```bash
57
+ bun install
83
58
  ```
84
59
 
85
- Conditional. We usually use `$switch` more as it tends to be more flexible than `$if`.
86
-
87
- ```yaml
88
- 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"':
60
+ 2. **Create project structure**:
61
+ ```bash
62
+ # Scaffold a new component
63
+ node ../rettangoli-cli/cli.js fe scaffold --category components --name MyButton
98
64
  ```
99
65
 
100
- `json-e` has many more features but we want to keep it simple and in most cases loops and conditionals are enough.
101
-
102
- The actual data used in the template is passed in as `viewData` from the state store which we will cover later.
103
-
104
- ### Define a elementName
66
+ 3. **Start development**:
67
+ ```bash
68
+ # Build once
69
+ node ../rettangoli-cli/cli.js fe build
105
70
 
106
- ```yaml
107
- elementName: custom-projects
71
+ # Watch for changes (recommended)
72
+ node ../rettangoli-cli/cli.js fe watch
108
73
  ```
109
74
 
110
- This will be the web component name that will be used for the component.
111
-
112
- The component can later be used as `<custom-projects></custom-projects>`.
113
-
114
- ### Styles
75
+ ### Project Structure
115
76
 
116
- Styles can also be completely written in yaml.
117
-
118
- ```yaml
119
- styles:
120
- '#title':
121
- font-size: 24px
122
- '@media (min-width: 768px)':
123
- '#title':
124
- font-size: 32px
125
77
  ```
126
-
127
- TODO better support nesting and issue with some global selectors.
128
-
129
- ### Event listeners
130
-
131
- ```yaml
132
- refs:
133
- createButton:
134
- eventListeners:
135
- click:
136
- handler: handleCreateButtonClick
137
- project-*:
138
- eventListeners:
139
- click:
140
- handler: handleProjectsClick
141
-
142
- 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}"
78
+ src/
79
+ โ”œโ”€โ”€ cli/
80
+ โ”‚ โ”œโ”€โ”€ build.js # Build component bundles
81
+ โ”‚ โ”œโ”€โ”€ watch.js # Development server with hot reload
82
+ โ”‚ โ”œโ”€โ”€ scaffold.js # Component scaffolding
83
+ โ”‚ โ”œโ”€โ”€ examples.js # Generate examples for testing
84
+ โ”‚ โ””โ”€โ”€ blank/ # Component templates
85
+ โ”œโ”€โ”€ createComponent.js # Component factory
86
+ โ”œโ”€โ”€ createWebPatch.js # Virtual DOM patching
87
+ โ”œโ”€โ”€ parser.js # YAML to JSON converter
88
+ โ”œโ”€โ”€ common.js # Shared utilities
89
+ โ””โ”€โ”€ index.js # Main exports
150
90
  ```
151
91
 
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`.
153
-
154
- ### Defining data schema
155
-
156
- Component have a few types of data that can be defined using a JSON schema:
157
-
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.
92
+ ## Configuration
161
93
 
94
+ Create a `rettangoli.config.yaml` file in your project root:
162
95
 
163
96
  ```yaml
164
- viewDataSchema:
165
- type: object
166
- properties:
167
- title:
168
- type: string
169
- default: Projects
170
- createButtonText:
171
- type: string
172
- default: Create Project
173
- projects:
174
- type: array
175
- items:
176
- 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
186
- propsSchema:
187
- type: object
188
- properties: {}
97
+ fe:
98
+ dirs:
99
+ - "./src/components"
100
+ - "./src/pages"
101
+ setup: "setup.js"
102
+ outfile: "./dist/bundle.js"
103
+ examples:
104
+ outputDir: "./vt/specs/examples"
189
105
  ```
190
106
 
191
-
192
- ## State Store
193
-
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.
198
-
199
- ```js
200
- 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
- ],
215
- });
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
- ```
229
-
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
233
-
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`
237
-
238
- A special object called `deps` is injected into all handlers. It has the following properties:
239
-
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.
245
-
246
-
247
- ```js
248
- export const handleOnMount = (deps) => {
249
- () => {
250
- // unsubscribe
251
- }
252
- }
253
-
254
- export const handleCreateButtonClick = async (e, deps) => {
255
- const { store, deps, render } = deps;
256
- const formIsVisible = store.selectFormIsVisible();
257
-
258
- if (!formIsVisible) {
259
- store.setFormIsVisible(true);
260
- }
261
- deps.render();
262
- }
263
-
264
- export const handleProjectsClick = (e, deps) => {
265
- const id = e.target.id
266
- console.log('handleProjectsClick', id);
267
- }
268
- ```
269
-
270
-
271
-
272
- * `deps.dispatchEvent` can be used to dispatch custom dom events.
273
-
274
- ```js
275
- export const handleProjectsClick = (e, deps) => {
276
- deps.dispatchEvent(new CustomEvent('project-clicked', {
277
- projectId: '1',
278
- }));
279
- }
280
- ```
281
-
282
-
283
- ### Adding additional dependencies
284
-
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.
286
-
287
- ```js
288
- const componentDependencies = {
289
- }
290
-
291
- const pageDependencies = {
292
- }
293
-
294
- export const deps = {
295
- components: componentDependencies,
296
- pages: pageDependencies,
297
- }
298
- ```
299
-
300
-
301
107
  ## Testing
302
108
 
303
- This framework is written with testability in mind.
304
-
305
-
306
- ## View
307
-
308
- Visual testing `rettangoli-vt`
109
+ ### View Components
309
110
 
111
+ Use visual testing with `rtgl vt`:
310
112
 
311
- ## State Store
312
-
313
- Those are all pure functions and it is straighforward to test them. Actions can be turned into pure functions using immer produce.
314
-
315
- Example
316
-
317
- ```yaml
318
- ...
113
+ ```bash
114
+ rtgl vt generate
115
+ rtgl vt report
319
116
  ```
320
117
 
321
- ## Handlers
118
+ ## Examples
322
119
 
323
- Test them as normal functions.
324
- They are not always pure per se due to calling of dependencies.
120
+ For a complete working example, see the todos app in `examples/example1/`.
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@rettangoli/fe",
3
- "version": "0.0.6",
3
+ "version": "0.0.7-rc10",
4
4
  "description": "Frontend framework for building reactive web components",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
- "keywords": ["frontend", "reactive", "components", "web", "framework"],
7
+ "keywords": [
8
+ "frontend",
9
+ "reactive",
10
+ "components",
11
+ "web",
12
+ "framework"
13
+ ],
8
14
  "files": [
9
15
  "src",
10
16
  "README.md",
@@ -23,7 +29,7 @@
23
29
  "dependencies": {
24
30
  "esbuild": "^0.25.5",
25
31
  "immer": "^10.1.1",
26
- "jempl": "^0.0.6",
32
+ "jempl": "0.1.2-rc2",
27
33
  "js-yaml": "^4.1.0",
28
34
  "rxjs": "^7.8.2",
29
35
  "snabbdom": "^3.6.2",
@@ -133,7 +133,7 @@ const examples = (options = {}) => {
133
133
  for (const [index, example] of examples.entries()) {
134
134
  const { name, viewData } = example;
135
135
  const ast = parse(template);
136
- const renderedView = flattenArrays(render({ ast, data: viewData }));
136
+ const renderedView = flattenArrays(render(ast, viewData, {}));
137
137
  const html = yamlToHtml(renderedView);
138
138
  output.push({
139
139
  category,
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`
@@ -40,6 +46,8 @@ async function startViteServer(options) {
40
46
  root,
41
47
  server: {
42
48
  port,
49
+ host: '0.0.0.0',
50
+ allowedHosts: true
43
51
  },
44
52
  });
45
53
  await server.listen();
@@ -53,14 +61,19 @@ async function startViteServer(options) {
53
61
  }
54
62
 
55
63
 
56
- const startWatching = (options) => {
64
+ const startWatching = async (options) => {
57
65
  const { dirs = ['src'], port = 3001 } = options;
58
66
 
67
+ // Do initial build with all directories
68
+ console.log('Starting initial build...');
69
+ await buildRettangoliFrontend(options);
70
+ console.log('Initial build complete');
71
+
59
72
  dirs.forEach(dir => {
60
- setupWatcher(dir);
73
+ setupWatcher(dir, options);
61
74
  });
62
75
 
63
- startViteServer({ port });
76
+ startViteServer({ port, outfile: options.outfile });
64
77
  }
65
78
 
66
79
  export default startWatching;
package/src/common.js CHANGED
@@ -8,7 +8,7 @@ import { Subject } from "rxjs";
8
8
  * const subject = new CustomSubject();
9
9
  *
10
10
  * const subscription = subject.subscribe(({ action, payload }) => {
11
- * console.log(action, payload);
11
+ * // handle action and payload
12
12
  * });
13
13
  *
14
14
  * subject.dispatch("action", { payload: "payload" });
@@ -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") {
@@ -104,7 +104,9 @@ function createAttrsProxy(source) {
104
104
  {
105
105
  get(_, prop) {
106
106
  if (typeof prop === "string") {
107
- return source.getAttribute(prop);
107
+ const value = source.getAttribute(prop);
108
+ // Return true for boolean attributes (empty string values)
109
+ return value === "" ? true : value;
108
110
  }
109
111
  return undefined;
110
112
  },
@@ -313,15 +315,24 @@ class BaseComponent extends HTMLElement {
313
315
  };
314
316
  });
315
317
 
316
- if (this.handlers?.subscriptions) {
317
- this.unsubscribeAll = subscribeAll(this.handlers.subscriptions(deps));
318
- }
318
+ if (this.handlers?.handleBeforeMount) {
319
+ this._unmountCallback = this.handlers?.handleBeforeMount(deps);
319
320
 
320
- if (this.handlers?.handleOnMount) {
321
- this._unmountCallback = this.handlers?.handleOnMount(deps);
321
+ // Validate that handleBeforeMount doesn't return a Promise
322
+ if (this._unmountCallback && typeof this._unmountCallback.then === 'function') {
323
+ throw new Error('handleBeforeMount must be synchronous and cannot return a Promise.');
324
+ }
322
325
  }
323
326
 
324
327
  this.render();
328
+
329
+ if (this.handlers?.handleAfterMount) {
330
+ this.handlers?.handleAfterMount(deps);
331
+ }
332
+
333
+ if (this.handlers?.subscriptions) {
334
+ this.unsubscribeAll = subscribeAll(this.handlers.subscriptions(deps));
335
+ }
325
336
  }
326
337
 
327
338
  disconnectedCallback() {
@@ -335,9 +346,23 @@ class BaseComponent extends HTMLElement {
335
346
 
336
347
  attributeChangedCallback(name, oldValue, newValue) {
337
348
  if (oldValue !== newValue && this.render) {
338
- requestAnimationFrame(() => {
339
- this.render();
340
- });
349
+ // Call handleOnUpdate if it exists
350
+ if (this.handlers?.handleOnUpdate) {
351
+ const changes = { [name]: { oldValue, newValue } };
352
+ const deps = {
353
+ ...this.deps,
354
+ refIds: this.refIds,
355
+ getRefIds: () => this.refIds,
356
+ dispatchEvent: this.dispatchEvent.bind(this),
357
+ store: this.store,
358
+ render: this.render.bind(this),
359
+ };
360
+ this.handlers.handleOnUpdate(changes, deps);
361
+ } else {
362
+ requestAnimationFrame(() => {
363
+ this.render();
364
+ });
365
+ }
341
366
  }
342
367
  }
343
368
 
@@ -353,7 +378,15 @@ class BaseComponent extends HTMLElement {
353
378
  }
354
379
 
355
380
  try {
356
- // const parseStart = performance.now();
381
+ const deps = {
382
+ ...this.deps,
383
+ refIds: this.refIds,
384
+ getRefIds: () => this.refIds,
385
+ dispatchEvent: this.dispatchEvent.bind(this),
386
+ store: this.store,
387
+ render: this.render.bind(this),
388
+ };
389
+
357
390
  const vDom = parseView({
358
391
  h: this.h,
359
392
  template: this.template,
@@ -361,9 +394,6 @@ class BaseComponent extends HTMLElement {
361
394
  refs: this.refs,
362
395
  handlers: this.transformedHandlers,
363
396
  });
364
-
365
- // const parseTime = performance.now() - parseStart;
366
- // console.log(`parseView took ${parseTime.toFixed(2)}ms`);
367
397
  // parse through vDom and recursively find all elements with id
368
398
  const ids = {};
369
399
  const findIds = (vDom) => {
@@ -377,15 +407,11 @@ class BaseComponent extends HTMLElement {
377
407
  findIds(vDom);
378
408
  this.refIds = ids;
379
409
 
380
- // const patchStart = performance.now();
381
410
  if (!this._oldVNode) {
382
411
  this._oldVNode = this.patch(this.renderTarget, vDom);
383
412
  } else {
384
413
  this._oldVNode = this.patch(this._oldVNode, vDom);
385
414
  }
386
-
387
- // const patchTime = performance.now() - patchStart;
388
- // console.log(`patch took ${patchTime.toFixed(2)}ms`);
389
415
  } catch (error) {
390
416
  console.error("Error during patching:", error);
391
417
  }
@@ -430,7 +456,7 @@ const bindStore = (store, props, attrs) => {
430
456
  };
431
457
 
432
458
  const createComponent = ({ handlers, view, store, patch, h }, deps) => {
433
- const { elementName, propsSchema, template, refs, styles } = view;
459
+ const { elementName, propsSchema, attrsSchema, template, refs, styles } = view;
434
460
 
435
461
  if (!patch) {
436
462
  throw new Error("Patch is not defined");
@@ -447,7 +473,9 @@ const createComponent = ({ handlers, view, store, patch, h }, deps) => {
447
473
  class MyComponent extends BaseComponent {
448
474
 
449
475
  static get observedAttributes() {
450
- return ["key"];
476
+ const baseAttrs = ["key"];
477
+ const attrKeys = attrsSchema?.properties ? Object.keys(attrsSchema.properties) : [];
478
+ return [...baseAttrs, ...attrKeys];
451
479
  }
452
480
 
453
481
  constructor() {
package/src/parser.js CHANGED
@@ -4,16 +4,16 @@ import { flattenArrays } from './common.js';
4
4
 
5
5
  const lodashGet = (obj, path) => {
6
6
  if (!path) return obj;
7
-
7
+
8
8
  // Parse path to handle both dot notation and bracket notation
9
9
  const parts = [];
10
10
  let current = '';
11
11
  let inBrackets = false;
12
12
  let quoteChar = null;
13
-
13
+
14
14
  for (let i = 0; i < path.length; i++) {
15
15
  const char = path[i];
16
-
16
+
17
17
  if (!inBrackets && char === '.') {
18
18
  if (current) {
19
19
  parts.push(current);
@@ -28,14 +28,14 @@ const lodashGet = (obj, path) => {
28
28
  } else if (inBrackets && char === ']') {
29
29
  if (current) {
30
30
  // Remove quotes if present and add the key
31
- if ((current.startsWith('"') && current.endsWith('"')) ||
32
- (current.startsWith("'") && current.endsWith("'"))) {
31
+ if ((current.startsWith('"') && current.endsWith('"')) ||
32
+ (current.startsWith("'") && current.endsWith("'"))) {
33
33
  parts.push(current.slice(1, -1));
34
- } else {
35
- // Numeric index or unquoted string
36
- const numValue = Number(current);
37
- parts.push(isNaN(numValue) ? current : numValue);
38
- }
34
+ } else {
35
+ // Numeric index or unquoted string
36
+ const numValue = Number(current);
37
+ parts.push(isNaN(numValue) ? current : numValue);
38
+ }
39
39
  current = '';
40
40
  }
41
41
  inBrackets = false;
@@ -51,24 +51,17 @@ const lodashGet = (obj, path) => {
51
51
  current += char;
52
52
  }
53
53
  }
54
-
54
+
55
55
  if (current) {
56
56
  parts.push(current);
57
57
  }
58
-
58
+
59
59
  return parts.reduce((acc, part) => acc && acc[part], obj);
60
60
  };
61
61
 
62
62
  export const parseView = ({ h, template, viewData, refs, handlers }) => {
63
- // const startTime = performance.now();
64
- const result = jemplRender({
65
- ast: template,
66
- data: viewData,
67
- });
68
- // const endTime = performance.now();
69
- // const executionTime = endTime - startTime;
70
- // console.log(`jemplRender execution time: ${executionTime.toFixed(2)}ms`);
71
-
63
+ const result = jemplRender(template, viewData, {});
64
+
72
65
  // Flatten the array carefully to maintain structure
73
66
  const flattenedResult = flattenArrays(result);
74
67
 
@@ -98,7 +91,7 @@ export const createVirtualDom = ({
98
91
  items,
99
92
  refs = {},
100
93
  handlers = {},
101
- viewData = {},
94
+ viewData = {}
102
95
  }) => {
103
96
  if (!Array.isArray(items)) {
104
97
  console.error("Input to createVirtualDom must be an array.");
@@ -120,7 +113,7 @@ export const createVirtualDom = ({
120
113
 
121
114
  const entries = Object.entries(item);
122
115
  if (entries.length === 0) {
123
- console.warn("Skipping empty object item:", item);
116
+ // skipping empty object item
124
117
  return null;
125
118
  }
126
119
 
@@ -167,17 +160,65 @@ export const createVirtualDom = ({
167
160
  const attrs = {}; // Ensure attrs is always an object
168
161
  const props = {};
169
162
  if (attrsString) {
170
- const attrRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|(\S+))/g;
163
+ // First, handle attributes with values
164
+ const attrRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|([^\s]+))/g;
171
165
  let match;
166
+ const processedAttrs = new Set();
167
+
172
168
  while ((match = attrRegex.exec(attrsString)) !== null) {
169
+ processedAttrs.add(match[1]);
173
170
  if (match[1].startsWith(".")) {
174
171
  const propName = match[1].substring(1);
175
172
  const valuePathName = match[4];
176
173
  props[propName] = lodashGet(viewData, valuePathName);
174
+ } else if (match[1].startsWith("?")) {
175
+ // Handle conditional boolean attributes
176
+ const attrName = match[1].substring(1);
177
+ const attrValue = match[2] || match[3] || match[4];
178
+
179
+ // Convert string values to boolean
180
+ let evalValue;
181
+ if (attrValue === "true") {
182
+ evalValue = true;
183
+ } else if (attrValue === "false") {
184
+ evalValue = false;
185
+ } else {
186
+ // Try to get from viewData if it's not a literal boolean
187
+ evalValue = lodashGet(viewData, attrValue);
188
+ }
189
+
190
+ // Only add attribute if value is truthy
191
+ if (evalValue) {
192
+ attrs[attrName] = "";
193
+ }
177
194
  } else {
178
195
  attrs[match[1]] = match[2] || match[3] || match[4];
179
196
  }
180
197
  }
198
+
199
+ // Then, handle boolean attributes without values
200
+ // Remove all processed attribute-value pairs from the string first
201
+ let remainingAttrsString = attrsString;
202
+ const processedMatches = [];
203
+ let tempMatch;
204
+ const tempAttrRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|([^\s]+))/g;
205
+ while ((tempMatch = tempAttrRegex.exec(attrsString)) !== null) {
206
+ processedMatches.push(tempMatch[0]);
207
+ }
208
+ // Remove all matched attribute=value pairs
209
+ processedMatches.forEach(match => {
210
+ remainingAttrsString = remainingAttrsString.replace(match, ' ');
211
+ });
212
+
213
+ const booleanAttrRegex = /\b(\S+?)(?=\s|$)/g;
214
+ let boolMatch;
215
+ while ((boolMatch = booleanAttrRegex.exec(remainingAttrsString)) !== null) {
216
+ const attrName = boolMatch[1];
217
+ // Skip if already processed or starts with . (prop) or contains =
218
+ if (!processedAttrs.has(attrName) && !attrName.startsWith(".") && !attrName.includes("=")) {
219
+ attrs[attrName] = "";
220
+ }
221
+ }
181
222
  }
182
223
 
183
224
  // 2. Handle ID from selector string (e.g., tag#id)
@@ -335,42 +376,60 @@ export const createVirtualDom = ({
335
376
  }
336
377
  if (Object.keys(props).length > 0) {
337
378
  snabbdomData.props = props;
379
+ }
338
380
 
339
- // For web components, add a hook to detect prop changes and set isDirty
340
- if (isWebComponent) {
341
- snabbdomData.hook = {
342
- update: (oldVnode, vnode) => {
343
- const oldProps = oldVnode.data?.props || {};
344
- const newProps = vnode.data?.props || {};
345
- const oldAttrs = oldVnode.data?.attrs || {};
346
- const newAttrs = vnode.data?.attrs || {};
347
-
348
- // Check if props have changed
349
- const propsChanged =
350
- JSON.stringify(oldProps) !== JSON.stringify(newProps);
351
-
352
- // Check if attrs have changed
353
- const attrsChanged =
354
- JSON.stringify(oldAttrs) !== JSON.stringify(newAttrs);
355
-
356
- if (propsChanged || attrsChanged) {
357
- // Set isDirty attribute and trigger re-render
358
- const element = vnode.elm;
359
- if (
360
- element &&
361
- element.render &&
362
- typeof element.render === "function"
363
- ) {
364
- element.setAttribute("isDirty", "true");
365
- requestAnimationFrame(() => {
366
- element.render();
367
- element.removeAttribute("isDirty");
368
- });
369
- }
381
+ // For web components, add a hook to detect prop and attr changes
382
+ if (isWebComponent) {
383
+ snabbdomData.hook = {
384
+ update: (oldVnode, vnode) => {
385
+ const oldProps = oldVnode.data?.props || {};
386
+ const newProps = vnode.data?.props || {};
387
+ const oldAttrs = oldVnode.data?.attrs || {};
388
+ const newAttrs = vnode.data?.attrs || {};
389
+
390
+ // Check if props have changed
391
+ const propsChanged =
392
+ JSON.stringify(oldProps) !== JSON.stringify(newProps);
393
+
394
+ // Check if attrs have changed
395
+ const attrsChanged =
396
+ JSON.stringify(oldAttrs) !== JSON.stringify(newAttrs);
397
+
398
+ if (propsChanged || attrsChanged) {
399
+ // Set isDirty attribute and trigger re-render
400
+ const element = vnode.elm;
401
+ if (
402
+ element &&
403
+ element.render &&
404
+ typeof element.render === "function"
405
+ ) {
406
+ element.setAttribute("isDirty", "true");
407
+ requestAnimationFrame(() => {
408
+ element.render();
409
+ element.removeAttribute("isDirty");
410
+ // Call the specific component's handleOnUpdate instead of the parent's onUpdate
411
+ if (element.handlers && element.handlers.handleOnUpdate) {
412
+ const deps = {
413
+ ...(element.deps || {}),
414
+ store: element.store,
415
+ render: element.render.bind(element),
416
+ handlers: element.handlers,
417
+ dispatchEvent: element.dispatchEvent.bind(element),
418
+ refIds: element.refIds || {},
419
+ getRefIds: () => element.refIds || {},
420
+ };
421
+ element.handlers.handleOnUpdate({
422
+ oldProps,
423
+ newProps,
424
+ oldAttrs,
425
+ newAttrs,
426
+ }, deps);
427
+ }
428
+ });
370
429
  }
371
- },
372
- };
373
- }
430
+ }
431
+ },
432
+ };
374
433
  }
375
434
 
376
435
  try {