I am trying to make a button that will be disabled for 1 minute when you click on it. Only when the button is enabled and you click on it, 25 points should be added to a div (div starts at 0). After each click, the button will be disabled and the timer will start running.
Here are some pictures to make the whole thing a bit more understandable:
One could create a Custom Element.
This requires knowledge in the following topics:
setInterval()
and clearInterval()
Defining a custom element requires a JS class that extends HTMLElement (or one of its sub-classes).
This class is then registered as an HTML-element by calling customElements.define()
with its HTML-element name (must include -
(hyphen) in its name, e.g. "example-element") and the class it is associated with.
I decided to call the class CounterElement
, and the HTML-element <counter-instance>
:
class CounterElement extends HTMLElement {
constructor() {
super(); // Required since we are extending a class
}
}
customElements.define('counter-instance', CounterElement);
Setting up the Shadow Root is done like this:
Element.attachShadow()
Since some comment mentioned that accessing the elements would still be easy, I decided to use {mode: 'closed'}
to keep unknowing users from accessing them. It is, however, not inaccessible. That would require a MutationObserver, but that is not included in this answer to keep it (somewhat) short, heh.
When using {mode: 'closed'}
, one needs to keep a reference to the shadow root manually. I decided to use a private member-variable #shadow
.
Now, the shadow root's elements need to be created and appended. Again, I kept a private reference for the "important" elements: #samp
and #button
.
I also added a little bit of styling.
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);
Now that all the important elements exist, we need to set them up correctly. By that, I mean add the texts they should have.
Since we will keep track of a counter and seconds, declaring them as member-variables makes sense.
To format the seconds to a mm:ss
-format, I wrote a utility function formatTime()
. That seems quite handy, so it can be in the global scope.
Note that you should try not to clutter the global scope with too many variables. You can use IIFEs to keep them local.
Now, adding the text is as easy as using HTMLElement.innerText
.
In a comment you requested to have the button say "Collect" when the seconds have reached 0. This can be done using an if-else statement, but for brevity I used the 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);
The last step now is to add the timer, and the onclick
-listener.
Since we want to start a timer when clicking on #button
, and stop it after seconds
reaches 0
, we can use setInterval()
and clearInterval()
respectively.
The former function returns the interval's ID, the latter clears it using its ID. To use these functions as intended, we need to keep a reference to the interval's ID.
When #button
is clicked, it should:
seconds
and increase counter
Here is our (shortened) code implementing it:
// 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);
As mentioned before, accessing the elements of the shadow root is still possible using the Developer Tools. Fully disabling the ability of changing the button's disabled
-property can be achieved using a MutationObserver, keeping the property from being changed, depending on what number seconds
currently has.
Here is the full code as a Stack-Snippet, so you can try it out yourselves:
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>