1

Here's my issue. I created a tool with vue.js and the WordPress API to search through the search endpoints for any keyword and display the result. So far so good, everything is working, except for a bug that I spotted.

Here's the deal:

const websiteurl = 'https://www.aaps.ca'; //yourwebsite or anything really
var vm = new Vue({
    el: '#blog-page',
    data: {
        noData: false,
        blogs: [],
        page: 0,
        search: '',
        totalPagesFetch: "",
        pageAmp: "&page=",
        apiURL: `${websiteurl}/wp-json/wp/v2/posts?per_page=6`,
        searchbyid: `${websiteurl}/wp-json/wp/v2/posts?per_page=6&include=`,
        searchUrl: `${websiteurl}/wp-json/wp/v2/search?subtype=post&per_page=6&search=`,
    },
    created: function () {
        this.fetchblogs();
    },
    methods: {
        fetchblogs: function () {
            let self = this;
            self.page = 1;
            let url = self.apiURL;
            fetch(url)
            .then(response => response.json())
            .then(data => vm.blogs = data);
        },
        searchonurl: function () {
            let ampersand = "&page=";
            searchPagination(1, this, ampersand);
        },
    }
});

function searchPagination(page, vm, pagen) {

    let self = vm;
    let searchword = self.search.toLowerCase();
    let newsearchbyid = self.searchbyid;
    let url;

    self.page = page;

    url = self.searchUrl + searchword + pagen + self.page;

    self.mycat = 'init';

    fetch(url)
    .then(response => {

        self.totalPagesFetch = response.headers.get("X-WP-TotalPages");
        return response.json();
    })
    .then(data => {
        
        let newid = [];
        data.forEach(function (item, index) {
            newid.push( item.id );
        });
        if (newid.length == 0) {
            return newsearchbyid + '0';
        } else {
            return newsearchbyid + newid;
        }
    })
    .then(response2 => {
        return fetch(response2)
    })
    .then(function(data2) {
        return data2.json();
    })
    .then(function(response3) {
        console.log(response3)
        if (response3.length == 0) {
            vm.noData = true;
            vm.blogs = response3;
        } else {
            vm.noData = false;
            vm.blogs = response3;
        }
    })
}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div class="lazyblock-blogs testblog" id="blog-page">
    <div class="container">
        <div class="row controls">
            <div class="col-md-12">
                <div class="search-blog">
                    <img height="13" src="" alt="search">
                    <input id="sb" type="text" v-model="search" @keyup="searchonurl" placeholder="search">
                </div>
            </div>
        </div>

        <div class="row">

            <div class="col-md-4" v-for="(b, index) in blogs">
                <div class="h-100 box" v-cloak>
                    <img width="100%" v-bind:src=b.featured_image_url>
                    <a v-bind:href="b.link">
                        <h3 v-html=b.title.rendered></h3>
                    </a>
                    <div v-html=b.excerpt.rendered></div>
                    <p class="read-more"><a v-bind:href="b.link">read more</a></p>
                </div>
            </div>

            <div class="no-data" v-if="noData">
                <div class="h-100">
                    No post
                </div>
            </div>

        </div>

    </div>
</div>

I'm using a keyup event which is causing me some problems because it works, but in same cases, for example, if the user is very fast to type characters and then suddenly he wants to delete the word and start again, the response for the API has some sort of lag.

The problem is that I guess that the Vue framework is very responsive (I create a variable call search that will update immediately) but the API call in the network is not (please check my image here):

enter image description here

This first image appears if I type lll very fast, the third result will return nothing so it is an empty array, but if I will delete it immediately, it will return an url like that: https://www.aaps.ca//wp-json/wp/v2/search?subtype=post&per_page=6&search=&page=1 which in turn should return 6 results (as a default status).

The problem is that the network request won't return the last request but it gets crazy, it flashs and most of the time it returns the previous request (it is also very slow).

Is that a way to fix that?

I tried the delay function:

function sleeper(ms) {
  return function(x) {
    return new Promise(resolve => setTimeout(() => resolve(x), ms));
  };
}

and then I put before the then function:

.then(sleeper(1000))

but the result is the same, delayed by one second (for example)

