Search code examples
javascripthtmltimersettimeoutdisable

Disable Button for one minute when it's clicked


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:

Timer starts at 1:00 and is disabled

enter image description here

enter image description here

enter image description here


Solution

  • One could create a Custom Element.
    This requires knowledge in the following topics:

    Basic structure

    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

    Setting up the Shadow Root is done like this:

    1. Create the shadow root by calling Element.attachShadow()
    2. Create and append its HTML-elements
    3. Add styling to the shadow root

    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);
    

    Setting up the elements

    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);
    

    Adding (the timer) functionality

    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:

    • Reset seconds and increase counter
    • Start our timer
    • Be disabled for the duration of our timer
    • Update texts respecting the now-resetted values

    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);
    

    Conclusion

    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>