Vue 2 – A Vue Button Component with Confirm — Part 2

Here I am going to show you how we to enhance the button component we wrote in Part 1 of this article here: http://www.scalingvue.com/2020/05/23/a-vue-button-component-with-confirm-part-1/

This time we are going to add a bit more of code and a few properties so we can display a countdown along with the label and a progress bar overlay like in the following animation:

Let’s add a new <div> element for the progress overlay to the component template. This will have class countdown-bar and its style will be bound to a computed property called countdownBarStyle:

<button type="button" :class="computedCss" @click="onClick">
    <div class="countdown-bar" :style="countdownBarStyle"></div>
    <span class="btn-label">{{ computedLabel }}</span>
</button>

To recap, we’ll now have 3 computed properties:

  1. computedCss: for the button css class attribute
  2. computedLabel: for the button text label
  3. countdownBarStyle: for the progress bar style (note: we have to use the style instead of the css binding because we’ll calculate the transition-duration based on the property timeoutSecs set when consuming the component. This gives us the ability to use different timeouts values in different parts of the applications if we need to.

Our button component will still expose the same properties as in Part 1 of this article. Here thry are again for easier reference:

  1. defaultLabel: this will be the default text for the button label, before it is clicked (i.e. “Delete”)
  2. confirmLabel: this will be the text for the button label after it is clicked once (i.e. “Are you sure?”)
  3. defaultCss: the default css class name for the button (i.e. “btn-primary”)
  4. timeoutSecs: the timeout in seconds within which the user has to click a second time to confirm the operation

As for the private/internal properties, we are going to need a couple more. Here is the complete list including those from Part 1:

  1. clickedOnce: a boolean to track if the user has already clicked the button one time
  2. timeoutId: this is to hold the reference to the setTimeout we’ll use to track if the user clicks a second time to confirm the operation within a certain time. We need to store the value return by setTimeout so we can also clear it when necessary
  3. countdown: this is used to display a counter along with the text label to show the number of seconds remaining
  4. countdownIntervalId: holds a reference from the setInterval we use to decreases the countdown value. We need this to clear the interval at some points.

countdownIntervalId: we need this to clear the interval we set with setInterval that will decrease the countdown value

We need to expand the code within onClick to also update the countdown with a setInterval:

this.countdownIntervalId = setInterval(() => {
    --this.countdown;
    if (this.countdown <= 0) {
        // reset the countdown interval and value
        clearInterval(this.countdownIntervalId);
        this.countdown = this.timeoutSecs;
    }
}, 1000);

Here is the complete JavaScript for the component with the necessary updates:

Vue.component('button-with-confirm', {
  template: `
    <button type="button" :class="computedCss" @click="onClick">
      <div class="countdown-bar" :style="countdownBarStyle"></div>
      <div class="ripple-container">
      </div>
      <span class="btn-label">{{ computedLabel }}</span>
    </button>
  `,
  props: {
    defaultLabel: {
      type: 'string',
      default: 'Confirm Button'
    },
    confirmLabel: {
      type: 'string',
      default: 'Are you sure?'
    },
    defaultCss: {
      type: 'string',
      default: 'btn'
    },
    timeoutSecs: {
      type: 'number',
      default: 5
    }
  },
  
  data() {
    return {
      defaultLabel: '',
      clickedOnce: false, // flag we'll use to track if button has been clicked already once
      countdown: 0 // optional for displaying a countdown progress
    };
  },
 
  computed: {
    computedLabel(){
      return this.clickedOnce ? `${ this.confirmLabel } ${ this.countdown }` : this.defaultLabel;
    },
    computedCss(){
      return this.clickedOnce ? `${ this.defaultCss } confirm` : this.defaultCss;
    },
    countdownBarStyle() {
      let transitionDuration = this.timeoutSecs;
      return `transition-duration: ${ transitionDuration }s`;
    }
  }, 
  
  created() {
    // private variables, non-reactive
    this.timeoutId = undefined;
    this.countdown = this.timeoutSecs;
    this.countdownIntervalId = undefined;
  },
  
  methods: {
    onClick() {
      
      if (this.clickedOnce) {
        // If clickedOnce is true, the user has already clicked once, so this is the 2nd click within the time specified by the timeouteSecs property
        // Here we emit 'confirmed'
        this.$emit('confirmed');
        // also need to clear the timeoutId
        clearTimeout(this.timeoutId);
   } else {
    // If clickedOnce is false, the user has clicked on the button for the first time
        // Here we set the clickedOnce flag to true and emit the 'once' event (usually you would not care about knowing it has been clicked the first time, but if you do you can respond to the @once event where you will consume this component)
        this.clickedOnce = true;
        this.$emit('once', this);
          
        // then we invoke setTimeout with the milliseconds parameter calculated from the timeoutSecs property value:
        const timeoutMs = (this.timeoutSecs * 1000); // convert secs to ms
        const self = this;
        this.timeoutId = setTimeout(function() {
            // after timeoutMs has passed, we clear timeoutId and reset clickedOnce to false
            clearTimeout(self.timeoutId);
            self.clickedOnce = false;
        self.$emit('timedout', this);
     }, timeoutMs);
        
        // this is just additional fancy stuff to show the countdown progress if you want:
        this.countdownIntervalId = setInterval(() => {
            --this.countdown;
            if (this.countdown <= 0) {
                // reset the countdown interval and value
                clearInterval(this.countdownIntervalId);
                this.countdown = this.timeoutSecs;
            }
        }, 1000);
      }
    },
    
    reset() {
      clearTimeout(this.timeoutId);
      this.countdown = this.timeoutSecs;
      this.clickedOnce = false;
    }
  }
});

And here is an example on how to consume it:

<button-with-confirm ref="btn" @confirmed="onConfirmed"></button-with-confirm>

You can see a demo here on Codepen: https://codepen.io/damianof/pen/MWaepLL

P.S. I invite you to use TypeScript for your Vue applications as it makes things much less error prone and code ends up being much more clean. You can download a free sample of my book here to help you get started with Vue and TypeScript: https://leanpub.com/large-scale-apps-with-vue-and-typescript

This article was also published on Medium for your convenience. You can find it here: https://medium.com/@DamianoMe/a-vue-button-component-with-confirm-part-2-1f3b6051f701