@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.
- package/README.md +324 -0
- package/package.json +37 -0
- package/src/common.js +203 -0
- package/src/commonBuild.js +22 -0
- package/src/createComponent.js +486 -0
- package/src/createWebPatch.js +18 -0
- package/src/index.js +7 -0
- package/src/parser.js +399 -0
|
@@ -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;
|