eggplantSword's avatar

Search and Pagination troubleshooting

I have this code, I want to be able to search through all products regardless on what page the pagination is on, however now it's doing a couple weird things.

  • Only searching through items on current page, when not on first page
  • When clearing the search the v-for doesn't reset correctly, it resets to the page is was before but the pagination number reads as 1 when it's not.
<el-row>
    <el-col>
        <el-input v-model="search"/>
    </el-col>
</el-row>

<div v-if="Object.keys(displayData).length !== 0">
    <span v-for="(prod, index) in displayData" :key="index">
        <product-component :product="prod" v-on:addToCart="addToCart"/>
    </span>

    <div>
        <el-pagination
            @current-change="handleCurrentChange"
            :page-size="pageSize"
            :total="total">
        </el-pagination>
    </div>
</div>
data() {
     return {
         search: '',
     }
},
computed: {
        displayData() {
            if (this.search == null) return this.products;

            this.filtered = this.products.filter(data => !this.search || data.name.toLowerCase().includes(this.search.toLowerCase()));
            this.total = this.filtered.length;

            let stor = this.sort.split(" ");
            if (stor[0] === 'price') {
                stor[0] = this.price;
            }

            this.filtered = _.orderBy(this.filtered, [stor[0]], [stor[1]]);

            return this.filtered.slice(this.pageSize * this.page - this.pageSize, this.pageSize * this.page);
        }
    },
    methods: {
        handleCurrentChange(val) {
            this.page = val;
        },
}

Any ideas on why this is happening and how to improve this code?

0 likes
8 replies
rodrigo.pedra's avatar

I would have the filtered records apart from the paginated one:

<el-row>
    <el-col>
        <el-input v-model.trim="search" />
    </el-col>
</el-row>

<div v-if="!isEmpty">
    <span v-for="(prod, index) in displayData" :key="index">
        <product-component :product="prod" @addToCart="addToCart" />
    </span>

    <div>
        <el-pagination
            :page-size="pageSize"
            :total="total"
            @current-change="handleCurrentChange" />
    </div>
</div>
data() {
    return {
        search: '',
    };
},

computed: {
    // I'd call this filtered, but can't tell from original code
    // if you have that variable already
    records() {
        const search = String(this.search || '').toLowerCase();

        if (search.length === 0) {
            return this.products;
        }

        return this.products
            .filter((data) => data.name.toLowerCase().includes(search));
    },

    isEmpty() {
        return this.records.length === 0; // edit: fixed
    },

    total() {
        return this.records.length;
    },

    displayData() {
        const stor = this.sort.split(' ');

        if (stor[0] === 'price') {
            stor[0] = this.price; // don't know if this is needed
        }

        return _.orderBy(this.records, [stor[0]], [stor[1]])
            .slice(this.pageSize * (this.page - 1), this.pageSize);
    },
},

methods: {
    handleCurrentChange(val) {
        this.page = val;
    },
},

Hope it helps

eggplantSword's avatar

@rodrigo.pedra I need to display all the products when the search is empty, and filter them when the search isn't in your code the products don't display until the search is activated.

rodrigo.pedra's avatar

Yes there is a typo in the isEmpty computed property, it should read:

isEmpty() {
    return this.records.length === 0;
},
eggplantSword's avatar

@rodrigo.pedra This is the solution I came up with

searching() {
    if (!this.search) {
        this.total = this.products.length;
        return this.products;
    }
    this.page = 1;
    return this.products.filter(data => data.name.toLowerCase().includes(this.search.toLowerCase()));
},
displayData() {
    this.total = this.searching.length;

    return this.searching.slice(this.pageSize * this.page - this.pageSize, this.pageSize * this.page);
}
rodrigo.pedra's avatar

I would keep total and page calculation on their own computed properties or data properties.

In general Vue docs advise computed properties not to have a side effect.

It would become something like this:

data() {
    return {
        // other data ...
        search: '',
        page: 1,
    };
},

computed: {
    searching() {
        if (!this.search) {
            return this.products;
        }

        return this.products
            .filter(data => data.name.toLowerCase().includes(this.search.toLowerCase()));
    },

    displayData() {
        return this.searching.slice(this.pageSize * this.page - this.pageSize, this.pageSize * this.page);
    },

    total() {
        return this.searching.length;
    },
},

watch: {
    search() {
        // whenever search changes, go back to page 1
        this.page = 1;
    },
},

I guess your slice version works better than mine. I confused with PHP's where the second index is the slice length, whereas in JavaScript is the last index.

rodrigo.pedra's avatar

Computed properties are optimized so their return value is cached and Vue intelligently changes them only when their dependencies changes.

From the Vue docs:

... computed properties are cached based on their reactive dependencies.

Reference: https://vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods

So as rule of thumb, computed properties should be very straightforward and not have side-effects (i.e. change other properties or call other methods/external entities).

If keeping them simple Vue will be able to optimize them and increase performance. Also it less likely that any strange behavior hard to track happens when keeping them tidy.

Please or to participate in this conversation.