@rettangoli/fe 0.0.3

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