Search code examples
ajaxvue.jsvuejs2vue-componentdebouncing

Search functionality with rest api prevent DDOSing the server


The Problem

I have a search component and component which implements the search component. When I type something in the search bar after 1/2 second of not typing (debounce) the server should be hit and the results should be returned.

The solution i am trying to implement comes from this post on Stackoverflow

The code

This leads me to the following code.

I have search.vue

<template>
    <label for="search">
        <input
                id="search"
                class="w-full py-2 px-1 border-gray-900 border"
                type="text"
                name=":searchTitle"
                v-model="searchFilter"
                :placeholder="searchPlaceholder"
                autocomplete="off"
                v-on:keydown="filteredDataset"
        />
    </label>
</template>
<script>
  import {debounce} from 'lodash';
  export default {
    props: {
      searchPlaceholder: {
        type: String,
        required: false,
        default: ''
      },
      searchName: {
        type: String,
        required: false,
        default: 'search'
      }
    },
    data() {
      return {
        searchFilter: '',
      }
    },
    methods: {
      filteredDataset() {
        console.log('event fired');
        this.$emit('searchValue', this.searchFilter);
      }
    },
  }
</script>

And product.vue

<template>
    <div>
        <div class="my-4">
            <search
                    search-placeholder=""
                    search-name=""
                    v-on:searchValue="filterValue = $event"
                    v-model="productsFiltered"
            >

            </search>
            <div class="flex w-full py-1 border px-2 my-2" v-for="product in productsFiltered"> (...)
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import {debounce} from 'lodash';
  export default {
    data() {
      return {
        products: [],
        filterValue: '',
        filteredProducts: ''
      }
    },
    computed: {
      productsFiltered: {
            get(){
              console.log('getter called');
                return this.filteredProducts;
            },
            set: _.debounce(function(){
              console.log('setter called');
              if (this.filterValue.length < 1) {
                this.filteredProducts = [];
              }

              axios.get(`${apiUrl}search/` + this.filterValue)
                .then(response => {
                  this.products = response.data.products;
                  const filtered = [];
                  const regOption = new RegExp(this.filterValue, 'ig');
                  for (const product of this.products) {
                    if (this.filterValue.length < 1 || product.productname.match(regOption)) {
                      filtered.push(product);
                    }
                  }
                  this.filteredProducts = filtered;
                });


            }, 500)
      }
    },

  }

</script>

The result The result is that the setter in the computed property in product.vue does not get called and no data is fetched from the server. Any ideas on how to solve this?


Solution

  • Your first code block imports debounce but does not use it. It also declares a prop, searchName, that isn't used. These aren't central issues, but clutter makes it harder to figure out what's going on.

    Your second code block uses v-model but does not follow the required conventions for getting v-model to work with components:

    1. the component must take a prop named value
    2. the component must emit input events to signal changes to value

    You have the component emit searchValue events, and handle them with a v-on that sets a data item. You seem to expect the v-model to call the setter, but as I noted, you haven't hooked it up to do so.

    From what's here, you don't even really need to store the input value. You just want to emit it when it changes. Here's a demo:

    const searchComponent = {
      template: '#search-template',
      props: {
        searchPlaceholder: {
          type: String,
          required: false,
          default: ''
        }
      },
      methods: {
        filteredDataset(searchFilter) {
          console.log('event fired');
          this.$emit('input', searchFilter);
        }
      }
    };
    
    new Vue({
      el: '#app',
      data() {
        return {
          products: [],
          filterValue: '',
          filteredProducts: ''
        }
      },
      components: {
        searchComponent
      },
      computed: {
        productsFiltered: {
          get() {
            console.log('getter called');
            return this.filteredProducts;
          },
          set: _.debounce(function() {
            console.log('setter called');
            if (this.filterValue.length < 1) {
              this.filteredProducts = [];
            }
            setTimeout(() => {
              console.log("This is the axios call");
              this.filteredProducts = ['one','two','three'];
            }, 200);
    
          }, 500)
        }
      }
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
    <template id="search-template">
        <label for="search">
            <input
                id="search"
                class="w-full py-2 px-1 border-gray-900 border"
                type="text"
                name=":searchTitle"
                :placeholder="searchPlaceholder"
                autocomplete="off"
                @input="filteredDataset"
            />
        </label>
    </template>
    
    <div id="app">
      <div class="my-4">
        <search-component search-placeholder="enter something" v-model="productsFiltered">
    
        </search-component>
        <div class="flex w-full py-1 border px-2 my-2" v-for="product in productsFiltered"> (...)
        </div>
      </div>
    </div>