wrec 0.0.1
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/.vscode/settings.json +3 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/demo/counter-vanilla.js +91 -0
- package/demo/counter-wreck.js +47 -0
- package/demo/data-bind.js +65 -0
- package/demo/hello-world.js +22 -0
- package/demo/index.html +38 -0
- package/demo/multiply-numbers.js +19 -0
- package/demo/number-input.js +53 -0
- package/demo/number-slider.js +29 -0
- package/demo/package-lock.json +341 -0
- package/demo/package.json +11 -0
- package/demo/radio-group.js +83 -0
- package/demo/temperature-eval.js +16 -0
- package/package.json +22 -0
- package/shipwreck.png +0 -0
- package/wrec.js +348 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mark Volkmann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Web Reactive Component (Wrec)
|
|
2
|
+
|
|
3
|
+
<img alt="shipwrec" src="shipwrec.png" style="width: 256px">
|
|
4
|
+
|
|
5
|
+
Wrec is a small library that greatly simplifies building web components.
|
|
6
|
+
It is inspired by [Lit](https://lit.dev).
|
|
7
|
+
|
|
8
|
+
Wrec has fewer features that Lit.
|
|
9
|
+
In exchange, Wrec:
|
|
10
|
+
|
|
11
|
+
- is much smaller than Lit (about 1/4 of the size)
|
|
12
|
+
- doesn't require any tooling
|
|
13
|
+
- doesn't require a build process
|
|
14
|
+
|
|
15
|
+
The main features of Wrec are that it
|
|
16
|
+
automates wiring event listeners
|
|
17
|
+
and automates implementing reactivity.
|
|
18
|
+
|
|
19
|
+
Check out the web app in the `demo` directory.
|
|
20
|
+
To run it, cd to that directory, enter `npm install`,
|
|
21
|
+
enter `npm run dev`, and browse localhost:5173.
|
|
22
|
+
This app begins by rendering "counter" components.
|
|
23
|
+
The first is implemented as a vanilla web component.
|
|
24
|
+
The next two uses the Wrec library.
|
|
25
|
+
Compare the files `counter-vanilla.js` and `counter-wrec.js`
|
|
26
|
+
to see how much using Wrec simplifies the code.
|
|
27
|
+
|
|
28
|
+
To wire event listeners,
|
|
29
|
+
Wrec looks for attributes whose name begins with "on".
|
|
30
|
+
It assumes the remainder of the attribute name is an event name.
|
|
31
|
+
It also assumes that the value of the attribute is either
|
|
32
|
+
a method name that should be called or code that should be executed
|
|
33
|
+
when that event is dispatched.
|
|
34
|
+
For example, the attribute `onclick="increment"` causes Wrec to
|
|
35
|
+
add an event listener to the element containing the attribute
|
|
36
|
+
for "click" events and calls `this.increment(event)`.
|
|
37
|
+
Alternatively, the attribute `onclick="this.count++"`
|
|
38
|
+
adds an event listener that increments `this.count`
|
|
39
|
+
when the element is clicked.
|
|
40
|
+
|
|
41
|
+
The case of the event name within the attribute name
|
|
42
|
+
does not matter because Wrec lowercases the name.
|
|
43
|
+
So the attribute in the previous example
|
|
44
|
+
can be replaced by `onClick="increment"`.
|
|
45
|
+
|
|
46
|
+
Wrec supports reactivity.
|
|
47
|
+
Attribute values and the text content of elements
|
|
48
|
+
can refer to web component properties with the syntax `this.propertyName`.
|
|
49
|
+
The DOM of the web component is surgically updated.
|
|
50
|
+
Only attribute values and text content
|
|
51
|
+
that refer to modified web component properties are updated.
|
|
52
|
+
Attribute values and text content that contain references to properties
|
|
53
|
+
must be valid JavaScript expressions that are NOT surrounded by `${...}`.
|
|
54
|
+
For an example of this kind of web component, see `demo/hello-world.js`.
|
|
55
|
+
|
|
56
|
+
Wrec supports two-way data binding for HTML form elements.
|
|
57
|
+
|
|
58
|
+
- `input` and `select` elements can have a `value` attribute
|
|
59
|
+
whose value is "this.somePropertyName".
|
|
60
|
+
- `textarea` elements can have text content
|
|
61
|
+
that is "this.somePropertyName"
|
|
62
|
+
|
|
63
|
+
In all these cases, if the user changes the value of the form element,
|
|
64
|
+
the specified property is updated.
|
|
65
|
+
When the property is updated,
|
|
66
|
+
the displayed value of the form element is updated.
|
|
67
|
+
For examples, see `demo/data-bind.js`.
|
|
68
|
+
|
|
69
|
+
Web components that extend `Wrec` can contribute values to
|
|
70
|
+
form submissions by adding the following line to their class definition.
|
|
71
|
+
Wrec looks for that automatically does the rest of the work.
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
static formAssociated = true;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Wrec supports conditional and iterative generation of HTML.
|
|
78
|
+
See `demo/temperature-eval.js` for an example of a web component
|
|
79
|
+
that conditionally decides what to render based on an attribute value.
|
|
80
|
+
See `demo/radio-group.js` for an example of a web component
|
|
81
|
+
that iterates over values in a comma-delimited attribute value
|
|
82
|
+
to determine what to render.
|
|
@@ -0,0 +1,91 @@
|
|
|
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);
|
|
@@ -0,0 +1,47 @@
|
|
|
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();
|
|
@@ -0,0 +1,65 @@
|
|
|
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();
|
|
@@ -0,0 +1,22 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
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>
|
|
@@ -0,0 +1,19 @@
|
|
|
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();
|
|
@@ -0,0 +1,53 @@
|
|
|
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();
|
|
@@ -0,0 +1,29 @@
|
|
|
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();
|
|
@@ -0,0 +1,341 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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();
|
|
@@ -0,0 +1,16 @@
|
|
|
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/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wrec",
|
|
3
|
+
"author": "R. Mark Volkmann",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mvolkmann/wrec.git"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"web",
|
|
12
|
+
"component"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"lint": "oxlint",
|
|
16
|
+
"minify": "terser wrec.js -c -m -o wrec.min.js"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"oxlint": "^1.5.0",
|
|
20
|
+
"terser": "^5.43.1"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/shipwreck.png
ADDED
|
Binary file
|
package/wrec.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
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;
|