可以创建Custom Element。
这需要了解以下主题:
基本结构
定义一个自定义元素需要一个扩展HTMLElement(或其子类之一)的JS类。
然后通过使用其 HTML 元素名称调用 customElements.define() 将此类注册为 HTML 元素(必须在其名称中包含 -(连字符)在中,例如“example-element” ) 及其关联的类。
我决定将类称为CounterElement,并将HTML元素称为<counter-instance>:
class CounterElement extends HTMLElement {
constructor() {
super(); // Required since we are extending a class
}
}
customElements.define('counter-instance', CounterElement);
设置影子根
Shadow Root 的设置如下:
- 通过调用
Element.attachShadow() 创建影子根
- 创建并附加其 HTML 元素
- 为影子根添加样式
由于一些评论提到访问元素仍然很容易,我决定使用{mode: 'closed'} 来阻止不知情的用户访问它们。但是,它并非无法访问。这将需要一个MutationObserver,但这不包含在这个答案中以保持它(有点)简短,heh。
使用{mode: 'closed'} 时,需要手动保留对影子根的引用。我决定使用私有成员变量#shadow。
现在,需要创建和附加影子根的元素。同样,我为“重要”元素保留了一个私人参考:#samp 和 #button。
我还添加了一些样式。
class CounterElement extends HTMLElement {
#shadow;
#samp;
#button;
constructor() {
super();
// Shadow Root
this.#shadow = this.attachShadow({mode: 'closed'});
let div = document.createElement('div');
let style = document.createElement('style');
style.innerHTML = 'div{padding:0.2rem;border:1px solid black}'
+ 'samp{display:block}button{font-family:monospace}';
this.#samp = document.createElement('samp');
this.#button = document.createElement('button');
div.append(this.#samp, this.#button);
this.#shadow.append(style, div);
}
}
customElements.define('counter-instance', CounterElement);
设置元素
现在所有重要元素都已存在,我们需要正确设置它们。我的意思是添加他们应该有的文本。
由于我们将跟踪计数器和秒,因此将它们声明为成员变量是有意义的。
为了将秒数格式化为mm:ss 格式,我编写了一个实用函数formatTime()。这看起来很方便,所以它可以在全局范围内。
注意,您应该尽量不要让太多变量使全局范围混乱。您可以使用IIFEs 将它们保留在本地。
现在,添加文本就像使用 HTMLElement.innerText 一样简单。
在评论中,您要求在秒数达到 0 时让按钮说“收集”。这可以使用 if-else 语句来完成,但为简洁起见,我使用了 Ternary Operator。
function formatTime(seconds) {
var min = new String(Math.floor(seconds / 60));
var sec = new String(seconds % 60);
while (min.length < 2) min = '0' + min;
while (sec.length < 2) sec = '0' + sec;
return min + ':' + sec;
};
class CounterElement extends HTMLElement {
counter = 0;
seconds = 0;
#shadow;
#samp;
#button;
constructor() {
super();
// Shadow Root
// ...
this.updateText();
}
updateText() {
this.#samp.innerText = this.counter;
this.#button.innerText = this.seconds ? formatTime(this.seconds) : 'Collect';
}
}
customElements.define('counter-instance', CounterElement);
添加(计时器)功能
现在的最后一步是添加计时器和onclick-listener。
由于我们想在点击#button时启动计时器,并在seconds到达0后停止,我们可以分别使用setInterval()和clearInterval()。
前一个函数返回间隔的 ID,后者使用它的 ID 清除它。要按预期使用这些函数,我们需要保留对区间 ID 的引用。
当#button被点击时,它应该:
- 重置
seconds并增加counter
- 启动我们的计时器
- 在我们的计时器期间被禁用
- 根据现在重置的值更新文本
这是我们实现它的(缩短的)代码:
// function formatTime(seconds) () { ... }
class CounterElement extends HTMLElement {
counter = 0;
seconds = 0;
#shadow;
#samp;
#button;
constructor() {
super();
// Shadow Root
// ...
let intervalId;
let intervalStep = () => { // Will be called every second
this.updateText();
if (--this.seconds <= 0) {
clearInterval(this.#intervalId);
this.#button.disabled = false;
}
};
this.#button.addEventListener('click', () => {
this.seconds = 60;
this.counter += 25;
this.#intervalId = setInterval(intervalStep, 1000);
this.#button.disabled = true;
this.updateText();
});
this.updateText();
}
// Member-functions ...
}
customElements.define('counter-instance', CounterElement);
结论
如前所述,仍然可以使用开发工具访问影子根的元素。可以使用 MutationObserver 完全禁用更改按钮的 disabled-property 的功能,从而防止属性被更改,具体取决于 seconds 当前拥有的数字。
这里是 Stack-Snippet 的完整代码,您可以自己尝试一下:
function formatTime(seconds) {
var min = new String(Math.floor(seconds / 60));
var sec = new String(seconds % 60);
while (min.length < 2) min = '0' + min;
while (sec.length < 2) sec = '0' + sec;
return min + ':' + sec;
};
class CounterElement extends HTMLElement {
counter = 0;
seconds = 0;
#shadow;
#samp;
#button;
constructor() {
super();
// Shadow Root
this.#shadow = this.attachShadow({mode: 'closed'});
let div = document.createElement('div');
let style = document.createElement('style');
style.innerHTML = 'div{padding:0.2rem;border:1px solid black}'
+ 'samp{display:block}button{font-family:monospace}';
this.#samp = document.createElement('samp');
this.#button = document.createElement('button');
div.append(this.#samp, this.#button);
this.#shadow.append(style, div);
let intervalId;
let intervalStep = () => { // Will be called every second
if (--this.seconds <= 0) {
clearInterval(intervalId);
this.#button.disabled = false;
}
this.updateText();
};
this.#button.addEventListener('click', () => {
this.seconds = 60;
this.counter += 25;
intervalId = setInterval(intervalStep, 1000);
this.#button.disabled = true;
this.updateText();
});
this.updateText();
}
updateText() {
this.#samp.innerText = this.counter;
this.#button.innerText = this.seconds ? formatTime(this.seconds) : 'Collect';
}
}
customElements.define('counter-instance', CounterElement);
/* Ignore; only for styling purposes */
body {
display: flex;
gap: 0.6rem;
}
<!-- Works with multiple elements -->
<counter-instance></counter-instance>
<counter-instance></counter-instance>