@w-lfpup/wctk 0.1.0
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/.github/workflows/build_and_test.yml +18 -0
- package/.prettierignore +5 -0
- package/.prettierrc +5 -0
- package/LICENSE +28 -0
- package/README.md +49 -0
- package/dist/bind.d.ts +7 -0
- package/dist/bind.js +14 -0
- package/dist/events.d.ts +21 -0
- package/dist/events.js +39 -0
- package/dist/microtask.d.ts +12 -0
- package/dist/microtask.js +23 -0
- package/dist/mod.d.ts +6 -0
- package/dist/mod.js +6 -0
- package/dist/query_selector.d.ts +15 -0
- package/dist/query_selector.js +25 -0
- package/dist/subscription.d.ts +19 -0
- package/dist/subscription.js +26 -0
- package/dist/wc.d.ts +33 -0
- package/dist/wc.js +46 -0
- package/docs/bind.md +26 -0
- package/docs/events.md +99 -0
- package/docs/microtask.md +35 -0
- package/docs/query_selector.md +39 -0
- package/docs/subscription.md +85 -0
- package/docs/wc.md +57 -0
- package/examples/counter/index.html +30 -0
- package/examples/counter/mod.js +39 -0
- package/examples/counter/mod.ts +51 -0
- package/examples/form_associated/index.html +31 -0
- package/examples/form_associated/mod.js +13 -0
- package/examples/form_associated/mod.ts +21 -0
- package/examples/form_associated/text_input.js +25 -0
- package/examples/form_associated/text_input.ts +28 -0
- package/examples/stopwatch/index.html +29 -0
- package/examples/stopwatch/mod.js +8 -0
- package/examples/stopwatch/mod.ts +11 -0
- package/examples/stopwatch/stopwatch.js +45 -0
- package/examples/stopwatch/stopwatch.ts +61 -0
- package/examples/tsconfig.json +10 -0
- package/package.json +22 -0
- package/src/bind.ts +22 -0
- package/src/events.ts +67 -0
- package/src/microtask.ts +37 -0
- package/src/mod.ts +6 -0
- package/src/query_selector.ts +46 -0
- package/src/subscription.ts +47 -0
- package/src/tsconfig.json +7 -0
- package/src/wc.ts +87 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Subscribe Controller
|
|
2
|
+
|
|
3
|
+
Subscribe web components to external state.
|
|
4
|
+
|
|
5
|
+
## How to use
|
|
6
|
+
|
|
7
|
+
### Callbacks
|
|
8
|
+
|
|
9
|
+
Create functions that subscribe and unsubscribe an element from a data store.
|
|
10
|
+
|
|
11
|
+
The result of the subscribe function is passed as the to the unsubscribe function.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Datastore } from "./some-datastore.js";
|
|
15
|
+
|
|
16
|
+
let store = new Datastore();
|
|
17
|
+
|
|
18
|
+
function subscribe(callback): number {
|
|
19
|
+
return store.subscribe(callback);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function unsubscribe(results: number): void {
|
|
23
|
+
store.unsubscribe(results);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { store, subscribe, unsubscribe };
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Controller
|
|
30
|
+
|
|
31
|
+
Add a `Subscription` controller to a web component. Pass subscribe, unsubscribe, and a callback function on instantiation.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { Subscription } from "wctk";
|
|
35
|
+
import { getState, subscribe, unsubscribe } from "./datastore.js";
|
|
36
|
+
|
|
37
|
+
class MyElement extends HTMLElement {
|
|
38
|
+
#sc = new Subscription({
|
|
39
|
+
host: this,
|
|
40
|
+
callback: this.#update,
|
|
41
|
+
subscribe,
|
|
42
|
+
unsubscribe,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
#update() {
|
|
46
|
+
let state = getState();
|
|
47
|
+
// do something with state
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// lifecycle method
|
|
51
|
+
connectedCallback() {
|
|
52
|
+
this.#sc.connect();
|
|
53
|
+
this.#update();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// lifecycle method
|
|
57
|
+
disconnectedCallback() {
|
|
58
|
+
this.#sc.disconnect();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Shortcut life cycle methods
|
|
64
|
+
|
|
65
|
+
In the example below, the `connected` property is set to true and the component is immediately subscribed on instantiation.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { Subscription } from "wctk";
|
|
69
|
+
import { getState, subscribe, unsubscribe } from "./datastore.js";
|
|
70
|
+
|
|
71
|
+
class MyElement extends HTMLElement {
|
|
72
|
+
#sc = new Subscription({
|
|
73
|
+
host: this,
|
|
74
|
+
callback: this.#update,
|
|
75
|
+
connected: true,
|
|
76
|
+
subscribe,
|
|
77
|
+
unsubscribe,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
#update() {
|
|
81
|
+
let state = getState();
|
|
82
|
+
// do something with state
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
package/docs/wc.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Wc Controller
|
|
2
|
+
|
|
3
|
+
Build a web component.
|
|
4
|
+
|
|
5
|
+
## How to use
|
|
6
|
+
|
|
7
|
+
Add a `Wc` controller to a custom element.
|
|
8
|
+
|
|
9
|
+
One line is all it takes.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Wc } from "wctk";
|
|
13
|
+
|
|
14
|
+
class MyElement extends HTMLElement {
|
|
15
|
+
#wc = new Wc({ host: this });
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Adopted stylesheets and form values
|
|
20
|
+
|
|
21
|
+
The `Wc` controller is also a facade for core web componet APIs like adopted stylesheets and form values.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
class MyElement extends HTMLElement {
|
|
25
|
+
#wc = new Wc({
|
|
26
|
+
host: this,
|
|
27
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options
|
|
28
|
+
shadowRootInit: { mode: "open" },
|
|
29
|
+
adoptedStyleSheets: [],
|
|
30
|
+
formValue: "^_^",
|
|
31
|
+
formState: ":3",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
showcaseApi() {
|
|
35
|
+
// true if declarative shadow dom is present
|
|
36
|
+
this.#wc.delcarative;
|
|
37
|
+
|
|
38
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot
|
|
39
|
+
this.#wc.shadowRoot;
|
|
40
|
+
|
|
41
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/adoptedStyleSheets
|
|
42
|
+
this.#wc.adopedStylesheets;
|
|
43
|
+
|
|
44
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setFormValue
|
|
45
|
+
this.#wc.setFormValue(value, state);
|
|
46
|
+
|
|
47
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/checkValidity
|
|
48
|
+
this.#wc.checkValidity();
|
|
49
|
+
|
|
50
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/reportValidity
|
|
51
|
+
this.#wc.reportValidity();
|
|
52
|
+
|
|
53
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setValidity
|
|
54
|
+
this.#wc.setValidity(flags, message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-us">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset=utf-8>
|
|
5
|
+
<meta name=viewport content="width=device-width, initial-scale=1">
|
|
6
|
+
<script type="importmap">
|
|
7
|
+
{
|
|
8
|
+
"imports": {
|
|
9
|
+
"wctk": "../../dist/mod.js"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
<script src="./mod.js" type=module></script>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<main>
|
|
17
|
+
<!-- web component -->
|
|
18
|
+
<counter-wc>
|
|
19
|
+
<template shadowrootmode="closed">
|
|
20
|
+
<button decrease>-</button>
|
|
21
|
+
<slot></slot>
|
|
22
|
+
<button increase>+</button>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<!-- DOM content -->
|
|
26
|
+
<span>42</span>
|
|
27
|
+
</counter-wc>
|
|
28
|
+
</main>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Wc, Events } from "wctk";
|
|
2
|
+
/*
|
|
3
|
+
Custom Element with state and interactivity.
|
|
4
|
+
*/
|
|
5
|
+
class Counter extends HTMLElement {
|
|
6
|
+
#wc = new Wc({ host: this });
|
|
7
|
+
#ev = new Events({
|
|
8
|
+
host: this,
|
|
9
|
+
target: this.#wc.shadowRoot,
|
|
10
|
+
connected: true,
|
|
11
|
+
callbacks: [["click", this.#clickHandler]],
|
|
12
|
+
});
|
|
13
|
+
#state = getStateFromDOM(this.#wc.shadowRoot);
|
|
14
|
+
#clickHandler(e) {
|
|
15
|
+
if (!this.#state)
|
|
16
|
+
return;
|
|
17
|
+
let increment = getIncrement(e);
|
|
18
|
+
if (increment) {
|
|
19
|
+
this.#state.count += increment;
|
|
20
|
+
this.#state.el.textContent = this.#state.count.toString();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function getStateFromDOM(shadowRoot) {
|
|
25
|
+
let slot = shadowRoot.querySelector("slot");
|
|
26
|
+
if (slot)
|
|
27
|
+
for (let el of slot.assignedNodes()) {
|
|
28
|
+
if (el instanceof HTMLSpanElement) {
|
|
29
|
+
return { el, count: parseInt(el.textContent ?? "0") };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function getIncrement(e) {
|
|
34
|
+
let { target } = e;
|
|
35
|
+
if (target instanceof HTMLButtonElement) {
|
|
36
|
+
return target.hasAttribute("increase") ? 1 : -1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
customElements.define("counter-wc", Counter);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Wc, Events } from "wctk";
|
|
2
|
+
|
|
3
|
+
interface State {
|
|
4
|
+
el: HTMLSpanElement;
|
|
5
|
+
count: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
Custom Element with state and interactivity.
|
|
10
|
+
*/
|
|
11
|
+
class Counter extends HTMLElement {
|
|
12
|
+
#wc = new Wc({ host: this });
|
|
13
|
+
|
|
14
|
+
#ev = new Events({
|
|
15
|
+
host: this,
|
|
16
|
+
target: this.#wc.shadowRoot,
|
|
17
|
+
connected: true,
|
|
18
|
+
callbacks: [["click", this.#clickHandler]],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
#state?: State = getStateFromDOM(this.#wc.shadowRoot);
|
|
22
|
+
|
|
23
|
+
#clickHandler(e: Event) {
|
|
24
|
+
if (!this.#state) return;
|
|
25
|
+
|
|
26
|
+
let increment = getIncrement(e);
|
|
27
|
+
if (increment) {
|
|
28
|
+
this.#state.count += increment;
|
|
29
|
+
this.#state.el.textContent = this.#state.count.toString();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getStateFromDOM(shadowRoot: ShadowRoot) {
|
|
35
|
+
let slot = shadowRoot.querySelector("slot");
|
|
36
|
+
if (slot)
|
|
37
|
+
for (let el of slot.assignedNodes()) {
|
|
38
|
+
if (el instanceof HTMLSpanElement) {
|
|
39
|
+
return { el, count: parseInt(el.textContent ?? "0") };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getIncrement(e: Event) {
|
|
45
|
+
let { target } = e;
|
|
46
|
+
if (target instanceof HTMLButtonElement) {
|
|
47
|
+
return target.hasAttribute("increase") ? 1 : -1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
customElements.define("counter-wc", Counter);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-us">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset=utf-8>
|
|
5
|
+
<meta name=viewport content="width=device-width, initial-scale=1">
|
|
6
|
+
<script type="importmap">
|
|
7
|
+
{
|
|
8
|
+
"imports": {
|
|
9
|
+
"wctk": "../../dist/mod.js"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
<script src="./mod.js" type=module></script>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<main>
|
|
17
|
+
<form>
|
|
18
|
+
<!-- html component input -->
|
|
19
|
+
<input name="html_element">
|
|
20
|
+
<!-- custom element input -->
|
|
21
|
+
<text-input name="web_component">
|
|
22
|
+
<template shadowrootmode="closed">
|
|
23
|
+
<input>
|
|
24
|
+
</template>
|
|
25
|
+
</text-input>
|
|
26
|
+
<button type="submit">submit !</button>
|
|
27
|
+
</form>
|
|
28
|
+
<pre results></pre>
|
|
29
|
+
</main>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TextInput } from "./text_input.js";
|
|
2
|
+
customElements.define("text-input", TextInput);
|
|
3
|
+
const results = document.querySelector("[results]");
|
|
4
|
+
document.addEventListener("submit", function (e) {
|
|
5
|
+
if (!(e.target instanceof HTMLFormElement))
|
|
6
|
+
return;
|
|
7
|
+
e.preventDefault();
|
|
8
|
+
let formdata = new FormData(e.target);
|
|
9
|
+
if (results)
|
|
10
|
+
results.textContent = JSON.stringify(
|
|
11
|
+
// @ts-expect-error
|
|
12
|
+
Object.fromEntries(formdata), undefined, " ");
|
|
13
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { TextInput } from "./text_input.js";
|
|
2
|
+
|
|
3
|
+
customElements.define("text-input", TextInput);
|
|
4
|
+
|
|
5
|
+
const results = document.querySelector("[results]");
|
|
6
|
+
|
|
7
|
+
document.addEventListener("submit", function (e: SubmitEvent) {
|
|
8
|
+
if (!(e.target instanceof HTMLFormElement)) return;
|
|
9
|
+
|
|
10
|
+
e.preventDefault();
|
|
11
|
+
|
|
12
|
+
let formdata: FormData = new FormData(e.target);
|
|
13
|
+
|
|
14
|
+
if (results)
|
|
15
|
+
results.textContent = JSON.stringify(
|
|
16
|
+
// @ts-expect-error
|
|
17
|
+
Object.fromEntries(formdata),
|
|
18
|
+
undefined,
|
|
19
|
+
" ",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Wc, Events } from "wctk";
|
|
2
|
+
/*
|
|
3
|
+
Form associated custom element.
|
|
4
|
+
*/
|
|
5
|
+
export class TextInput extends HTMLElement {
|
|
6
|
+
static formAssociated = true;
|
|
7
|
+
#wc = new Wc({ host: this });
|
|
8
|
+
#ev = new Events({
|
|
9
|
+
host: this,
|
|
10
|
+
target: this.#wc.shadowRoot,
|
|
11
|
+
connected: true,
|
|
12
|
+
callbacks: [["change", this.#changeHandler]],
|
|
13
|
+
});
|
|
14
|
+
#changeHandler(event) {
|
|
15
|
+
let { target } = event;
|
|
16
|
+
if (target instanceof HTMLInputElement)
|
|
17
|
+
this.#wc.setFormValue(target.value);
|
|
18
|
+
}
|
|
19
|
+
// lifecycle method
|
|
20
|
+
formStateRestoreCallback(state) {
|
|
21
|
+
let input = this.#wc.shadowRoot.querySelector("input");
|
|
22
|
+
if (input)
|
|
23
|
+
input.value = state;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Wc, Events } from "wctk";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Form associated custom element.
|
|
5
|
+
*/
|
|
6
|
+
export class TextInput extends HTMLElement {
|
|
7
|
+
static formAssociated = true;
|
|
8
|
+
|
|
9
|
+
#wc = new Wc({ host: this });
|
|
10
|
+
#ev = new Events({
|
|
11
|
+
host: this,
|
|
12
|
+
target: this.#wc.shadowRoot,
|
|
13
|
+
connected: true,
|
|
14
|
+
callbacks: [["change", this.#changeHandler]],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
#changeHandler(event: Event) {
|
|
18
|
+
let { target } = event;
|
|
19
|
+
if (target instanceof HTMLInputElement)
|
|
20
|
+
this.#wc.setFormValue(target.value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// lifecycle method
|
|
24
|
+
formStateRestoreCallback(state: string) {
|
|
25
|
+
let input = this.#wc.shadowRoot.querySelector("input");
|
|
26
|
+
if (input) input.value = state;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-us">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset=utf-8>
|
|
5
|
+
<meta name=viewport content="width=device-width, initial-scale=1">
|
|
6
|
+
<script type="importmap">
|
|
7
|
+
{
|
|
8
|
+
"imports": {
|
|
9
|
+
"wctk": "../../dist/mod.js"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
<script src="./mod.js" type=module></script>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<main>
|
|
17
|
+
<button start>|></button>
|
|
18
|
+
<button>| |</button>
|
|
19
|
+
|
|
20
|
+
<!-- web component -->
|
|
21
|
+
<stopwatch-wc>
|
|
22
|
+
<template shadowrootmode="closed">
|
|
23
|
+
<!-- Shadow DOM content -->
|
|
24
|
+
<span>117.00</span>
|
|
25
|
+
</template>
|
|
26
|
+
</stopwatch-wc>
|
|
27
|
+
</main>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Stopwatch } from "./stopwatch.js";
|
|
2
|
+
customElements.define("stopwatch-wc", Stopwatch);
|
|
3
|
+
const stopwatch = document.querySelector("stopwatch-wc");
|
|
4
|
+
document.addEventListener("click", function (e) {
|
|
5
|
+
if (stopwatch && e.target instanceof HTMLButtonElement) {
|
|
6
|
+
e.target.hasAttribute("start") ? stopwatch.start() : stopwatch.pause();
|
|
7
|
+
}
|
|
8
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Stopwatch } from "./stopwatch.js";
|
|
2
|
+
|
|
3
|
+
customElements.define("stopwatch-wc", Stopwatch);
|
|
4
|
+
|
|
5
|
+
const stopwatch = document.querySelector<Stopwatch>("stopwatch-wc");
|
|
6
|
+
|
|
7
|
+
document.addEventListener("click", function (e: Event) {
|
|
8
|
+
if (stopwatch && e.target instanceof HTMLButtonElement) {
|
|
9
|
+
e.target.hasAttribute("start") ? stopwatch.start() : stopwatch.pause();
|
|
10
|
+
}
|
|
11
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Wc, Microtask } from "wctk";
|
|
2
|
+
/*
|
|
3
|
+
Custom Element with performant and "asynchronous" renders
|
|
4
|
+
on the microtask queue.
|
|
5
|
+
*/
|
|
6
|
+
export class Stopwatch extends HTMLElement {
|
|
7
|
+
#wc = new Wc({ host: this });
|
|
8
|
+
#rc = new Microtask({ host: this, callback: this.#render });
|
|
9
|
+
#boundUpdate = this.#update.bind(this);
|
|
10
|
+
#state = getStateFromShadowDOM(this.#wc.shadowRoot);
|
|
11
|
+
#render() {
|
|
12
|
+
if (this.#state)
|
|
13
|
+
this.#state.el.textContent = this.#state.count.toFixed(2);
|
|
14
|
+
}
|
|
15
|
+
#update(timestamp) {
|
|
16
|
+
if (!this.#state)
|
|
17
|
+
return;
|
|
18
|
+
this.#state.receipt = requestAnimationFrame(this.#boundUpdate);
|
|
19
|
+
this.#state.count += (timestamp - this.#state.prevTimestamp) * 0.001;
|
|
20
|
+
this.#state.prevTimestamp = timestamp;
|
|
21
|
+
// push render to microtask queue
|
|
22
|
+
this.#rc.queue();
|
|
23
|
+
}
|
|
24
|
+
start() {
|
|
25
|
+
if (!this.#state || this.#state?.receipt)
|
|
26
|
+
return;
|
|
27
|
+
this.#state.receipt = requestAnimationFrame(this.#boundUpdate);
|
|
28
|
+
this.#state.prevTimestamp = performance.now();
|
|
29
|
+
}
|
|
30
|
+
pause() {
|
|
31
|
+
if (this.#state && this.#state.receipt)
|
|
32
|
+
this.#state.receipt = cancelAnimationFrame(this.#state.receipt);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function getStateFromShadowDOM(shadowRoot) {
|
|
36
|
+
let el = shadowRoot.querySelector("span");
|
|
37
|
+
if (el instanceof HTMLSpanElement) {
|
|
38
|
+
return {
|
|
39
|
+
el,
|
|
40
|
+
count: parseInt(el.textContent ?? "0"),
|
|
41
|
+
receipt: 0,
|
|
42
|
+
prevTimestamp: performance.now(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Bind, Wc, Microtask } from "wctk";
|
|
2
|
+
|
|
3
|
+
interface State {
|
|
4
|
+
receipt: number | void;
|
|
5
|
+
count: number;
|
|
6
|
+
prevTimestamp: DOMHighResTimeStamp;
|
|
7
|
+
el: HTMLSpanElement;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
Custom Element with performant and "asynchronous" renders
|
|
12
|
+
on the microtask queue.
|
|
13
|
+
*/
|
|
14
|
+
export class Stopwatch extends HTMLElement {
|
|
15
|
+
#wc = new Wc({ host: this });
|
|
16
|
+
#rc = new Microtask({ host: this, callback: this.#render });
|
|
17
|
+
|
|
18
|
+
#boundUpdate = this.#update.bind(this);
|
|
19
|
+
#state?: State = getStateFromShadowDOM(this.#wc.shadowRoot);
|
|
20
|
+
|
|
21
|
+
#render() {
|
|
22
|
+
if (this.#state)
|
|
23
|
+
this.#state.el.textContent = this.#state.count.toFixed(2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#update(timestamp: DOMHighResTimeStamp) {
|
|
27
|
+
if (!this.#state) return;
|
|
28
|
+
|
|
29
|
+
this.#state.receipt = requestAnimationFrame(this.#boundUpdate);
|
|
30
|
+
|
|
31
|
+
this.#state.count += (timestamp - this.#state.prevTimestamp) * 0.001;
|
|
32
|
+
this.#state.prevTimestamp = timestamp;
|
|
33
|
+
|
|
34
|
+
// push render to microtask queue
|
|
35
|
+
this.#rc.queue();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
start() {
|
|
39
|
+
if (!this.#state || this.#state?.receipt) return;
|
|
40
|
+
|
|
41
|
+
this.#state.receipt = requestAnimationFrame(this.#boundUpdate);
|
|
42
|
+
this.#state.prevTimestamp = performance.now();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pause() {
|
|
46
|
+
if (this.#state && this.#state.receipt)
|
|
47
|
+
this.#state.receipt = cancelAnimationFrame(this.#state.receipt);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getStateFromShadowDOM(shadowRoot: ShadowRoot): State | undefined {
|
|
52
|
+
let el = shadowRoot.querySelector("span");
|
|
53
|
+
if (el instanceof HTMLSpanElement) {
|
|
54
|
+
return {
|
|
55
|
+
el,
|
|
56
|
+
count: parseInt(el.textContent ?? "0"),
|
|
57
|
+
receipt: 0,
|
|
58
|
+
prevTimestamp: performance.now(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@w-lfpup/wctk",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"main": "dist/mod.js",
|
|
5
|
+
"description": "A bare-metal web component toolkit",
|
|
6
|
+
"license": "BSD-3-Clause",
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"prepare": "npm run build && npm run build:examples",
|
|
10
|
+
"build": "npx tsc --project ./src",
|
|
11
|
+
"build:examples": "npx tsc --project ./examples",
|
|
12
|
+
"format": "prettier --write ./"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"prettier": "^3.3.0",
|
|
16
|
+
"typescript": "^5.4.5"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/w-lfpup/wctk-js.git"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/bind.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface BindParamsInterface {
|
|
2
|
+
host: Object;
|
|
3
|
+
callbacks: Function[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class Bind {
|
|
7
|
+
constructor(params: BindParamsInterface) {
|
|
8
|
+
let { host, callbacks } = params;
|
|
9
|
+
|
|
10
|
+
for (let callback of callbacks) {
|
|
11
|
+
// do not bind and replace already bound functions
|
|
12
|
+
if (
|
|
13
|
+
callback instanceof Function &&
|
|
14
|
+
!callback.hasOwnProperty("prototype")
|
|
15
|
+
) {
|
|
16
|
+
let { name } = callback;
|
|
17
|
+
if (!name.startsWith("#"))
|
|
18
|
+
host[name as keyof typeof host] = callback.bind(host);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type Callbacks = Array<[string, EventListenerOrEventListenerObject]>;
|
|
2
|
+
|
|
3
|
+
export interface EventsInterface {
|
|
4
|
+
connect(): void;
|
|
5
|
+
disconnect(): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface EventElementInterface {
|
|
9
|
+
addEventListener: EventTarget["addEventListener"];
|
|
10
|
+
removeEventListener: EventTarget["removeEventListener"];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface EventParamsInterface {
|
|
14
|
+
host: EventElementInterface;
|
|
15
|
+
connected?: boolean;
|
|
16
|
+
target?: EventElementInterface;
|
|
17
|
+
callbacks: Callbacks;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class Events implements EventsInterface {
|
|
21
|
+
#connected: boolean = false;
|
|
22
|
+
#callbacks: Callbacks = [];
|
|
23
|
+
#target: EventElementInterface;
|
|
24
|
+
|
|
25
|
+
constructor(params: EventParamsInterface) {
|
|
26
|
+
const { host, target, callbacks, connected } = params;
|
|
27
|
+
|
|
28
|
+
this.#target = target ?? host;
|
|
29
|
+
this.#callbacks = getBoundCallbacks(host, callbacks);
|
|
30
|
+
|
|
31
|
+
if (connected) this.connect();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
connect() {
|
|
35
|
+
if (this.#connected) return;
|
|
36
|
+
this.#connected = true;
|
|
37
|
+
|
|
38
|
+
for (let [name, callback] of this.#callbacks) {
|
|
39
|
+
this.#target.addEventListener(name, callback);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
disconnect() {
|
|
44
|
+
if (!this.#connected) return;
|
|
45
|
+
this.#connected = false;
|
|
46
|
+
|
|
47
|
+
for (let [name, callback] of this.#callbacks) {
|
|
48
|
+
this.#target.removeEventListener(name, callback);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getBoundCallbacks(host: Object, callbacks: Callbacks): Callbacks {
|
|
54
|
+
let boundCallbacks: Callbacks = [];
|
|
55
|
+
for (let [name, callback] of callbacks) {
|
|
56
|
+
if (
|
|
57
|
+
callback instanceof Function &&
|
|
58
|
+
!callback.hasOwnProperty("prototype")
|
|
59
|
+
) {
|
|
60
|
+
callback = callback.bind(host);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
boundCallbacks.push([name, callback]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return boundCallbacks;
|
|
67
|
+
}
|