@rettangoli/fe 0.0.14 → 1.0.0-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.
@@ -1,485 +1,32 @@
1
- import { produce } from "immer";
2
- import { parseView } from "./parser.js";
3
- import { parseAndRender } from "jempl";
4
-
5
- /**
6
- * covert this format of json into raw css strings
7
- * notice if propoperty starts with \@, it will need to nest it
8
- *
9
- ':host':
10
- display: contents
11
- 'button':
12
- background-color: var(--background)
13
- font-size: var(--sm-font-size)
14
- font-weight: var(--sm-font-weight)
15
- line-height: var(--sm-line-height)
16
- letter-spacing: var(--sm-letter-spacing)
17
- border: 1px solid var(--ring)
18
- border-radius: var(--border-radius-lg)
19
- padding-left: var(--spacing-md)
20
- padding-right: var(--spacing-md)
21
- height: 32px
22
- color: var(--foreground)
23
- outline: none
24
- cursor: pointer
25
- 'button:focus':
26
- border-color: var(--foreground)
27
- '@media (min-width: 768px)':
28
- 'button':
29
- height: 40px
30
- * @param {*} styleObject
31
- * @returns
32
- */
33
- const yamlToCss = (elementName, styleObject) => {
34
- if (!styleObject || typeof styleObject !== "object") {
35
- return "";
36
- }
37
-
38
- let css = ``;
39
- const convertPropertiesToCss = (properties) => {
40
- return Object.entries(properties)
41
- .map(([property, value]) => ` ${property}: ${value};`)
42
- .join("\n");
43
- };
44
-
45
- const processSelector = (selector, rules) => {
46
- if (typeof rules !== "object" || rules === null) {
47
- return "";
48
- }
49
-
50
- // Check if this is an @ rule (like @media, @keyframes, etc.)
51
- if (selector.startsWith("@")) {
52
- const nestedCss = Object.entries(rules)
53
- .map(([nestedSelector, nestedRules]) => {
54
- const nestedProperties = convertPropertiesToCss(nestedRules);
55
- return ` ${nestedSelector} {\n${nestedProperties
56
- .split("\n")
57
- .map((line) => (line ? ` ${line}` : ""))
58
- .join("\n")}\n }`;
59
- })
60
- .join("\n");
61
-
62
- return `${selector} {\n${nestedCss}\n}`;
63
- } else {
64
- // Regular selector
65
- const properties = convertPropertiesToCss(rules);
66
- return `${selector} {\n${properties}\n}`;
67
- }
68
- };
69
-
70
- // Process all top-level selectors
71
- Object.entries(styleObject).forEach(([selector, rules]) => {
72
- const selectorCss = processSelector(selector, rules);
73
- if (selectorCss) {
74
- css += (css ? "\n\n" : "") + selectorCss;
75
- }
76
- });
77
-
78
- return css;
79
- };
80
-
81
- /**
82
- * Subscribes to all observables and returns a function that will unsubscribe
83
- * from all observables when called
84
- * @param {*} observables
85
- * @returns
86
- */
87
- const subscribeAll = (observables) => {
88
- // Subscribe to all observables and store the subscription objects
89
- const subscriptions = observables.map((observable) => observable.subscribe());
90
-
91
- // Return a function that will unsubscribe from all observables when called
92
- return () => {
93
- for (const subscription of subscriptions) {
94
- if (subscription && typeof subscription.unsubscribe === "function") {
95
- subscription.unsubscribe();
96
- }
97
- }
98
- };
99
- };
100
-
101
-
102
- function createAttrsProxy(source) {
103
- return new Proxy(
104
- {},
105
- {
106
- get(_, prop) {
107
- if (typeof prop === "string") {
108
- const value = source.getAttribute(prop);
109
- // Return true for boolean attributes (empty string values)
110
- return value === "" ? true : value;
111
- }
112
- return undefined;
113
- },
114
- set() {
115
- throw new Error("Cannot assign to read-only proxy");
116
- },
117
- defineProperty() {
118
- throw new Error("Cannot define properties on read-only proxy");
119
- },
120
- deleteProperty() {
121
- throw new Error("Cannot delete properties from read-only proxy");
122
- },
123
- has(_, prop) {
124
- return typeof prop === "string" && source.hasAttribute(prop);
125
- },
126
- ownKeys() {
127
- return source.getAttributeNames();
128
- },
129
- getOwnPropertyDescriptor(_, prop) {
130
- if (typeof prop === "string" && source.hasAttribute(prop)) {
131
- return {
132
- configurable: true,
133
- enumerable: true,
134
- get: () => source.getAttribute(prop),
135
- };
136
- }
137
- return undefined;
138
- },
139
- },
140
- );
141
- }
142
-
143
- /**
144
- * Creates a read-only proxy object that only allows access to specified properties from the source object
145
- * Props are directly attached to the web-component element for example this.title
146
- * We don't want to expose the whole web compoenent but only want to expose the props
147
- * createPropsProxy(this, ['title']) will expose only the title
148
- * @param {Object} source - The source object to create a proxy from
149
- * @param {string[]} allowedKeys - Array of property names that are allowed to be accessed
150
- * @returns {Proxy} A read-only proxy object that only allows access to the specified properties
151
- * @throws {Error} When attempting to modify the proxy object
152
- */
153
- function createPropsProxy(source, allowedKeys) {
154
- // return source;
155
- const allowed = new Set(allowedKeys);
156
- return new Proxy(
157
- {},
158
- {
159
- get(_, prop) {
160
- if (typeof prop === "string" && allowed.has(prop)) {
161
- return source[prop];
162
- }
163
- return undefined;
164
- },
165
- set() {
166
- throw new Error("Cannot assign to read-only proxy");
167
- },
168
- defineProperty() {
169
- throw new Error("Cannot define properties on read-only proxy");
170
- },
171
- deleteProperty() {
172
- throw new Error("Cannot delete properties from read-only proxy");
173
- },
174
- has(_, prop) {
175
- return typeof prop === "string" && allowed.has(prop);
176
- },
177
- ownKeys() {
178
- return [...allowed];
179
- },
180
- getOwnPropertyDescriptor(_, prop) {
181
- if (typeof prop === "string" && allowed.has(prop)) {
182
- return {
183
- configurable: true,
184
- enumerable: true,
185
- get: () => source[prop],
186
- };
187
- }
188
- return undefined;
189
- },
190
- },
191
- );
192
- }
193
-
194
- /**
195
- * Base class for web components
196
- * Connects web compnent with the rettangoli framework
197
- */
198
- class BaseComponent extends HTMLElement {
199
-
200
- /**
201
- * @type {string}
202
- */
203
- elementName;
204
-
205
- /**
206
- * @type {Object}
207
- */
208
- styles;
209
-
210
- /**
211
- * @type {Function}
212
- */
213
- h;
214
-
215
- /**
216
- * @type {Object}
217
- */
218
- store;
219
-
220
- /**
221
- * @type {Object}
222
- */
223
- props;
224
-
225
- /**
226
- * @type {Object}
227
- */
228
- propsSchema;
229
-
230
- /**
231
- * @type {Object}
232
- */
233
- template;
234
-
235
- /**
236
- * @type {Object}
237
- */
238
- handlers;
239
-
240
- /**
241
- * @type {Object}
242
- */
243
- transformedHandlers = {};
244
-
245
- /**
246
- * @type {Object}
247
- */
248
- refs;
249
- refIds = {};
250
- patch;
251
- _unmountCallback;
252
- _oldVNode;
253
- deps;
254
-
255
- /**
256
- * @type {string}
257
- */
258
- cssText;
259
-
260
- static get observedAttributes() {
261
- return ["key"];
262
- }
263
-
264
- get viewData() {
265
- let data = {};
266
- if (this.store.selectViewData) {
267
- data = this.store.selectViewData();
268
- }
269
- return data;
270
- }
271
-
272
- connectedCallback() {
273
- this.shadow = this.attachShadow({ mode: "open" });
274
-
275
- const commonStyleSheet = new CSSStyleSheet();
276
- commonStyleSheet.replaceSync(`
277
- a, a:link, a:visited, a:hover, a:active {
278
- display: contents;
279
- color: inherit;
280
- text-decoration: none;
281
- background: none;
282
- border: none;
283
- padding: 0;
284
- margin: 0;
285
- font: inherit;
286
- cursor: pointer;
287
- }
288
- `);
289
-
290
- const adoptedStyleSheets = [commonStyleSheet];
291
-
292
- if (this.cssText) {
293
- const styleSheet = new CSSStyleSheet();
294
- styleSheet.replaceSync(this.cssText);
295
- adoptedStyleSheets.push(styleSheet);
296
- }
297
- this.shadow.adoptedStyleSheets = adoptedStyleSheets;
298
- this.renderTarget = document.createElement("div");
299
- this.renderTarget.style.cssText = "display: contents;";
300
- this.shadow.appendChild(this.renderTarget);
301
- if (!this.renderTarget.parentNode) {
302
- this.appendChild(this.renderTarget);
303
- }
304
- this.style.display = "contents";
305
- const deps = {
306
- ...this.deps,
307
- refIds: this.refIds,
308
- getRefIds: () => this.refIds,
309
- dispatchEvent: this.dispatchEvent.bind(this),
310
- };
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
- };
327
- // TODO don't include onmount, subscriptions, etc in transformedHandlers
328
- Object.keys(this.handlers || {}).forEach((key) => {
329
- this.transformedHandlers[key] = (event, payload) => {
330
- const result = this.handlers[key](deps, event, payload);
331
- return result;
332
- };
333
- });
334
-
335
- if (this.handlers?.handleBeforeMount) {
336
- this._unmountCallback = this.handlers?.handleBeforeMount(deps);
337
-
338
- // Validate that handleBeforeMount doesn't return a Promise
339
- if (this._unmountCallback && typeof this._unmountCallback.then === 'function') {
340
- throw new Error('handleBeforeMount must be synchronous and cannot return a Promise.');
341
- }
342
- }
343
-
344
- this.render();
345
-
346
- if (this.handlers?.handleAfterMount) {
347
- this.handlers?.handleAfterMount(deps);
348
- }
349
-
350
- if (this.handlers?.subscriptions) {
351
- this.unsubscribeAll = subscribeAll(this.handlers.subscriptions(deps));
352
- }
353
- }
354
-
355
- disconnectedCallback() {
356
- if (this._unmountCallback) {
357
- this._unmountCallback();
358
- }
359
- if (this.unsubscribeAll) {
360
- this.unsubscribeAll();
361
- }
1
+ import { toCamelCase } from "./core/runtime/props.js";
2
+ import { validateSchemaContract } from "./core/schema/validateSchemaContract.js";
3
+ import { createWebComponentClass } from "./web/createWebComponentClass.js";
4
+
5
+ const createComponent = (
6
+ { handlers, methods, constants, schema, view, store, patch, h },
7
+ deps,
8
+ ) => {
9
+ if (!view) {
10
+ throw new Error("view is not defined");
362
11
  }
363
12
 
364
- attributeChangedCallback(name, oldValue, newValue) {
365
- if (oldValue !== newValue && this.render) {
366
- // Call handleOnUpdate if it exists
367
- if (this.handlers?.handleOnUpdate) {
368
- const deps = {
369
- ...this.deps,
370
- refIds: this.refIds,
371
- getRefIds: () => this.refIds,
372
- dispatchEvent: this.dispatchEvent.bind(this),
373
- store: this.store,
374
- render: this.render.bind(this),
375
- };
376
- const changes = {
377
- oldAttrs: { [name]: oldValue },
378
- newAttrs: { [name]: newValue },
379
- oldProps: deps.props,
380
- newProps: deps.props,
381
- };
382
- this.handlers.handleOnUpdate(deps, changes);
383
- } else {
384
- requestAnimationFrame(() => {
385
- this.render();
386
- });
387
- }
388
- }
13
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
14
+ throw new Error("schema is required. Define component metadata in .schema.yaml.");
389
15
  }
390
16
 
391
- render = () => {
392
- if (!this.patch) {
393
- console.error("Patch function is not defined!");
394
- return;
395
- }
17
+ const resolvedSchema = schema;
18
+ const { template, refs, styles } = view;
396
19
 
397
- if (!this.template) {
398
- console.error("Template is not defined!");
399
- return;
400
- }
401
-
402
- try {
403
- const deps = {
404
- ...this.deps,
405
- refIds: this.refIds,
406
- getRefIds: () => this.refIds,
407
- dispatchEvent: this.dispatchEvent.bind(this),
408
- store: this.store,
409
- render: this.render.bind(this),
410
- };
411
-
412
- const vDom = parseView({
413
- h: this.h,
414
- template: this.template,
415
- viewData: this.viewData,
416
- refs: this.refs,
417
- handlers: this.transformedHandlers,
418
- });
419
- // parse through vDom and recursively find all elements with id
420
- const ids = {};
421
- const findIds = (vDom) => {
422
- if (vDom.data?.attrs && vDom.data.attrs.id) {
423
- ids[vDom.data.attrs.id] = vDom;
424
- }
425
- if (vDom.children) {
426
- vDom.children.forEach(findIds);
427
- }
428
- };
429
- findIds(vDom);
430
- this.refIds = ids;
431
-
432
- if (!this._oldVNode) {
433
- this._oldVNode = this.patch(this.renderTarget, vDom);
434
- } else {
435
- this._oldVNode = this.patch(this._oldVNode, vDom);
436
- }
437
- } catch (error) {
438
- console.error("Error during patching:", error);
439
- }
440
- };
441
- }
442
-
443
- /**
444
- * Binds store functions with actual framework data flow
445
- * Makes state changes immutable with immer
446
- * Passes props to selectors
447
- * @param {*} store
448
- * @param {*} props
449
- * @returns
450
- */
451
- const bindStore = (store, props, attrs) => {
452
- const { createInitialState, ...selectorsAndActions } = store;
453
- const selectors = {};
454
- const actions = {};
455
- let currentState = {};
456
- if (createInitialState) {
457
- currentState = createInitialState();
458
- }
459
- Object.entries(selectorsAndActions).forEach(([key, fn]) => {
460
- if (key.startsWith("select")) {
461
- selectors[key] = (...args) => {
462
- return fn({ state: currentState, props, attrs }, ...args);
463
- };
464
- } else {
465
- actions[key] = (payload) => {
466
- currentState = produce(currentState, (draft) => {
467
- return fn(draft, payload);
468
- });
469
- return currentState;
470
- };
471
- }
20
+ validateSchemaContract({
21
+ schema: resolvedSchema,
22
+ methodExports: Object.keys(methods || {}),
472
23
  });
473
24
 
474
- return {
475
- getState: () => currentState,
476
- ...actions,
477
- ...selectors,
478
- };
479
- };
480
-
481
- const createComponent = ({ handlers, view, store, patch, h }, deps) => {
482
- const { elementName, propsSchema, attrsSchema, template, refs, styles } = view;
25
+ const elementName = resolvedSchema.componentName;
26
+ const propsSchema = resolvedSchema.propsSchema;
27
+ const propsSchemaKeys = propsSchema?.properties
28
+ ? [...new Set(Object.keys(propsSchema.properties).map((propKey) => toCamelCase(propKey)))]
29
+ : [];
483
30
 
484
31
  if (!patch) {
485
32
  throw new Error("Patch is not defined");
@@ -489,49 +36,21 @@ const createComponent = ({ handlers, view, store, patch, h }, deps) => {
489
36
  throw new Error("h is not defined");
490
37
  }
491
38
 
492
- if (!view) {
493
- throw new Error("view is not defined");
494
- }
495
-
496
- class MyComponent extends BaseComponent {
497
-
498
- static get observedAttributes() {
499
- const baseAttrs = ["key"];
500
- const attrKeys = attrsSchema?.properties ? Object.keys(attrsSchema.properties) : [];
501
- return [...baseAttrs, ...attrKeys];
502
- }
503
-
504
- constructor() {
505
- super();
506
- const attrsProxy = createAttrsProxy(this);
507
- this.propsSchema = propsSchema;
508
- this.props = propsSchema
509
- ? createPropsProxy(this, Object.keys(propsSchema.properties))
510
- : {};
511
- /**
512
- * TODO currently if user forgot to define propsSchema for a prop
513
- * there will be no warning. would be better to shos some warnng
514
- */
515
- this.elementName = elementName;
516
- this.styles = styles;
517
- this.store = bindStore(store, this.props, attrsProxy);
518
- this.template = template;
519
- this.handlers = handlers;
520
- this.refs = refs;
521
- this.patch = patch;
522
- this.deps = {
523
- ...deps,
524
- store: this.store,
525
- render: this.render,
526
- handlers,
527
- attrs: attrsProxy,
528
- props: this.props,
529
- };
530
- this.h = h;
531
- this.cssText = yamlToCss(elementName, styles);
532
- }
533
- }
534
- return MyComponent;
39
+ return createWebComponentClass({
40
+ elementName,
41
+ propsSchema,
42
+ propsSchemaKeys,
43
+ template,
44
+ refs,
45
+ styles,
46
+ handlers,
47
+ methods,
48
+ constants,
49
+ store,
50
+ patch,
51
+ h,
52
+ deps,
53
+ });
535
54
  };
536
55
 
537
56
  export default createComponent;