wrec 0.0.2 → 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 CHANGED
@@ -1,4 +1,4 @@
1
- # Web Reactive Component (Wrec)
1
+ # Web Reactive Component (wrec)
2
2
 
3
3
  <img alt="shipwreck" src="shipwreck.png" style="width: 256px">
4
4
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wrec",
3
3
  "author": "R. Mark Volkmann",
4
- "version": "0.0.2",
4
+ "version": "0.0.3",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -11,9 +11,18 @@
11
11
  "web",
12
12
  "component"
13
13
  ],
14
+ "main": "dist/wrec.min.js",
15
+ "exports": {
16
+ "import": "./dist/wrec.min.js",
17
+ "require": "./dist/wrec.min.js"
18
+ },
19
+ "files": [
20
+ "dist/wrec.min.js",
21
+ "README.md"
22
+ ],
14
23
  "scripts": {
15
24
  "lint": "oxlint",
16
- "minify": "terser wrec.js -c -m -o wrec.min.js"
25
+ "minify": "terser wrec.js -c -m -o dist/wrec.min.js"
17
26
  },
18
27
  "devDependencies": {
19
28
  "oxlint": "^1.5.0",
@@ -1,3 +0,0 @@
1
- {
2
- "cSpell.words": ["Wrec"]
3
- }
@@ -1,91 +0,0 @@
1
- const template = document.createElement("template");
2
- template.innerHTML = /*html*/ `
3
- <style>
4
- :not(:defined) {
5
- visibility: hidden;
6
- }
7
-
8
- .counter {
9
- display: flex;
10
- align-items: center;
11
- gap: 0.5rem;
12
- }
13
-
14
- button {
15
- background-color: lightgreen;
16
- }
17
-
18
- button:disabled {
19
- background-color: gray;
20
- }
21
- </style>
22
- <div>
23
- <button id="decrement-btn" type="button">-</button>
24
- <span></span>
25
- <button id="increment-btn" type="button">+</button>
26
- </div>
27
- `;
28
-
29
- class CounterVanilla extends HTMLElement {
30
- static get observedAttributes() {
31
- return ["count"];
32
- }
33
-
34
- constructor() {
35
- super();
36
- this.attachShadow({ mode: "open" });
37
- }
38
- attributeChangedCallback() {
39
- if (this.isConnected) this.#update();
40
- }
41
-
42
- connectedCallback() {
43
- const root = this.shadowRoot;
44
- root.appendChild(template.content.cloneNode(true));
45
-
46
- this.decrementBtn = root.querySelector("#decrement-btn");
47
- this.decrementBtn.addEventListener("click", () => {
48
- this.decrement();
49
- });
50
- root.querySelector("#increment-btn").addEventListener("click", () => {
51
- this.increment();
52
- });
53
-
54
- this.span = root.querySelector("span");
55
- this.#update();
56
- }
57
-
58
- // Treat the count attribute as the source of truth
59
- // rather than adding a property.
60
- get count() {
61
- return this.getAttribute("count") || 0;
62
- }
63
-
64
- set count(newCount) {
65
- this.setAttribute("count", newCount);
66
- }
67
-
68
- decrement() {
69
- if (this.count == 0) return;
70
-
71
- this.count--;
72
- // this.count gets converted to a string,
73
- // so we have to use == instead of === on the next line.
74
- if (this.count == 0) {
75
- this.decrementBtn.setAttribute("disabled", "disabled");
76
- }
77
- this.#update();
78
- }
79
-
80
- increment() {
81
- this.count++;
82
- this.decrementBtn.removeAttribute("disabled");
83
- this.#update();
84
- }
85
-
86
- #update() {
87
- if (this.span) this.span.textContent = this.count;
88
- }
89
- }
90
-
91
- customElements.define("counter-vanilla", CounterVanilla);
@@ -1,47 +0,0 @@
1
- import Wrec from "../Wrec.js";
2
-
3
- class CounterWrec extends Wrec {
4
- static properties = {
5
- count: { type: Number, reflect: true },
6
- };
7
-
8
- css() {
9
- return /*css*/ `
10
- :not(:defined) {
11
- visibility: hidden;
12
- }
13
-
14
- .counter {
15
- display: flex;
16
- align-items: center;
17
- gap: 0.5rem;
18
- }
19
-
20
- button {
21
- background-color: lightgreen;
22
- }
23
-
24
- button:disabled {
25
- background-color: gray;
26
- }
27
- `;
28
- }
29
-
30
- html() {
31
- return /*html*/ `
32
- <div>
33
- <button onClick="decrement" type="button"
34
- disabled="this.count === 0">-</button>
35
- <span>this.count</span>
36
- <button onClick="this.count++" type="button">+</button>
37
- <span>(this.count < 10 ? "single" : "double") + " digit"</span>
38
- </div>
39
- `;
40
- }
41
-
42
- decrement() {
43
- if (this.count > 0) this.count--;
44
- }
45
- }
46
-
47
- CounterWrec.register();
package/demo/data-bind.js DELETED
@@ -1,65 +0,0 @@
1
- import Wrec from "../wrec.js";
2
-
3
- class DataBind extends Wrec {
4
- static formAssociated = true;
5
- static properties = {
6
- color: { type: String, reflect: true },
7
- name: { type: String, reflect: true },
8
- score: { type: Number, reflect: true },
9
- story: { type: String, reflect: true },
10
- };
11
-
12
- css() {
13
- return /*css*/ `
14
- :host {
15
- font-family: sans-serif;
16
- }
17
-
18
- label {
19
- font-weight: bold;
20
- }
21
- `;
22
- }
23
-
24
- html() {
25
- return /*html*/ `
26
- <div>
27
- <div>
28
- <label>Name:</label>
29
- <input value="this.name">
30
- <p>Hello, <span>this.name</span>!</p>
31
- </div>
32
- <div style="display: flex">
33
- <label for="color">Color:</label>
34
- <radio-group name="color" options="red,green,blue" value="this.color"></radio-group>
35
- </div>
36
- <div>
37
- <label>Color:</label>
38
- <select value="this.color">
39
- <option value="red">Red</option>
40
- <option value="green">Green</option>
41
- <option value="blue">Blue</option>
42
- </select>
43
- </div>
44
- <p>You selected the color <span>this.color</span>.</p>
45
- <div>
46
- <label>Story:</label>
47
- <textarea>this.story</textarea>
48
- <p>Your story is <span>this.story</span>.</p>
49
- </div>
50
- <number-input label="Favorite Number:" value="this.score"></number-input>
51
- <number-slider label="Slider:" value="this.score"></number-slider>
52
- <p>Your score is <span>this.score</span>.</p>
53
- </div>
54
- `;
55
- }
56
-
57
- formResetCallback() {
58
- this.color = "red";
59
- this.name = "";
60
- this.score = 0;
61
- this.story = "";
62
- }
63
- }
64
-
65
- DataBind.register();
@@ -1,22 +0,0 @@
1
- import Wrec from "../wrec.js";
2
-
3
- class HelloWorld extends Wrec {
4
- static properties = {
5
- name: { type: String, value: "World", reflect: true },
6
- };
7
-
8
- css() {
9
- return /*css*/ `p { color: purple; }`;
10
- }
11
-
12
- html() {
13
- return /*html*/ `
14
- <p>
15
- Hello, <span>this.name</span>.
16
- Shouting <span>this.name.toUpperCase()</span>!
17
- </p>
18
- `;
19
- }
20
- }
21
-
22
- HelloWorld.register();
package/demo/index.html DELETED
@@ -1,38 +0,0 @@
1
- <html>
2
- <head>
3
- <style></style>
4
-
5
- <script src="counter-vanilla.js" type="module"></script>
6
- <script src="counter-wrec.js" type="module"></script>
7
- <script src="data-bind.js" type="module"></script>
8
- <script src="hello-world.js" type="module"></script>
9
- <script src="multiply-numbers.js" type="module"></script>
10
- <script src="number-input.js" type="module"></script>
11
- <script src="number-slider.js" type="module"></script>
12
- <script src="radio-group.js" type="module"></script>
13
- <script src="temperature-eval.js" type="module"></script>
14
- </head>
15
- <body>
16
- <h2>Vanilla</h2>
17
- <counter-vanilla count="3"></counter-vanilla>
18
-
19
- <h2>Wrec</h2>
20
- <temperature-eval temperature="100"></temperature-eval>
21
- <counter-wrec count="3"></counter-wrec>
22
- <counter-wrec count="1"></counter-wrec>
23
- <hello-world></hello-world>
24
- <hello-world name="Mark"></hello-world>
25
- <multiply-numbers n1="3" n2="4"></multiply-numbers>
26
- <hr />
27
- <form method="GET" action="/missing">
28
- <data-bind
29
- name="Mark"
30
- color="blue"
31
- score="5"
32
- story="Once upon a time..."
33
- ></data-bind>
34
- <button>Submit</button>
35
- <button type="reset">Reset</button>
36
- </form>
37
- </body>
38
- </html>
@@ -1,19 +0,0 @@
1
- import Wrec from "../wrec.js";
2
-
3
- class MultiplyNumbers extends Wrec {
4
- static properties = {
5
- n1: { type: Number },
6
- n2: { type: Number },
7
- };
8
-
9
- html() {
10
- return /*html*/ `
11
- <p>
12
- <span>this.n1</span> * <span>this.n2</span> =
13
- <span>this.n1 * this.n2</span>
14
- </p>
15
- `;
16
- }
17
- }
18
-
19
- MultiplyNumbers.register();
@@ -1,53 +0,0 @@
1
- import Wrec from "../wrec.js";
2
-
3
- class NumberInput extends Wrec {
4
- static formAssociated = true;
5
- static properties = {
6
- label: { type: String, reflect: true },
7
- value: { type: Number, reflect: true },
8
- };
9
-
10
- css() {
11
- return /*css*/ `
12
- button {
13
- background-color: cornflowerblue;
14
- border: none;
15
- border-radius: 50%;
16
- color: white;
17
- }
18
-
19
- input[type="number"] {
20
- text-align: right;
21
- width: 2rem;
22
- }
23
-
24
- input[type="number"]::-webkit-inner-spin-button,
25
- input[type="number"]::-webkit-outer-spin-button {
26
- appearance: none;
27
- }
28
-
29
- label { font-weight: bold; }
30
- `;
31
- }
32
-
33
- html() {
34
- return /*html*/ `
35
- <div>
36
- <label>this.label</label>
37
- <button onclick="decrement" type="button">-</button>
38
- <input type="number" value="this.value" />
39
- <button onclick="increment" type="button">+</button>
40
- </div>
41
- `;
42
- }
43
-
44
- decrement() {
45
- if (this.value > 0) this.value--;
46
- }
47
-
48
- increment() {
49
- this.value++;
50
- }
51
- }
52
-
53
- NumberInput.register();
@@ -1,29 +0,0 @@
1
- import Wrec from "../wrec.js";
2
-
3
- class NumberSlider extends Wrec {
4
- static properties = {
5
- label: { type: String, reflect: true },
6
- value: { type: Number, reflect: true },
7
- };
8
-
9
- css() {
10
- return /*css*/ `
11
- input[type="number"] {
12
- width: 6rem;
13
- }
14
-
15
- label { font-weight: bold; }
16
- `;
17
- }
18
-
19
- html() {
20
- return /*html*/ `
21
- <div>
22
- <label>this.label</label>
23
- <input type="range" min="0" value="this.value" />
24
- </div>
25
- `;
26
- }
27
- }
28
-
29
- NumberSlider.register();
@@ -1,341 +0,0 @@
1
- {
2
- "name": "wrec-demo",
3
- "lockfileVersion": 3,
4
- "requires": true,
5
- "packages": {
6
- "": {
7
- "name": "wrec-demo",
8
- "devDependencies": {
9
- "oxlint": "^1.5.0",
10
- "vite": "^7.0.1"
11
- }
12
- },
13
- "node_modules/@esbuild/darwin-arm64": {
14
- "version": "0.25.5",
15
- "cpu": [
16
- "arm64"
17
- ],
18
- "dev": true,
19
- "license": "MIT",
20
- "optional": true,
21
- "os": [
22
- "darwin"
23
- ],
24
- "engines": {
25
- "node": ">=18"
26
- }
27
- },
28
- "node_modules/@oxlint/darwin-arm64": {
29
- "version": "1.5.0",
30
- "cpu": [
31
- "arm64"
32
- ],
33
- "dev": true,
34
- "license": "MIT",
35
- "optional": true,
36
- "os": [
37
- "darwin"
38
- ]
39
- },
40
- "node_modules/@rollup/rollup-darwin-arm64": {
41
- "version": "4.44.1",
42
- "cpu": [
43
- "arm64"
44
- ],
45
- "dev": true,
46
- "license": "MIT",
47
- "optional": true,
48
- "os": [
49
- "darwin"
50
- ]
51
- },
52
- "node_modules/@types/estree": {
53
- "version": "1.0.8",
54
- "dev": true,
55
- "license": "MIT"
56
- },
57
- "node_modules/esbuild": {
58
- "version": "0.25.5",
59
- "dev": true,
60
- "hasInstallScript": true,
61
- "license": "MIT",
62
- "bin": {
63
- "esbuild": "bin/esbuild"
64
- },
65
- "engines": {
66
- "node": ">=18"
67
- },
68
- "optionalDependencies": {
69
- "@esbuild/aix-ppc64": "0.25.5",
70
- "@esbuild/android-arm": "0.25.5",
71
- "@esbuild/android-arm64": "0.25.5",
72
- "@esbuild/android-x64": "0.25.5",
73
- "@esbuild/darwin-arm64": "0.25.5",
74
- "@esbuild/darwin-x64": "0.25.5",
75
- "@esbuild/freebsd-arm64": "0.25.5",
76
- "@esbuild/freebsd-x64": "0.25.5",
77
- "@esbuild/linux-arm": "0.25.5",
78
- "@esbuild/linux-arm64": "0.25.5",
79
- "@esbuild/linux-ia32": "0.25.5",
80
- "@esbuild/linux-loong64": "0.25.5",
81
- "@esbuild/linux-mips64el": "0.25.5",
82
- "@esbuild/linux-ppc64": "0.25.5",
83
- "@esbuild/linux-riscv64": "0.25.5",
84
- "@esbuild/linux-s390x": "0.25.5",
85
- "@esbuild/linux-x64": "0.25.5",
86
- "@esbuild/netbsd-arm64": "0.25.5",
87
- "@esbuild/netbsd-x64": "0.25.5",
88
- "@esbuild/openbsd-arm64": "0.25.5",
89
- "@esbuild/openbsd-x64": "0.25.5",
90
- "@esbuild/sunos-x64": "0.25.5",
91
- "@esbuild/win32-arm64": "0.25.5",
92
- "@esbuild/win32-ia32": "0.25.5",
93
- "@esbuild/win32-x64": "0.25.5"
94
- }
95
- },
96
- "node_modules/fdir": {
97
- "version": "6.4.6",
98
- "dev": true,
99
- "license": "MIT",
100
- "peerDependencies": {
101
- "picomatch": "^3 || ^4"
102
- },
103
- "peerDependenciesMeta": {
104
- "picomatch": {
105
- "optional": true
106
- }
107
- }
108
- },
109
- "node_modules/fsevents": {
110
- "version": "2.3.3",
111
- "dev": true,
112
- "license": "MIT",
113
- "optional": true,
114
- "os": [
115
- "darwin"
116
- ],
117
- "engines": {
118
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
119
- }
120
- },
121
- "node_modules/nanoid": {
122
- "version": "3.3.11",
123
- "dev": true,
124
- "funding": [
125
- {
126
- "type": "github",
127
- "url": "https://github.com/sponsors/ai"
128
- }
129
- ],
130
- "license": "MIT",
131
- "bin": {
132
- "nanoid": "bin/nanoid.cjs"
133
- },
134
- "engines": {
135
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
136
- }
137
- },
138
- "node_modules/oxlint": {
139
- "version": "1.5.0",
140
- "dev": true,
141
- "license": "MIT",
142
- "bin": {
143
- "oxc_language_server": "bin/oxc_language_server",
144
- "oxlint": "bin/oxlint"
145
- },
146
- "engines": {
147
- "node": ">=8.*"
148
- },
149
- "funding": {
150
- "url": "https://github.com/sponsors/Boshen"
151
- },
152
- "optionalDependencies": {
153
- "@oxlint/darwin-arm64": "1.5.0",
154
- "@oxlint/darwin-x64": "1.5.0",
155
- "@oxlint/linux-arm64-gnu": "1.5.0",
156
- "@oxlint/linux-arm64-musl": "1.5.0",
157
- "@oxlint/linux-x64-gnu": "1.5.0",
158
- "@oxlint/linux-x64-musl": "1.5.0",
159
- "@oxlint/win32-arm64": "1.5.0",
160
- "@oxlint/win32-x64": "1.5.0"
161
- }
162
- },
163
- "node_modules/picocolors": {
164
- "version": "1.1.1",
165
- "dev": true,
166
- "license": "ISC"
167
- },
168
- "node_modules/picomatch": {
169
- "version": "4.0.2",
170
- "dev": true,
171
- "license": "MIT",
172
- "engines": {
173
- "node": ">=12"
174
- },
175
- "funding": {
176
- "url": "https://github.com/sponsors/jonschlinkert"
177
- }
178
- },
179
- "node_modules/postcss": {
180
- "version": "8.5.6",
181
- "dev": true,
182
- "funding": [
183
- {
184
- "type": "opencollective",
185
- "url": "https://opencollective.com/postcss/"
186
- },
187
- {
188
- "type": "tidelift",
189
- "url": "https://tidelift.com/funding/github/npm/postcss"
190
- },
191
- {
192
- "type": "github",
193
- "url": "https://github.com/sponsors/ai"
194
- }
195
- ],
196
- "license": "MIT",
197
- "dependencies": {
198
- "nanoid": "^3.3.11",
199
- "picocolors": "^1.1.1",
200
- "source-map-js": "^1.2.1"
201
- },
202
- "engines": {
203
- "node": "^10 || ^12 || >=14"
204
- }
205
- },
206
- "node_modules/rollup": {
207
- "version": "4.44.1",
208
- "dev": true,
209
- "license": "MIT",
210
- "dependencies": {
211
- "@types/estree": "1.0.8"
212
- },
213
- "bin": {
214
- "rollup": "dist/bin/rollup"
215
- },
216
- "engines": {
217
- "node": ">=18.0.0",
218
- "npm": ">=8.0.0"
219
- },
220
- "optionalDependencies": {
221
- "@rollup/rollup-android-arm-eabi": "4.44.1",
222
- "@rollup/rollup-android-arm64": "4.44.1",
223
- "@rollup/rollup-darwin-arm64": "4.44.1",
224
- "@rollup/rollup-darwin-x64": "4.44.1",
225
- "@rollup/rollup-freebsd-arm64": "4.44.1",
226
- "@rollup/rollup-freebsd-x64": "4.44.1",
227
- "@rollup/rollup-linux-arm-gnueabihf": "4.44.1",
228
- "@rollup/rollup-linux-arm-musleabihf": "4.44.1",
229
- "@rollup/rollup-linux-arm64-gnu": "4.44.1",
230
- "@rollup/rollup-linux-arm64-musl": "4.44.1",
231
- "@rollup/rollup-linux-loongarch64-gnu": "4.44.1",
232
- "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1",
233
- "@rollup/rollup-linux-riscv64-gnu": "4.44.1",
234
- "@rollup/rollup-linux-riscv64-musl": "4.44.1",
235
- "@rollup/rollup-linux-s390x-gnu": "4.44.1",
236
- "@rollup/rollup-linux-x64-gnu": "4.44.1",
237
- "@rollup/rollup-linux-x64-musl": "4.44.1",
238
- "@rollup/rollup-win32-arm64-msvc": "4.44.1",
239
- "@rollup/rollup-win32-ia32-msvc": "4.44.1",
240
- "@rollup/rollup-win32-x64-msvc": "4.44.1",
241
- "fsevents": "~2.3.2"
242
- }
243
- },
244
- "node_modules/source-map-js": {
245
- "version": "1.2.1",
246
- "dev": true,
247
- "license": "BSD-3-Clause",
248
- "engines": {
249
- "node": ">=0.10.0"
250
- }
251
- },
252
- "node_modules/tinyglobby": {
253
- "version": "0.2.14",
254
- "dev": true,
255
- "license": "MIT",
256
- "dependencies": {
257
- "fdir": "^6.4.4",
258
- "picomatch": "^4.0.2"
259
- },
260
- "engines": {
261
- "node": ">=12.0.0"
262
- },
263
- "funding": {
264
- "url": "https://github.com/sponsors/SuperchupuDev"
265
- }
266
- },
267
- "node_modules/vite": {
268
- "version": "7.0.1",
269
- "dev": true,
270
- "license": "MIT",
271
- "dependencies": {
272
- "esbuild": "^0.25.0",
273
- "fdir": "^6.4.6",
274
- "picomatch": "^4.0.2",
275
- "postcss": "^8.5.6",
276
- "rollup": "^4.40.0",
277
- "tinyglobby": "^0.2.14"
278
- },
279
- "bin": {
280
- "vite": "bin/vite.js"
281
- },
282
- "engines": {
283
- "node": "^20.19.0 || >=22.12.0"
284
- },
285
- "funding": {
286
- "url": "https://github.com/vitejs/vite?sponsor=1"
287
- },
288
- "optionalDependencies": {
289
- "fsevents": "~2.3.3"
290
- },
291
- "peerDependencies": {
292
- "@types/node": "^20.19.0 || >=22.12.0",
293
- "jiti": ">=1.21.0",
294
- "less": "^4.0.0",
295
- "lightningcss": "^1.21.0",
296
- "sass": "^1.70.0",
297
- "sass-embedded": "^1.70.0",
298
- "stylus": ">=0.54.8",
299
- "sugarss": "^5.0.0",
300
- "terser": "^5.16.0",
301
- "tsx": "^4.8.1",
302
- "yaml": "^2.4.2"
303
- },
304
- "peerDependenciesMeta": {
305
- "@types/node": {
306
- "optional": true
307
- },
308
- "jiti": {
309
- "optional": true
310
- },
311
- "less": {
312
- "optional": true
313
- },
314
- "lightningcss": {
315
- "optional": true
316
- },
317
- "sass": {
318
- "optional": true
319
- },
320
- "sass-embedded": {
321
- "optional": true
322
- },
323
- "stylus": {
324
- "optional": true
325
- },
326
- "sugarss": {
327
- "optional": true
328
- },
329
- "terser": {
330
- "optional": true
331
- },
332
- "tsx": {
333
- "optional": true
334
- },
335
- "yaml": {
336
- "optional": true
337
- }
338
- }
339
- }
340
- }
341
- }
package/demo/package.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "name": "wrec-demo",
3
- "scripts": {
4
- "dev": "vite",
5
- "lint": "oxlint"
6
- },
7
- "devDependencies": {
8
- "oxlint": "^1.5.0",
9
- "vite": "^7.0.1"
10
- }
11
- }
@@ -1,83 +0,0 @@
1
- import Wrec from "../wrec.js";
2
-
3
- class RadioGroup extends Wrec {
4
- // This is the only thing a Wrec subclass
5
- // must contain to contribute to form submission.
6
- static formAssociated = true;
7
-
8
- static properties = {
9
- default: { type: String },
10
- name: { type: String },
11
- options: { type: String },
12
- value: { type: String },
13
- };
14
-
15
- attributeChangedCallback(attr, oldValue, newValue) {
16
- super.attributeChangedCallback(attr, oldValue, newValue);
17
- if (attr === "value") {
18
- const inputs = this.shadowRoot.querySelectorAll("input");
19
- for (const input of inputs) {
20
- input.checked = input.value === newValue;
21
- }
22
- }
23
- }
24
-
25
- connectedCallback() {
26
- super.connectedCallback();
27
- if (!this.default) this.default = this.options.split(",")[0];
28
- if (!this.value) this.value = this.default;
29
- }
30
-
31
- css() {
32
- return /*css*/ `
33
- .radio-group {
34
- display: flex;
35
- gap: 0.25rem;
36
-
37
- > div {
38
- display: flex;
39
- align-items: center;
40
- }
41
- }
42
- `;
43
- }
44
-
45
- html() {
46
- // This web component uses iteration to determine what to render.
47
- return /*html*/ `
48
- <div class="radio-group">
49
- this.options.split(",").map((option) => this.makeRadio(option)).join("")
50
- </div>
51
- `;
52
- }
53
-
54
- // This method cannot be private because it is called when
55
- // a change event is dispatched from a radio button.
56
- handleChange(event) {
57
- const { value } = event.target;
58
- this.value = value;
59
-
60
- // This allows users of the this web component to listen for changes.
61
- this.dispatchEvent(new Event("change"));
62
- }
63
-
64
- // This method cannot be private because it is
65
- // called from the expression in the html method.
66
- makeRadio(option) {
67
- return /*html*/ `
68
- <div>
69
- <input
70
- type="radio"
71
- id="${option}"
72
- name="${this.name}"
73
- value="${option}"
74
- ${option === this.value ? "checked" : ""}
75
- onchange="handleChange"
76
- />
77
- <label for="${option}">${option}</label>
78
- </div>
79
- `;
80
- }
81
- }
82
-
83
- RadioGroup.register();
@@ -1,16 +0,0 @@
1
- import Wrec from "../wrec.js";
2
-
3
- class TemperatureEval extends Wrec {
4
- static properties = {
5
- temperature: { type: Number },
6
- };
7
-
8
- html() {
9
- // This web component uses conditional logic to determine what to render.
10
- return /*html*/ `
11
- <p>this.temperature < 32 ? "freezing" : "not freezing"</p>
12
- `;
13
- }
14
- }
15
-
16
- TemperatureEval.register();
package/shipwreck.png DELETED
Binary file
package/wrec.js DELETED
@@ -1,348 +0,0 @@
1
- const FIRST_CHAR = "a-zA-Z_$";
2
- const OTHER_CHAR = FIRST_CHAR + "0-9";
3
- const IDENTIFIER = `[${FIRST_CHAR}][${OTHER_CHAR}]*`;
4
- const REFERENCE_RE = new RegExp("this." + IDENTIFIER, "g");
5
- const SKIP = "this.".length;
6
-
7
- class Wrec extends HTMLElement {
8
- static #propertyToExpressionsMap = new Map();
9
- static #template = document.createElement("template");
10
-
11
- #expressionReferencesMap = new Map();
12
- #formData;
13
- #internals;
14
- #propertyToBindingsMap = new Map();
15
-
16
- constructor() {
17
- super();
18
- this.attachShadow({ mode: "open" });
19
-
20
- if (this.constructor.formAssociated) {
21
- this.#internals = this.attachInternals();
22
- this.#formData = new FormData();
23
- this.#internals.setFormValue(this.#formData);
24
- }
25
- }
26
-
27
- attributeChangedCallback(attrName, _, newValue) {
28
- // Update the corresponding property.
29
- const value = this.#getTypedValue(attrName, newValue);
30
- this[attrName] = value;
31
- this.#setFormValue(attrName, value);
32
- }
33
-
34
- #bind(element, propertyName, attrName) {
35
- element.addEventListener("input", (event) => {
36
- this[propertyName] = event.target.value;
37
- });
38
-
39
- let bindings = this.#propertyToBindingsMap.get(propertyName);
40
- if (!bindings) {
41
- bindings = [];
42
- this.#propertyToBindingsMap.set(propertyName, bindings);
43
- }
44
- bindings.push(attrName ? { element, attrName } : element);
45
- }
46
-
47
- // This is not private so it can be called from subclasses.
48
- buildDOM() {
49
- let template = this.constructor.prototype.css
50
- ? `<style>${this.css()}</style>`
51
- : "";
52
- template += this.html();
53
- Wrec.#template.innerHTML = template;
54
-
55
- this.shadowRoot.replaceChildren(Wrec.#template.content.cloneNode(true));
56
- }
57
-
58
- connectedCallback() {
59
- this.#defineProperties();
60
- this.buildDOM();
61
-
62
- // Wait for the DOM to update.
63
- requestAnimationFrame(() => {
64
- this.#wireEvents(this.shadowRoot);
65
- this.#makeReactive(this.shadowRoot);
66
- this.constructor.processed = true;
67
- });
68
- }
69
-
70
- #defineProperties() {
71
- const properties = this.constructor.properties;
72
- const { observedAttributes } = this.constructor;
73
- for (const [name, options] of Object.entries(properties)) {
74
- this.#defineProperty(name, options, observedAttributes);
75
- }
76
- }
77
-
78
- #defineProperty(propertyName, options, observedAttributes) {
79
- // Copy the property value to a new property with a leading underscore.
80
- // The property is replaced below with Object.defineProperty.
81
- const value =
82
- observedAttributes.includes(propertyName) &&
83
- this.hasAttribute(propertyName)
84
- ? this.#getTypedAttribute(propertyName)
85
- : options.value;
86
- this["_" + propertyName] = value;
87
-
88
- Object.defineProperty(this, propertyName, {
89
- enumerable: true,
90
- get() {
91
- return this["_" + propertyName];
92
- },
93
- set(value) {
94
- const oldValue = this["_" + propertyName];
95
- if (value === oldValue) return;
96
-
97
- this["_" + propertyName] = value;
98
-
99
- // If the property propertyName is configured to "reflect" and
100
- // there is a matching attribute on the custom element,
101
- // update that attribute.
102
- const options = this.constructor.properties[propertyName];
103
- if (options.reflect && this.hasAttribute(propertyName)) {
104
- const oldValue = this.#getTypedAttribute(propertyName);
105
- if (value !== oldValue) {
106
- this.#updateAttribute(this, propertyName, value);
107
- }
108
- }
109
-
110
- this.#react(propertyName);
111
-
112
- // If this property is bound to a parent web component property,
113
- // update that as well.
114
- const map = this.propertyToParentPropertyMap;
115
- const parentProperty = map ? map.get(propertyName) : null;
116
- if (parentProperty) {
117
- const parent = this.getRootNode().host;
118
- parent.setAttribute(parentProperty, value);
119
- }
120
-
121
- this.#setFormValue(propertyName, value);
122
- },
123
- });
124
- }
125
-
126
- #evaluateAttributes(element) {
127
- const isWC = element.localName.includes("-");
128
-
129
- for (const attrName of element.getAttributeNames()) {
130
- const text = element.getAttribute(attrName);
131
- if (REFERENCE_RE.test(text)) {
132
- // Configure data binding.
133
- const propertyName = text.substring(SKIP);
134
- const propertyValue = this[propertyName];
135
- element.setAttribute(attrName, propertyValue);
136
- element[attrName] = propertyValue;
137
- this.#bind(element, propertyName, attrName);
138
-
139
- // If the element is a web component,
140
- // save a mapping from the attribute name in this web component
141
- // to the property name in the parent web component.
142
- if (isWC) {
143
- let map = element.propertyToParentPropertyMap;
144
- if (!map) {
145
- map = new Map();
146
- element.propertyToParentPropertyMap = map;
147
- }
148
- map.set(attrName, propertyName);
149
- }
150
- }
151
-
152
- this.#registerPlaceholders(text, element, attrName);
153
- }
154
- }
155
-
156
- static #evaluateInContext(expression, context) {
157
- return function () {
158
- // oxlint-disable-next-line no-eval
159
- return eval(expression);
160
- }.call(context);
161
- }
162
-
163
- #evaluateText(element) {
164
- const { localName } = element;
165
-
166
- // Don't allow style elements to be affected by property values.
167
- if (localName === "style") return;
168
-
169
- const text = element.textContent.trim();
170
- if (localName === "textarea" && REFERENCE_RE.test(text)) {
171
- // Configure data binding.
172
- const propertyName = text.substring(SKIP);
173
- element.textContent = this[propertyName];
174
- this.#bind(element, propertyName);
175
- } else {
176
- this.#registerPlaceholders(text, element);
177
- }
178
- }
179
-
180
- #getTypedAttribute(attrName) {
181
- return this.#getTypedValue(attrName, this.getAttribute(attrName));
182
- }
183
-
184
- #getTypedValue(attrName, stringValue) {
185
- const type = this.constructor.properties[attrName].type;
186
- if (type === Number) return Number(stringValue);
187
- if (type === Boolean) return Boolean(stringValue);
188
- return stringValue;
189
- }
190
-
191
- #makeReactive(root) {
192
- const elements = root.querySelectorAll("*");
193
- for (const element of elements) {
194
- this.#evaluateAttributes(element);
195
-
196
- // If the element has no child elements, evaluate its text content.
197
- if (!element.firstElementChild) this.#evaluateText(element);
198
- }
199
- //console.log("#propertyToExpressionsMap =", Wrec.#propertyToExpressionsMap);
200
- //console.log("#expressionReferencesMap =", this.#expressionReferencesMap);
201
- }
202
-
203
- static get observedAttributes() {
204
- return Object.keys(this.properties || {});
205
- }
206
-
207
- #react(propertyName) {
208
- // Update all expression references.
209
- const expressions = Wrec.#propertyToExpressionsMap.get(propertyName) || [];
210
- for (const expression of expressions) {
211
- const value = Wrec.#evaluateInContext(expression, this);
212
- const references = this.#expressionReferencesMap.get(expression) || [];
213
- for (const reference of references) {
214
- if (reference instanceof Element) {
215
- this.#updateElementContent(reference, value);
216
- } else {
217
- const { element, attrName } = reference;
218
- this.#updateAttribute(element, attrName, value);
219
- }
220
- }
221
- }
222
-
223
- this.#updateBindings(propertyName);
224
- }
225
-
226
- static register() {
227
- const elementName = Wrec.#toKebabCase(this.name);
228
- if (!customElements.get(elementName)) {
229
- customElements.define(elementName, this);
230
- }
231
- }
232
-
233
- // Do not place untrusted expressions in
234
- // attribute values or the text content of elements!
235
- #registerPlaceholders(text, element, attrName) {
236
- const matches = text.match(REFERENCE_RE);
237
- if (!matches) return;
238
-
239
- // Only map properties to expressions once for each web component because
240
- // the mapping will be the same for every instance of the web component.
241
- if (!this.constructor.processed) {
242
- matches.forEach((capture) => {
243
- const propertyName = capture.substring(SKIP);
244
- let expressions = Wrec.#propertyToExpressionsMap.get(propertyName);
245
- if (!expressions) {
246
- expressions = [];
247
- Wrec.#propertyToExpressionsMap.set(propertyName, expressions);
248
- }
249
- expressions.push(text);
250
- });
251
- }
252
-
253
- let references = this.#expressionReferencesMap.get(text);
254
- if (!references) {
255
- references = [];
256
- this.#expressionReferencesMap.set(text, references);
257
- }
258
- references.push(attrName ? { element, attrName } : element);
259
-
260
- const value = Wrec.#evaluateInContext(text, this);
261
- if (attrName) {
262
- this.#updateAttribute(element, attrName, value);
263
- } else {
264
- this.#updateElementContent(element, value);
265
- }
266
- }
267
-
268
- static #toKebabCase = (str) =>
269
- str
270
- // Insert a dash before each uppercase letter
271
- // that is preceded by a lowercase letter or digit.
272
- .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
273
- .toLowerCase();
274
-
275
- #setFormValue(propertyName, value) {
276
- if (!this.#formData) return;
277
- this.#formData.set(propertyName, value);
278
- this.#internals.setFormValue(this.#formData);
279
- }
280
-
281
- #updateAttribute(element, attrName, value) {
282
- const currentValue = element.getAttribute(attrName);
283
- if (typeof value === "boolean") {
284
- if (value) {
285
- if (currentValue !== attrName) {
286
- element.setAttribute(attrName, attrName);
287
- }
288
- } else {
289
- element.removeAttribute(attrName);
290
- }
291
- } else if (currentValue !== value) {
292
- element.setAttribute(attrName, value);
293
- }
294
- }
295
-
296
- #updateBindings(propertyName) {
297
- const value = this[propertyName];
298
- const bindings = this.#propertyToBindingsMap.get(propertyName) || [];
299
- for (const binding of bindings) {
300
- if (binding instanceof Element) {
301
- if (binding.localName === "textarea") {
302
- binding.value = value;
303
- } else {
304
- binding.textContent = value;
305
- }
306
- } else {
307
- const { element, attrName } = binding;
308
- this.#updateAttribute(element, attrName, value);
309
- element[attrName] = value;
310
- }
311
- }
312
- }
313
-
314
- #updateElementContent(element, text) {
315
- const { localName } = element;
316
- if (localName === "textarea") {
317
- element.value = text;
318
- } else if (typeof text === "string" && text.trim().startsWith("<")) {
319
- element.innerHTML = text;
320
- this.#wireEvents(element);
321
- this.#makeReactive(element);
322
- } else {
323
- element.textContent = text;
324
- }
325
- }
326
-
327
- #wireEvents(root) {
328
- const elements = root.querySelectorAll("*");
329
- for (const element of elements) {
330
- for (const attr of element.attributes) {
331
- const { name } = attr;
332
- if (name.startsWith("on")) {
333
- const eventName = name.slice(2).toLowerCase();
334
- const { value } = attr;
335
- const fn =
336
- typeof this[value] === "function"
337
- ? (event) => this[value](event)
338
- : // oxlint-disable-next-line no-eval
339
- () => eval(value);
340
- element.addEventListener(eventName, fn);
341
- element.removeAttribute(name);
342
- }
343
- }
344
- }
345
- }
346
- }
347
-
348
- export default Wrec;