p-elements-core 1.2.30 → 1.2.32-rc-10

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.
Files changed (82) hide show
  1. package/.editorconfig +17 -17
  2. package/.gitlab-ci.yml +18 -18
  3. package/CHANGELOG.md +201 -0
  4. package/demo/sample.js +1 -1
  5. package/demo/screen.css +16 -16
  6. package/demo/theme.css +1 -0
  7. package/dist/p-elements-core-modern.js +1 -1
  8. package/dist/p-elements-core.js +1 -1
  9. package/docs/package-lock.json +6897 -6897
  10. package/docs/package.json +27 -27
  11. package/docs/src/404.md +8 -8
  12. package/docs/src/_data/demos/hello-world/hello-world.tsx +35 -35
  13. package/docs/src/_data/demos/hello-world/index.html +10 -10
  14. package/docs/src/_data/demos/hello-world/project.json +7 -7
  15. package/docs/src/_data/demos/timer/demo-timer.tsx +120 -120
  16. package/docs/src/_data/demos/timer/icons.tsx +62 -62
  17. package/docs/src/_data/demos/timer/index.html +12 -12
  18. package/docs/src/_data/demos/timer/project.json +8 -8
  19. package/docs/src/_data/global.js +13 -13
  20. package/docs/src/_data/helpers.js +19 -19
  21. package/docs/src/_includes/layouts/base.njk +30 -30
  22. package/docs/src/_includes/layouts/playground.njk +40 -40
  23. package/docs/src/_includes/partials/app-header.njk +8 -8
  24. package/docs/src/_includes/partials/head.njk +14 -14
  25. package/docs/src/_includes/partials/nav.njk +19 -19
  26. package/docs/src/_includes/partials/top-nav.njk +51 -51
  27. package/docs/src/documentation/custom-element.md +221 -221
  28. package/docs/src/documentation/decorators/bind.md +71 -71
  29. package/docs/src/documentation/decorators/custom-element-config.md +63 -63
  30. package/docs/src/documentation/decorators/property.md +83 -83
  31. package/docs/src/documentation/decorators/query.md +66 -66
  32. package/docs/src/documentation/decorators/render-property-on-set.md +60 -60
  33. package/docs/src/documentation/decorators.md +9 -9
  34. package/docs/src/documentation/reactive-properties.md +53 -53
  35. package/docs/src/index.d.ts +25 -25
  36. package/docs/src/index.md +3 -3
  37. package/docs/src/scripts/components/app-mode-switch/app-mode-switch.css +78 -78
  38. package/docs/src/scripts/components/app-mode-switch/app-mode-switch.tsx +166 -166
  39. package/docs/src/scripts/components/app-playground/app-playground.tsx +189 -189
  40. package/docs/tsconfig.json +22 -22
  41. package/index.html +15 -2
  42. package/p-elements-core.d.ts +12 -3
  43. package/package.json +11 -4
  44. package/readme.md +206 -206
  45. package/src/custom-element-controller.test.ts +226 -0
  46. package/src/custom-element-controller.ts +31 -31
  47. package/src/custom-element.test.ts +906 -0
  48. package/src/custom-element.ts +471 -188
  49. package/src/custom-style-element.ts +4 -1
  50. package/src/decorators/bind.test.ts +163 -0
  51. package/src/decorators/bind.ts +46 -46
  52. package/src/decorators/custom-element-config.ts +17 -17
  53. package/src/decorators/property.test.ts +279 -0
  54. package/src/decorators/property.ts +822 -150
  55. package/src/decorators/query.test.ts +146 -0
  56. package/src/decorators/query.ts +12 -12
  57. package/src/decorators/render-property-on-set.ts +3 -3
  58. package/src/helpers/css.test.ts +150 -0
  59. package/src/helpers/css.ts +71 -71
  60. package/src/maquette/cache.test.ts +150 -0
  61. package/src/maquette/cache.ts +35 -35
  62. package/src/maquette/dom.test.ts +263 -0
  63. package/src/maquette/dom.ts +115 -115
  64. package/src/maquette/h.test.ts +165 -0
  65. package/src/maquette/h.ts +100 -100
  66. package/src/maquette/index.ts +12 -12
  67. package/src/maquette/interfaces.ts +536 -536
  68. package/src/maquette/jsx.ts +61 -61
  69. package/src/maquette/mapping.test.ts +294 -0
  70. package/src/maquette/mapping.ts +56 -56
  71. package/src/maquette/maquette.test.ts +493 -0
  72. package/src/maquette/projection.test.ts +366 -0
  73. package/src/maquette/projection.ts +666 -666
  74. package/src/maquette/projector.test.ts +351 -0
  75. package/src/maquette/projector.ts +200 -200
  76. package/src/sample/mixin/highlight.tsx +33 -32
  77. package/src/sample/sample.tsx +167 -7
  78. package/src/test-setup.ts +85 -0
  79. package/src/test-utils.ts +223 -0
  80. package/tsconfig.json +1 -0
  81. package/vitest.config.ts +41 -0
  82. package/webpack.config.js +1 -1
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Tests for @query decorator
3
+ * Covers Shadow DOM and Light DOM queries
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import '../test-setup.js';
8
+ import { query } from './query.js';
9
+ import { CustomElement } from '../custom-element.js';
10
+ import { customElementConfig } from './custom-element-config.js';
11
+ import { generateUniqueTagName } from '../test-setup.js';
12
+ import { waitForRender } from '../test-utils.js';
13
+
14
+ describe('@query decorator', () => {
15
+ it('should query element in shadow DOM by default', async () => {
16
+ const tagName = generateUniqueTagName('query-shadow');
17
+
18
+ @customElementConfig({ tagName })
19
+ class QueryTest extends CustomElement {
20
+ static style = ':host { display: block; }';
21
+ @query('.test-target')
22
+ target: HTMLElement;
23
+
24
+ render() {
25
+ return {
26
+ vnodeSelector: 'div',
27
+ properties: {},
28
+ children: [
29
+ {
30
+ vnodeSelector: 'span',
31
+ properties: { class: 'test-target' },
32
+ text: 'Found me!',
33
+ domNode: null as any,
34
+ children: []
35
+ },
36
+ ],
37
+ text: undefined,
38
+ domNode: null
39
+ };
40
+ }
41
+ }
42
+
43
+ const el = document.createElement(tagName) as QueryTest;
44
+ document.body.appendChild(el);
45
+ await waitForRender(el);
46
+
47
+ expect(el.target).toBeDefined();
48
+ expect(el.target.tagName).toBe('SPAN');
49
+ expect(el.target.classList.contains('test-target')).toBe(true);
50
+
51
+ document.body.removeChild(el);
52
+ });
53
+
54
+ it('should query element in light DOM when useShadowRoot is false', async () => {
55
+ const tagName = generateUniqueTagName('query-light');
56
+
57
+ @customElementConfig({ tagName })
58
+ class QueryTest extends CustomElement {
59
+ static style = ':host { display: block; }';
60
+ @query('.light-target', false)
61
+ target: HTMLElement;
62
+
63
+ render() {
64
+ return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
65
+ }
66
+ }
67
+
68
+ const el = document.createElement(tagName) as QueryTest;
69
+ const lightChild = document.createElement('div');
70
+ lightChild.className = 'light-target';
71
+ lightChild.textContent = 'Light DOM';
72
+ el.appendChild(lightChild);
73
+
74
+ document.body.appendChild(el);
75
+ await waitForRender(el);
76
+
77
+ expect(el.target).toBeDefined();
78
+ expect(el.target.className).toBe('light-target');
79
+
80
+ document.body.removeChild(el);
81
+ });
82
+
83
+ it('should return null when element not found', async () => {
84
+ const tagName = generateUniqueTagName('query-missing');
85
+
86
+ @customElementConfig({ tagName })
87
+ class QueryTest extends CustomElement {
88
+ static style = ':host { display: block; }';
89
+ @query('.does-not-exist')
90
+ target: HTMLElement;
91
+
92
+ render() {
93
+ return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
94
+ }
95
+ }
96
+
97
+ const el = document.createElement(tagName) as QueryTest;
98
+ document.body.appendChild(el);
99
+ await waitForRender(el);
100
+
101
+ expect(el.target).toBeNull();
102
+
103
+ document.body.removeChild(el);
104
+ });
105
+
106
+ it('should update query result when DOM changes', async () => {
107
+ const tagName = generateUniqueTagName('query-dynamic');
108
+ let showElement = false;
109
+
110
+ @customElementConfig({ tagName })
111
+ class QueryTest extends CustomElement {
112
+ static style = ':host { display: block; }';
113
+ @query('.dynamic')
114
+ target: HTMLElement;
115
+
116
+ render() {
117
+ const children = showElement
118
+ ? [{ vnodeSelector: 'span', properties: { class: 'dynamic' }, text: 'Dynamic', domNode: null as any, children: [] }]
119
+ : [];
120
+
121
+ return {
122
+ vnodeSelector: 'div',
123
+ properties: {},
124
+ children,
125
+ text: undefined,
126
+ domNode: null
127
+ };
128
+ }
129
+ }
130
+
131
+ const el = document.createElement(tagName) as QueryTest;
132
+ document.body.appendChild(el);
133
+ await waitForRender(el);
134
+
135
+ expect(el.target).toBeNull();
136
+
137
+ showElement = true;
138
+ el.renderNow();
139
+ await waitForRender(el);
140
+
141
+ expect(el.target).toBeDefined();
142
+ expect(el.target.tagName).toBe('SPAN');
143
+
144
+ document.body.removeChild(el);
145
+ });
146
+ });
@@ -1,12 +1,12 @@
1
- export const query = (selector: string, useShadowRoot = true) => {
2
- return function(target: Object, propertyKey: string) {
3
- Object.defineProperty(target, propertyKey, {
4
- get: function() {
5
- if (useShadowRoot) {
6
- return this.shadowRoot.querySelector(selector);
7
- }
8
- return this.querySelector(selector);
9
- },
10
- });
11
- }
12
- };
1
+ export const query = (selector: string, useShadowRoot = true) => {
2
+ return function(target: Object, propertyKey: string) {
3
+ Object.defineProperty(target, propertyKey, {
4
+ get: function() {
5
+ if (useShadowRoot) {
6
+ return this.shadowRoot.querySelector(selector);
7
+ }
8
+ return this.querySelector(selector);
9
+ },
10
+ });
11
+ }
12
+ };
@@ -1,3 +1,3 @@
1
- import {property} from "./property";
2
- const propertDecorator = property({});
3
- export const propertyRenderOnSet = propertDecorator;
1
+ import {property} from "./property";
2
+ const propertDecorator = property({});
3
+ export const propertyRenderOnSet = propertDecorator;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Tests for CSS helpers
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { cssApplyToCssVars, cssApplyVars, replaceApplyToCssVars } from './css.js';
7
+
8
+ describe('CSS helpers', () => {
9
+ describe('cssApplyToCssVars', () => {
10
+ it('should convert CSS @apply mixins to CSS variables', () => {
11
+ const inputCSS = `
12
+ --my-mixin: {
13
+ color: red;
14
+ font-size: 16px;
15
+ };
16
+ `;
17
+
18
+ const result = cssApplyToCssVars(inputCSS);
19
+
20
+ expect(result).toContain('--my-mixin_-_color: red');
21
+ expect(result).toContain('--my-mixin_-_font-size: 16px');
22
+ expect(cssApplyVars.has('--my-mixin')).toBe(true);
23
+ expect(cssApplyVars.get('--my-mixin')).toContain('color');
24
+ expect(cssApplyVars.get('--my-mixin')).toContain('font-size');
25
+ });
26
+
27
+ it('should return null if no mixins found', () => {
28
+ const inputCSS = `
29
+ .class {
30
+ color: blue;
31
+ padding: 10px;
32
+ }
33
+ `;
34
+
35
+ const result = cssApplyToCssVars(inputCSS);
36
+ expect(result).toBeNull();
37
+ });
38
+
39
+ it('should handle multiple mixins', () => {
40
+ const inputCSS = `
41
+ --mixin-one: {
42
+ color: red;
43
+ };
44
+ --mixin-two: {
45
+ background: blue;
46
+ margin: 5px;
47
+ };
48
+ `;
49
+
50
+ const result = cssApplyToCssVars(inputCSS);
51
+
52
+ expect(result).toContain('--mixin-one_-_color: red');
53
+ expect(result).toContain('--mixin-two_-_background: blue');
54
+ expect(result).toContain('--mixin-two_-_margin: 5px');
55
+ expect(cssApplyVars.has('--mixin-one')).toBe(true);
56
+ expect(cssApplyVars.has('--mixin-two')).toBe(true);
57
+ });
58
+
59
+ it('should preserve regular CSS variables', () => {
60
+ const inputCSS = `
61
+ --regular-var: 20px;
62
+ --my-mixin: {
63
+ padding: 10px;
64
+ };
65
+ `;
66
+
67
+ const result = cssApplyToCssVars(inputCSS);
68
+
69
+ expect(result).toContain('--regular-var: 20px');
70
+ expect(result).toContain('--my-mixin_-_padding: 10px');
71
+ });
72
+
73
+ it('should handle empty mixin values', () => {
74
+ const inputCSS = `
75
+ --empty-mixin: {
76
+ ;
77
+ };
78
+ `;
79
+
80
+ const result = cssApplyToCssVars(inputCSS);
81
+ expect(result).not.toBeNull();
82
+ });
83
+ });
84
+
85
+ describe('replaceApplyToCssVars', () => {
86
+ it('should replace @apply with CSS variable references', () => {
87
+ // First set up a mixin
88
+ cssApplyVars.set('--theme-colors', ['color', 'background']);
89
+
90
+ const inputCSS = `
91
+ .element {
92
+ @apply --theme-colors;
93
+ }
94
+ `;
95
+
96
+ const result = replaceApplyToCssVars(inputCSS);
97
+
98
+ expect(result).toContain('color: var(--theme-colors_-_color)');
99
+ expect(result).toContain('background: var(--theme-colors_-_background)');
100
+ expect(result).not.toContain('@apply');
101
+ });
102
+
103
+ it('should handle @apply with parentheses', () => {
104
+ cssApplyVars.set('--button-style', ['padding', 'border']);
105
+
106
+ const inputCSS = `
107
+ button {
108
+ @apply(--button-style);
109
+ }
110
+ `;
111
+
112
+ const result = replaceApplyToCssVars(inputCSS);
113
+
114
+ expect(result).toContain('padding: var(--button-style_-_padding)');
115
+ expect(result).toContain('border: var(--button-style_-_border)');
116
+ });
117
+
118
+ it('should leave CSS unchanged if mixin not defined', () => {
119
+ const inputCSS = `
120
+ .element {
121
+ @apply --undefined-mixin;
122
+ }
123
+ `;
124
+
125
+ const result = replaceApplyToCssVars(inputCSS);
126
+
127
+ // Should still contain @apply since mixin isn't defined
128
+ expect(result).toContain('@apply --undefined-mixin');
129
+ });
130
+
131
+ it('should handle multiple @apply calls', () => {
132
+ cssApplyVars.set('--mixin-a', ['color']);
133
+ cssApplyVars.set('--mixin-b', ['background']);
134
+
135
+ const inputCSS = `
136
+ .one {
137
+ @apply --mixin-a;
138
+ }
139
+ .two {
140
+ @apply --mixin-b;
141
+ }
142
+ `;
143
+
144
+ const result = replaceApplyToCssVars(inputCSS);
145
+
146
+ expect(result).toContain('color: var(--mixin-a_-_color)');
147
+ expect(result).toContain('background: var(--mixin-b_-_background)');
148
+ });
149
+ });
150
+ });
@@ -1,71 +1,71 @@
1
- import { CustomStyleElement } from "../custom-style-element";
2
-
3
- const VAR_ASSIGN =
4
- /(?:^|[;\s{]\s*)(--[\w-]*?)\s*:\s*(?:((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};{])+)|\{([^}]*)\}(?:(?=[;\s}])|$))/gi;
5
-
6
- export const cssApplyVars = new Map<string, string[]>();
7
-
8
- function getMixinValues(valueMixin: string): { key: string; value: string }[] {
9
- return valueMixin
10
- ?.split(";")
11
- .map((mixin: string) => {
12
- const [key, value] = mixin.split(":", 2);
13
- return { key: key.trim(), value: value ? value.trim() : "" };
14
- })
15
- .filter(
16
- (mixin: any) =>
17
- mixin.key && mixin.key !== "" && mixin.value !== ""
18
- );
19
- }
20
-
21
- export function cssApplyToCssVars(style: string): string {
22
- let nrOfReplacements = 0;
23
- const cssText = style.replace(
24
- VAR_ASSIGN,
25
- (matchText, propertyName, valueProperty, valueMixin) => {
26
- if (valueMixin) {
27
- nrOfReplacements++;
28
- const values = getMixinValues(valueMixin);
29
- let replaceText = "";
30
- const props: string[] = [];
31
- values.forEach((mixin: any) => {
32
- props.push(mixin.key);
33
- replaceText +=
34
- `${propertyName}_-_${mixin.key}: ${mixin.value};` + "\n";
35
- });
36
- cssApplyVars.set(propertyName, props);
37
- return matchText.replace(
38
- propertyName,
39
- `${replaceText}
40
- ${propertyName}`
41
- );
42
- }
43
- return matchText;
44
- }
45
- );
46
- return nrOfReplacements > 0 ? cssText : null;
47
- }
48
-
49
- export function replaceApplyToCssVars(cssText: string) : string {
50
- const allVars = (
51
- customElements.get("custom-style") as typeof CustomStyleElement
52
- ).cssApplyVars;
53
-
54
- let style = cssText;
55
- const MIXIN_MATCH = /(?:^|\W+)@apply\s*\(?([^);\n]*)\)?/gi;
56
- let m;
57
- while ((m = MIXIN_MATCH.exec(style))) {
58
- let matchText = m[0];
59
- let mixinName = m[1];
60
- if (allVars.has(mixinName)) {
61
- const searchText = "@apply" + matchText.split("@apply", 2)[1];
62
- const replaceText = allVars
63
- .get(mixinName)
64
- .map((prop: string) => `;${prop}: var(${mixinName}_-_${prop});`)
65
- .join("");
66
-
67
- style = style.replace(searchText, replaceText);
68
- }
69
- }
70
- return style;
71
- }
1
+ import { CustomStyleElement } from "../custom-style-element";
2
+
3
+ const VAR_ASSIGN =
4
+ /(?:^|[;\s{]\s*)(--[\w-]*?)\s*:\s*(?:((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};{])+)|\{([^}]*)\}(?:(?=[;\s}])|$))/gi;
5
+
6
+ export const cssApplyVars = new Map<string, string[]>();
7
+
8
+ function getMixinValues(valueMixin: string): { key: string; value: string }[] {
9
+ return valueMixin
10
+ ?.split(";")
11
+ .map((mixin: string) => {
12
+ const [key, value] = mixin.split(":", 2);
13
+ return { key: key.trim(), value: value ? value.trim() : "" };
14
+ })
15
+ .filter(
16
+ (mixin: any) =>
17
+ mixin.key && mixin.key !== "" && mixin.value !== ""
18
+ );
19
+ }
20
+
21
+ export function cssApplyToCssVars(style: string): string {
22
+ let nrOfReplacements = 0;
23
+ const cssText = style.replace(
24
+ VAR_ASSIGN,
25
+ (matchText, propertyName, valueProperty, valueMixin) => {
26
+ if (valueMixin) {
27
+ nrOfReplacements++;
28
+ const values = getMixinValues(valueMixin);
29
+ let replaceText = "";
30
+ const props: string[] = [];
31
+ values.forEach((mixin: any) => {
32
+ props.push(mixin.key);
33
+ replaceText +=
34
+ `${propertyName}_-_${mixin.key}: ${mixin.value};` + "\n";
35
+ });
36
+ cssApplyVars.set(propertyName, props);
37
+ return matchText.replace(
38
+ propertyName,
39
+ `${replaceText}
40
+ ${propertyName}`
41
+ );
42
+ }
43
+ return matchText;
44
+ }
45
+ );
46
+ return nrOfReplacements > 0 ? cssText : null;
47
+ }
48
+
49
+ export function replaceApplyToCssVars(cssText: string) : string {
50
+ const allVars = (
51
+ customElements.get("custom-style") as typeof CustomStyleElement
52
+ ).cssApplyVars;
53
+
54
+ let style = cssText;
55
+ const MIXIN_MATCH = /(?:^|\W+)@apply\s*\(?([^);\n]*)\)?/gi;
56
+ let m;
57
+ while ((m = MIXIN_MATCH.exec(style))) {
58
+ let matchText = m[0];
59
+ let mixinName = m[1];
60
+ if (allVars.has(mixinName)) {
61
+ const searchText = "@apply" + matchText.split("@apply", 2)[1];
62
+ const replaceText = allVars
63
+ .get(mixinName)
64
+ .map((prop: string) => `;${prop}: var(${mixinName}_-_${prop});`)
65
+ .join("");
66
+
67
+ style = style.replace(searchText, replaceText);
68
+ }
69
+ }
70
+ return style;
71
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Tests for Maquette cache module
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { createCache } from './cache.js';
7
+
8
+ describe('Maquette cache', () => {
9
+ describe('createCache', () => {
10
+ it('should create a cache instance', () => {
11
+ const cache = createCache<number>();
12
+ expect(cache).toBeDefined();
13
+ expect(cache.result).toBeDefined();
14
+ expect(cache.invalidate).toBeDefined();
15
+ });
16
+
17
+ it('should calculate result on first call', () => {
18
+ const cache = createCache<number>();
19
+ let calculationCount = 0;
20
+
21
+ const result = cache.result([1, 2, 3], () => {
22
+ calculationCount++;
23
+ return 10;
24
+ });
25
+
26
+ expect(result).toBe(10);
27
+ expect(calculationCount).toBe(1);
28
+ });
29
+
30
+ it('should return cached result when inputs unchanged', () => {
31
+ const cache = createCache<number>();
32
+ let calculationCount = 0;
33
+
34
+ const inputs = [1, 2, 3];
35
+ const calculation = () => {
36
+ calculationCount++;
37
+ return 10;
38
+ };
39
+
40
+ cache.result(inputs, calculation);
41
+ const result2 = cache.result(inputs, calculation);
42
+ const result3 = cache.result(inputs, calculation);
43
+
44
+ expect(result2).toBe(10);
45
+ expect(result3).toBe(10);
46
+ expect(calculationCount).toBe(1); // Only calculated once
47
+ });
48
+
49
+ it('should recalculate when inputs change', () => {
50
+ const cache = createCache<number>();
51
+ let calculationCount = 0;
52
+
53
+ cache.result([1, 2, 3], () => {
54
+ calculationCount++;
55
+ return 10;
56
+ });
57
+
58
+ cache.result([1, 2, 4], () => {
59
+ calculationCount++;
60
+ return 20;
61
+ });
62
+
63
+ expect(calculationCount).toBe(2);
64
+ });
65
+
66
+ it('should detect changes in any input position', () => {
67
+ const cache = createCache<string>();
68
+ let calculationCount = 0;
69
+
70
+ const calc = () => {
71
+ calculationCount++;
72
+ return 'result';
73
+ };
74
+
75
+ cache.result(['a', 'b', 'c'], calc);
76
+ cache.result(['x', 'b', 'c'], calc); // First element changed
77
+ cache.result(['x', 'y', 'c'], calc); // Second element changed
78
+ cache.result(['x', 'y', 'z'], calc); // Third element changed
79
+
80
+ expect(calculationCount).toBe(4);
81
+ });
82
+
83
+ it('should invalidate cache', () => {
84
+ const cache = createCache<number>();
85
+ let calculationCount = 0;
86
+
87
+ const inputs = [1, 2, 3];
88
+ const calculation = () => {
89
+ calculationCount++;
90
+ return 10;
91
+ };
92
+
93
+ cache.result(inputs, calculation);
94
+ cache.invalidate();
95
+ cache.result(inputs, calculation);
96
+
97
+ expect(calculationCount).toBe(2);
98
+ });
99
+
100
+ it('should handle empty inputs', () => {
101
+ const cache = createCache<number>();
102
+ let calculationCount = 0;
103
+
104
+ const result1 = cache.result([], () => {
105
+ calculationCount++;
106
+ return 42;
107
+ });
108
+
109
+ const result2 = cache.result([], () => {
110
+ calculationCount++;
111
+ return 42;
112
+ });
113
+
114
+ expect(result1).toBe(42);
115
+ expect(result2).toBe(42);
116
+ expect(calculationCount).toBe(1);
117
+ });
118
+
119
+ it('should cache complex objects', () => {
120
+ const cache = createCache<{ value: number }>();
121
+ let calculationCount = 0;
122
+
123
+ const obj1 = { id: 1 };
124
+ const obj2 = { id: 2 };
125
+
126
+ const result1 = cache.result([obj1, obj2], () => {
127
+ calculationCount++;
128
+ return { value: 100 };
129
+ });
130
+
131
+ const result2 = cache.result([obj1, obj2], () => {
132
+ calculationCount++;
133
+ return { value: 200 };
134
+ });
135
+
136
+ expect(result1).toBe(result2); // Same cached object
137
+ expect(calculationCount).toBe(1);
138
+ });
139
+
140
+ it('should invalidate and allow new calculations', () => {
141
+ const cache = createCache<string>();
142
+
143
+ cache.result([1], () => 'first');
144
+ cache.invalidate();
145
+ const result = cache.result([2], () => 'second');
146
+
147
+ expect(result).toBe('second');
148
+ });
149
+ });
150
+ });
@@ -1,35 +1,35 @@
1
- import { CalculationCache } from "./interfaces";
2
-
3
- /**
4
- * Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees.
5
- * In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem.
6
- * For more information, see [[CalculationCache]].
7
- *
8
- * @param <Result> The type of the value that is cached.
9
- */
10
- export let createCache = <Result,>(): CalculationCache<Result> => {
11
- let cachedInputs: unknown[] | undefined;
12
- let cachedOutcome: Result | undefined;
13
-
14
- return {
15
- invalidate: () => {
16
- cachedOutcome = undefined;
17
- cachedInputs = undefined;
18
- },
19
-
20
- result: (inputs: unknown[], calculation: () => Result) => {
21
- if (cachedInputs) {
22
- for (let i = 0; i < inputs.length; i++) {
23
- if (cachedInputs[i] !== inputs[i]) {
24
- cachedOutcome = undefined;
25
- }
26
- }
27
- }
28
- if (!cachedOutcome) {
29
- cachedOutcome = calculation();
30
- cachedInputs = inputs;
31
- }
32
- return cachedOutcome;
33
- },
34
- };
35
- };
1
+ import { CalculationCache } from "./interfaces";
2
+
3
+ /**
4
+ * Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees.
5
+ * In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem.
6
+ * For more information, see [[CalculationCache]].
7
+ *
8
+ * @param <Result> The type of the value that is cached.
9
+ */
10
+ export let createCache = <Result,>(): CalculationCache<Result> => {
11
+ let cachedInputs: unknown[] | undefined;
12
+ let cachedOutcome: Result | undefined;
13
+
14
+ return {
15
+ invalidate: () => {
16
+ cachedOutcome = undefined;
17
+ cachedInputs = undefined;
18
+ },
19
+
20
+ result: (inputs: unknown[], calculation: () => Result) => {
21
+ if (cachedInputs) {
22
+ for (let i = 0; i < inputs.length; i++) {
23
+ if (cachedInputs[i] !== inputs[i]) {
24
+ cachedOutcome = undefined;
25
+ }
26
+ }
27
+ }
28
+ if (!cachedOutcome) {
29
+ cachedOutcome = calculation();
30
+ cachedInputs = inputs;
31
+ }
32
+ return cachedOutcome;
33
+ },
34
+ };
35
+ };