How to prevent unwanted clicks if someone clicks too fast?

Viewed 198

I have a sidebar with moveable panels - you can see in the snippet they move left or right when you click Menu item or Go back.

The problem is that if you click fast on the buttons (You can recreate the issue by clicking fast and many times on the buttons), it will sometimes calculate currentIndex++ (or --) twice on the same panel which will make it stuck, and then it will just keep adding translate amount to the same panel.

In my example there are only 3 panels with width 40, so it should only move somewhere between translate-x-0 to -translate-x-120, but if it gets stuck, it will simply keep adding jumps of 40 to the stuck panels, so you can see something like -translate-x-800 if you just keep clicking

const App = {
    data() {
        return {
                sidebarWidth: 40,
                currentIndex: 0,      
       }
    },
    computed: {
            position() {
                if (this.currentIndex === 0) {
            return 'translate-x-0'
        } else {
                  return `-translate-x-${this.sidebarWidth * this.currentIndex}`
        }
            }
        }
}

Vue.createApp(App).mount('#app')
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/vue@next"></script>
<div id="app">
    <div class="h-screen flex">
        <div class="flex container w-40 h-80 bg-black mx-auto my-auto text-black overflow-hidden">
            <div class="sidebar container flex transform duration-1000" :class="position">

                <div class="sidebar-menu flex-shrink-0 w-40 transform duration-1000 bg-gray-300">
                    <button @click="currentIndex++">Menu item</button>
                </div>

                <div class="sidebar-menu flex-shrink-0 w-40 bg-blue-300 transform duration-1000">
                    <div class="flex flex-col">
                        <button class="flex justify-start" @click="currentIndex--">
                        Go back
                        </button>

                        <button class="flex justify-start" @click="currentIndex++">
                        Menu item
                        </button>
                    </div>
                </div>

                <div class="sidebar-menu flex-shrink-0 w-40 transform duration-1000 bg-red-300">
                    <div class="flex flex-col">
                        <button class="flex justify-start" @click="currentIndex--">
                        Go back
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

2 Answers

The problem is the index increment/decrement is not limited/truncated, so it's possible to exceed the actual number of menus even if using a debouncer.

Determine the menu count by querying .sidebar-menu elements in the component, and prevent the index from exceeding this maximum value:

export default {
    mounted() {
        this.menuCount = this.$el.querySelectorAll('.sidebar-menu').length
    },
    methods: {
        nextIndex(delta) {
           this.currentIndex = (this.currentIndex + delta) % this.menuCount
        }
    }
}

Then bind that method to the click-handlers:

<button class="flex justify-start" @click="nextIndex(-1)">
  Go back
</button>
<button class="flex justify-start" @click="nextIndex(1)">
  Menu item
</button>

const App = {
    data() {
        return {
                sidebarWidth: 40,
                currentIndex: 0,
                menuCount: 0,
       }
    },
    computed: {
       position() {
           if (this.currentIndex === 0) {
               return 'translate-x-0'
           } else {
              return `-translate-x-${this.sidebarWidth * this.currentIndex}`
           }
       }
    },
    mounted() {
        this.menuCount = this.$el.querySelectorAll('.sidebar-menu').length
    },
    methods: {
        nextIndex(delta) {
           this.currentIndex = (this.currentIndex + delta) % this.menuCount
        }
    }
}

Vue.createApp(App).mount('#app')
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/vue@next"></script>
<div id="app">
    <div class="h-screen flex">
        <div class="flex container w-40 h-80 bg-black mx-auto my-auto text-black overflow-hidden">
            <div class="sidebar container flex transform duration-1000" :class="position">

                <div class="sidebar-menu flex-shrink-0 w-40 transform duration-1000 bg-gray-300">
                    <button @click="nextIndex(1)">Menu item</button>
                </div>

                <div class="sidebar-menu flex-shrink-0 w-40 bg-blue-300 transform duration-1000">
                    <div class="flex flex-col">
                        <button class="flex justify-start" @click="nextIndex(-1)">
                        Go back
                        </button>

                        <button class="flex justify-start" @click="nextIndex(1)">
                        Menu item
                        </button>
                    </div>
                </div>

                <div class="sidebar-menu flex-shrink-0 w-40 transform duration-1000 bg-red-300">
                    <div class="flex flex-col">
                        <button class="flex justify-start" @click="nextIndex(-1)">
                        Go back
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

Here is an example using debounce from lodash. It works by aggregating repeated events and only firing the last one. You can run the example to see how it works.

const App = {
    data() {
        return {
                sidebarWidth: 40,
                currentIndex: 0,      
       }
    },
    methods: {
      debounceInput: _.debounce(function (e) {
        // decides whether to increment or decrement current Index
        if (e == "+"){
          this.currentIndex++
        }
        else if (e == '-') {
          this.currentIndex--
        }
      }, 300)
    }
    ,
    computed: {
            position() {
                if (this.currentIndex === 0) {
            return 'translate-x-0'
        } else {
                  return `-translate-x-${this.sidebarWidth * this.currentIndex}`
        }
            }
        }
}

Vue.createApp(App).mount('#app')
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />
<script src="https://unpkg.com/vue@next"></script>
<div id="app">
  <div class="h-screen flex">
    <div class="flex container w-40 h-80 bg-black mx-auto my-auto text-black overflow-hidden">
      <div class="sidebar container flex transform duration-1000" :class="position">

        <div class="sidebar-menu flex-shrink-0 w-40 transform duration-1000 bg-gray-300">
          <button @click="debounceInput('+')">Menu item</button>
        </div>

        <div class="sidebar-menu flex-shrink-0 w-40 bg-blue-300 transform duration-1000">
          <div class="flex flex-col">
            <button class="flex justify-start" @click="debounceInput('-')">
                        Go back
                        </button>

            <button class="flex justify-start" @click="debounceInput('+')">
                        Menu item
                        </button>
          </div>
        </div>

        <div class="sidebar-menu flex-shrink-0 w-40 transform duration-1000 bg-red-300">
          <div class="flex flex-col">
            <button class="flex justify-start" @click="currentIndex--">
                        Go back
                        </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Related