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

I have worked through so many different user experiences (UX) for asking the users to confirm a potentially dangerous operation like a delete, or maybe just a cancel operation while they still have unsaved changes, I came across the idea of using a button that requires to be clicked a second time within a certain time.

This is nothing new as other developers have created similar controls or components in jQuery or just pure JavaScript. Here I am going to show you how to do something like this in Vue.

First, let’s create our component as a local component for this example.

The template for the button is this:

<button type="button" :class="computedCss" @click="onClick">
    <span>{{ computedLabel }}</span>
</button>

We are going to bind one-way to two computed properties in the template:

  1. computedCss: for the button css class attribute
  2. computedLabel: for the button text label

I named them with the computed prefix to make it more clear they are going to be computed, but you are welcome to name them as you like.

Our button component will have to expose the following properties to the code that will consume it:

  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

We are going to also need the following private properties for the internal component logic:

  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

We also need to handle the component @click event and within our handler implement the main component logic.

Here is the content of our onClick method that will handle the click:

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;
        }, timeoutMs);
    }
}

Here is the complete JavaScript for the component:

Vue.component('button-with-confirm', {
  template: `
    <button type="button" :class="computedCss" @click="onClick">
      <span>{{ computedLabel }}</span>
      <div class="ripple-container">
      </div>
    </button>
  `,
  props: {
    defaultLabel: {
      type: 'string',
      default: 'Confirm Button'
    },
    confirmLabel: {
      type: 'string',
      default: 'Are you sure?'
    },
    defaultCss: {
      type: 'string',
      default: 'btn'
    },
    timeoutSecs: {
      type: 'number',
      default: 3
    }
  },
  
  data() {
    return {
      defaultLabel: '',
      clickedOnce: false // flag we'll use to track if button has been clicked already once
    };
  },
 
  computed: {
    computedLabel(){
      return this.clickedOnce ? this.confirmLabel : this.defaultLabel;
    },
    computedCss(){
      return this.clickedOnce ? `${ this.defaultCss } confirm` : this.defaultCss;
    }
  }, 
  
  created() {
    // private variables, non-reactive
    this.timeoutId = 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;
         }, timeoutMs);
      }
    },
    
    reset() {
      clearTimeout(this.timeoutId);
      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/PoPzWwg

In Part 2, we will enhance this component a bit more to show a few more visual clues to the user and make the user experience much better. You can find Part 2 here: http://www.scalingvue.com/2020/05/23/a-vue-button-component-with-confirm-part-2/

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-1-b79bb923dc