Any thought?

  • 3
    You might want to [cancel](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) old requests if the users types fast, alternatively you can implement something like [debounce or throttling](https://stackoverflow.com/questions/25991367/difference-between-throttling-and-debouncing-a-function) to send requests less frequent. – Reyno Jun 30 '22 at 12:37
  • How can I cancel an old request on keyup? I read something about aborting with the fetch api, but it is not clear and I'm not sure if that would be applicable here – Marcuzio Developuzio Jun 30 '22 at 12:42
  • 1
    Is this: https://www.codingdeft.com/posts/fetch-cancel-previous-request/ something similar to what I'm looking for @Reyno? it is seems to work so far – Marcuzio Developuzio Jun 30 '22 at 12:53
  • 1
    Yes, using a [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) is the way to go! – Reyno Jun 30 '22 at 12:54
  • hey @Reyno, you were right, it is the way to go (even if the tool will throw an error in the console, whatever). Would you like to add your solution so I can mark it? Great suggestion I just added if (window.controller) { window.controller.abort() } window.controller = new AbortController() window.signal = window.controller.signal – Marcuzio Developuzio Jun 30 '22 at 14:40
  • You should probably put a `debounce` anyway. – kissu Jul 04 '22 at 12:15
  • 3
    I would go for the debounce, I couldn't find anywhere how the http server handles abort signals (and if signal any is actually sent), but my guess is that since Abort is on the client side, after the server gets the request it won't be interrupted, just the response ignored by the client. So your server will still be slower and have to handle a bunch of useless ignored requests – Alykam Burdzaki Jul 05 '22 at 09:04

3 Answers3

1

You could use debounce, no call will leave until the user stop typing in the amount of time you chose

function debounce(func, wait, immediate) {
  var timeout;
  return function() {
    var context = this, args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(function() {
        timeout = null;
        if (!immediate) func.apply(context, args);
    }, wait);
    if (immediate && !timeout) func.apply(context, args);
  };
}

// in your "methods" (I put 1000ms of delay) :
searchonurl: function () {
  let ampersand = "&page=";
  debounce(searchPagination, 1000)(1, this, ampersand);
}
Jon
  • 29
  • 5
  • 1
    The answer just uses someone else's debounce implementation without attribution and applies it in a wrong way. It doesn't make sense to call debounced function instantly. – Estus Flask Jul 10 '22 at 20:56
  • @EstusFlask how would you implement it? Please, add your solution and I will reward it with the bounty if it is good! – Marcuzio Developuzio Jul 11 '22 at 14:12
1

One of best ways is to use Debounce which is mentioned in this topic

Or use a function and combine it with watch. Follow these lines:

In mounted or created make an interval with any peroid you like (300 etc.) define a variable in data() and name it something like searched_value. In interval function check the value of your input and saerch_value, if they were not equal (===) then replace search_value with input value. Now you have to watch search_value. When it changed you call your api.

I use this method and works fine for me. Also it`s managable and everything is in your hand to config and modify.

===== UPDATE: =====

A simple code to do what I said above

<template>
    <div>
        <input type="search" v-model="search_key">
    </div> </template>

<script> export default {
    name: "SearchByApi",
    data() {
        return {
            search_key: null,
            searched_item: null,
            loading: false,
            debounceTime: 300,
        }
    },
    created() {
        this.checkTime()
        const self = this
        setInterval(function() {
            self.checkTime()
        }, this.debounceTime);
    },
    watch: {
        searched_item() {
            this.loadApi()
        }
    },
    methods: {
        checkTime() {
            if (this.searched_item !== this.search_key && !this.loading) {
                this.searched_item = this.search_key
            }
        },
        loadApi() {
            if (!this.loading && this.searched_item?.length > 0) {
                this.loading = true
                const api_url = 'http://api.yourdomain.com'
                axios(api_url, {search: this.searched_item}).then(res => {
                    // whatever you want to do when SUCCESS
                }).catch(err => {
                    // whatever you want to do when ERROR
                }).then(res => {
                    this.loading = false
                })
            }
        }
    }
}
</script>
MojSkin
  • 81
  • 1
  • 5
1

This is the case for debounced function. Any existing implementation can be used, e.g. Lodash debounce. It needs to be declared once per component instance, i.e. in some lifecycle hook.

That searchPagination accepts this as an argument means that something went wrong with its signature. Since it operates on component instance, it can be just a method and receive correct this context:

methods: {
  searchPagination(page, pagen) {
    var vm = this; 
    ...
  },
  _rawsearchonurl() {
    let ampersand = "&page=";
    this.searchPagination(1, ampersand);
  }
},
created() {
  this.searchonurl = debounce(this._rawsearchonurl, 500);
  ...
}
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • I'm not sure if I understand it correctly, and it seems an incomplete answer because I cannot really see the debounce function. I couldn't understand the fact why this shouldn't work on the searchPagination function – Marcuzio Developuzio Jul 12 '22 at 08:22
  • 1
    As said, `debounce` can be any implementation, e.g. Lodash. I'm not sure about the second part. `searchPagination` can be as in your original code, but passing `this` as an argument is an antipattern, means that a component weren't properly designed. – Estus Flask Jul 12 '22 at 08:30
  • Just another question, but please keep in mind that I've been starting to use VUE very recently. Can't everything happen in the methods time or must the debounce occur in the created phase? Like the previous answer, for example. What it is wrong with that approach? – Marcuzio Developuzio Jul 12 '22 at 09:22
  • 1
    Debounced function should be defined in `created` like shown above but can be called any time later. Should be workable like that, `@keyup="searchonurl"`, what you do is a common case. You can additionally add `searchonurl: null` placeholder to `data`, it's more correct this way but I'm not sure it can affect the way it works here – Estus Flask Jul 12 '22 at 09:25