@rettangoli/fe 0.0.7-rc9 → 0.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/fe",
3
- "version": "0.0.7-rc9",
3
+ "version": "0.0.9",
4
4
  "description": "Frontend framework for building reactive web components",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -29,7 +29,7 @@
29
29
  "dependencies": {
30
30
  "esbuild": "^0.25.5",
31
31
  "immer": "^10.1.1",
32
- "jempl": "0.1.1",
32
+ "jempl": "0.3.2-rc2",
33
33
  "js-yaml": "^4.1.0",
34
34
  "rxjs": "^7.8.2",
35
35
  "snabbdom": "^3.6.2",
@@ -1,8 +1,8 @@
1
1
 
2
- export const handleOnMount = (deps) => {
2
+ export const handleOnMount = (deps, event) => {
3
3
  //
4
4
  }
5
5
 
6
- export const handlerSomeEvent = (e, deps) => {
6
+ export const handlerSomeEvent = (deps, event) => {
7
7
  //
8
8
  }
@@ -1,8 +1,8 @@
1
- export const INITIAL_STATE = Object.freeze({
2
- //
1
+ export const createInitialState = () => ({
2
+ //
3
3
  });
4
4
 
5
- export const toViewData = ({ state, props }) => {
5
+ export const selectViewData = ({ state, props }) => {
6
6
  return state;
7
7
  }
8
8
 
package/src/cli/build.js CHANGED
@@ -18,7 +18,7 @@ function capitalize(word) {
18
18
  // Function to process view files - loads YAML and creates temporary JS file
19
19
  export const writeViewFile = (view, category, component) => {
20
20
  // const { category, component } = extractCategoryAndComponent(filePath);
21
-
21
+
22
22
  const dir = `./.temp/${category}`;
23
23
  if (!existsSync(dir)) {
24
24
  mkdirSync(dir, { recursive: true });
@@ -30,12 +30,12 @@ export const writeViewFile = (view, category, component) => {
30
30
  };
31
31
 
32
32
  export const bundleFile = async (options) => {
33
- const { outfile = "./vt/static/main.js" } = options;
33
+ const { outfile = "./vt/static/main.js", development = false } = options;
34
34
  await esbuild.build({
35
35
  entryPoints: ["./.temp/dynamicImport.js"],
36
36
  bundle: true,
37
- minify: false,
38
- sourcemap: true,
37
+ minify: !development,
38
+ sourcemap: !!development,
39
39
  outfile: outfile,
40
40
  format: "esm",
41
41
  loader: {
@@ -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 = "./vt/static/main.js", setup = "setup.js" } = options;
50
+ const { dirs = ["./example"], outfile = "./vt/static/main.js", setup = "setup.js", development = false } = options;
51
51
 
52
52
  const allFiles = getAllFiles(dirs).filter((filePath) => {
53
53
  return (
@@ -93,7 +93,12 @@ const buildRettangoliFrontend = async (options) => {
93
93
  count++;
94
94
  } else if (["view"].includes(fileType)) {
95
95
  const view = loadYaml(readFileSync(filePath, "utf8"));
96
- view.template = parse(view.template);
96
+ try {
97
+ view.template = parse(view.template);
98
+ } catch (error) {
99
+ console.error(`Error parsing template in file: ${filePath}`);
100
+ throw error;
101
+ }
97
102
  writeViewFile(view, category, component);
98
103
  output += `import ${component}${capitalize(
99
104
  fileType,
@@ -125,7 +130,7 @@ Object.keys(imports).forEach(category => {
125
130
 
126
131
  writeFileSync("./.temp/dynamicImport.js", output);
127
132
 
128
- await bundleFile({ outfile });
133
+ await bundleFile({ outfile, development });
129
134
 
130
135
  console.log(`Build complete. Output file: ${outfile}`);
131
136
  };
package/src/cli/watch.js CHANGED
@@ -7,6 +7,10 @@ import { writeViewFile } from './build.js';
7
7
  import buildRettangoliFrontend from './build.js';
8
8
  import { extractCategoryAndComponent } from '../common.js';
9
9
 
10
+ // Debounce mechanism to prevent excessive rebuilds
11
+ let rebuildTimeout = null;
12
+ const DEBOUNCE_DELAY = 200; // 200ms delay
13
+
10
14
 
11
15
  const setupWatcher = (directory, options) => {
12
16
  watch(
@@ -21,7 +25,17 @@ const setupWatcher = (directory, options) => {
21
25
  const { category, component } = extractCategoryAndComponent(filename);
22
26
  await writeViewFile(view, category, component);
23
27
  }
24
- await buildRettangoliFrontend(options);
28
+
29
+ // Debounce the rebuild
30
+ if (rebuildTimeout) {
31
+ clearTimeout(rebuildTimeout);
32
+ }
33
+
34
+ rebuildTimeout = setTimeout(async () => {
35
+ console.log('Triggering rebuild...');
36
+ await buildRettangoliFrontend(options);
37
+ }, DEBOUNCE_DELAY);
38
+
25
39
  } catch (error) {
26
40
  console.error(`Error processing ${filename}:`, error);
27
41
  // Keep the watcher running
@@ -64,13 +78,19 @@ async function startViteServer(options) {
64
78
  const startWatching = async (options) => {
65
79
  const { dirs = ['src'], port = 3001 } = options;
66
80
 
81
+ // Set development mode for all builds in watch mode
82
+ const watchOptions = {
83
+ development: true,
84
+ ...options
85
+ };
86
+
67
87
  // Do initial build with all directories
68
88
  console.log('Starting initial build...');
69
- await buildRettangoliFrontend(options);
89
+ await buildRettangoliFrontend(watchOptions);
70
90
  console.log('Initial build complete');
71
91
 
72
92
  dirs.forEach(dir => {
73
- setupWatcher(dir, options);
93
+ setupWatcher(dir, watchOptions);
74
94
  });
75
95
 
76
96
  startViteServer({ port, outfile: options.outfile });
@@ -1,5 +1,6 @@
1
1
  import { produce } from "immer";
2
2
  import { parseView } from "./parser.js";
3
+ import { parseAndRender } from "jempl";
3
4
 
4
5
  /**
5
6
  * covert this format of json into raw css strings
@@ -261,8 +262,10 @@ class BaseComponent extends HTMLElement {
261
262
  }
262
263
 
263
264
  get viewData() {
264
- // TODO decide whether to pass globalStore state
265
- const data = this.store.toViewData();
265
+ let data = {};
266
+ if (this.store.selectViewData) {
267
+ data = this.store.selectViewData();
268
+ }
266
269
  return data;
267
270
  }
268
271
 
@@ -295,7 +298,6 @@ class BaseComponent extends HTMLElement {
295
298
  this.renderTarget = document.createElement("div");
296
299
  this.renderTarget.style.cssText = "display: contents;";
297
300
  this.shadow.appendChild(this.renderTarget);
298
- this.transformedHandlers = {};
299
301
  if (!this.renderTarget.parentNode) {
300
302
  this.appendChild(this.renderTarget);
301
303
  }
@@ -307,10 +309,25 @@ class BaseComponent extends HTMLElement {
307
309
  dispatchEvent: this.dispatchEvent.bind(this),
308
310
  };
309
311
 
312
+ this.transformedHandlers = {
313
+ handleCallStoreAction: (payload) => {
314
+ const { render, store } = deps;
315
+ const { _event, _action } = payload;
316
+ const context = parseAndRender(payload, {
317
+ event: _event
318
+ })
319
+ console.log('context', context)
320
+ if (!store[_action]) {
321
+ throw new Error(`store action store.${store._action} is not defined`)
322
+ }
323
+ store[_action](context);
324
+ render();
325
+ }
326
+ };
310
327
  // TODO don't include onmount, subscriptions, etc in transformedHandlers
311
328
  Object.keys(this.handlers || {}).forEach((key) => {
312
- this.transformedHandlers[key] = (payload) => {
313
- const result = this.handlers[key](payload, deps);
329
+ this.transformedHandlers[key] = (event, payload) => {
330
+ const result = this.handlers[key](deps, event, payload);
314
331
  return result;
315
332
  };
316
333
  });
@@ -348,7 +365,10 @@ class BaseComponent extends HTMLElement {
348
365
  if (oldValue !== newValue && this.render) {
349
366
  // Call handleOnUpdate if it exists
350
367
  if (this.handlers?.handleOnUpdate) {
351
- const changes = { [name]: { oldValue, newValue } };
368
+ const changes = {
369
+ oldAttrs: { [name]: oldValue },
370
+ newAttrs: { [name]: newValue }
371
+ };
352
372
  const deps = {
353
373
  ...this.deps,
354
374
  refIds: this.refIds,
@@ -421,17 +441,19 @@ class BaseComponent extends HTMLElement {
421
441
  /**
422
442
  * Binds store functions with actual framework data flow
423
443
  * Makes state changes immutable with immer
424
- * Passes props to selectors and toViewData
444
+ * Passes props to selectors
425
445
  * @param {*} store
426
446
  * @param {*} props
427
447
  * @returns
428
448
  */
429
449
  const bindStore = (store, props, attrs) => {
430
- const { INITIAL_STATE, toViewData, ...selectorsAndActions } = store;
450
+ const { createInitialState, ...selectorsAndActions } = store;
431
451
  const selectors = {};
432
452
  const actions = {};
433
- let currentState = structuredClone(INITIAL_STATE);
434
-
453
+ let currentState = {};
454
+ if (createInitialState) {
455
+ currentState = createInitialState();
456
+ }
435
457
  Object.entries(selectorsAndActions).forEach(([key, fn]) => {
436
458
  if (key.startsWith("select")) {
437
459
  selectors[key] = (...args) => {
@@ -448,7 +470,6 @@ const bindStore = (store, props, attrs) => {
448
470
  });
449
471
 
450
472
  return {
451
- toViewData: () => toViewData({ state: currentState, props, attrs }),
452
473
  getState: () => currentState,
453
474
  ...actions,
454
475
  ...selectors,
package/src/parser.js CHANGED
@@ -164,7 +164,7 @@ export const createVirtualDom = ({
164
164
  const attrRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|([^\s]+))/g;
165
165
  let match;
166
166
  const processedAttrs = new Set();
167
-
167
+
168
168
  while ((match = attrRegex.exec(attrsString)) !== null) {
169
169
  processedAttrs.add(match[1]);
170
170
  if (match[1].startsWith(".")) {
@@ -175,7 +175,7 @@ export const createVirtualDom = ({
175
175
  // Handle conditional boolean attributes
176
176
  const attrName = match[1].substring(1);
177
177
  const attrValue = match[2] || match[3] || match[4];
178
-
178
+
179
179
  // Convert string values to boolean
180
180
  let evalValue;
181
181
  if (attrValue === "true") {
@@ -186,7 +186,7 @@ export const createVirtualDom = ({
186
186
  // Try to get from viewData if it's not a literal boolean
187
187
  evalValue = lodashGet(viewData, attrValue);
188
188
  }
189
-
189
+
190
190
  // Only add attribute if value is truthy
191
191
  if (evalValue) {
192
192
  attrs[attrName] = "";
@@ -195,7 +195,7 @@ export const createVirtualDom = ({
195
195
  attrs[match[1]] = match[2] || match[3] || match[4];
196
196
  }
197
197
  }
198
-
198
+
199
199
  // Then, handle boolean attributes without values
200
200
  // Remove all processed attribute-value pairs from the string first
201
201
  let remainingAttrsString = attrsString;
@@ -209,7 +209,7 @@ export const createVirtualDom = ({
209
209
  processedMatches.forEach(match => {
210
210
  remainingAttrsString = remainingAttrsString.replace(match, ' ');
211
211
  });
212
-
212
+
213
213
  const booleanAttrRegex = /\b(\S+?)(?=\s|$)/g;
214
214
  let boolMatch;
215
215
  while ((boolMatch = booleanAttrRegex.exec(remainingAttrsString)) !== null) {
@@ -327,9 +327,27 @@ export const createVirtualDom = ({
327
327
  const eventListeners = refs[bestMatchRefKey].eventListeners;
328
328
  Object.entries(eventListeners).forEach(
329
329
  ([eventType, eventConfig]) => {
330
+ if (eventConfig.handler && eventConfig.action) {
331
+ throw new Error('Each listener can have hanlder or action but not both')
332
+ }
333
+
334
+ if (eventConfig.action) {
335
+ eventHandlers[eventType] = (event) => {
336
+ handlers.handleCallStoreAction({
337
+ ...eventConfig.payload,
338
+ _event: event,
339
+ _action: eventConfig.action,
340
+ })
341
+ }
342
+ return;
343
+ }
344
+
330
345
  if (eventConfig.handler && handlers[eventConfig.handler]) {
331
346
  eventHandlers[eventType] = (event) => {
332
- handlers[eventConfig.handler](event);
347
+ handlers[eventConfig.handler]({
348
+ ...eventConfig.payload,
349
+ _event: event,
350
+ });
333
351
  };
334
352
  } else if (eventConfig.handler) {
335
353
  // Keep this warning for missing handlers