Search code examples
javascriptvue.jsvue-resource

vue.js with Laravel pagination and filtering for list


I'm still debating the approach but this is what I have so far. The filters work great but the data list is over 2k items, so some sort of limit is going to need to be applied. The issue is that any limit to the view would have to require a new API pull with any filter being set by any methods I've tried.

My question is should I somehow paginate from the API call or pull all data and paginate with vue? Should I perhaps filter in the backend and apply different API calls or should I use what I have below? The filtering works nicely but I am not clear how I could limit whats displayed yet filter through the entire list then return.

My Vue app and components:

Vue.component('filter', {

props: ['list', 'name'],

template: '#filter-template',

watch: {

    selected: function(currentValue) {

        this.$dispatch('filter-update', [this.name, currentValue]);

    }

}

 });

Vue.component('products', {

template: '#product-template',

created() {

    this.fetchProducts();

},

data:function(){
    return {
        products:[],
        category: 'All',
        collection: 'All',
        design: 'All'
    }
},

events: {

    'push-category': function(id) {

        this.category = id;
        this.filterProducts();

    },

    'push-collection': function(id) {

        this.collection = id;
        this.filterProducts();

    },

    'push-design': function(id) {

        this.design = id;
        this.filterProducts();

    }

},

computed: {

    total: function () {
        return this.products.length;
    }

},

methods: {

    fetchProducts: function() {

        this.$http.get('api/internal/products', function(products) {

            this.$set('products', products);

        });

    },

    filterProducts: function() {

        var parent = this;
        var catFilter = [];
        var collFilter = [];
        var designFilter = [];

        // Collect all the bad guys
        if(this.category == 'All') { 
            catFilter = [];
        } else {

            var key = 'Item_Disc_Group';

            filter(key, this.category, catFilter);

        }

        if(this.collection == 'All') { 
            collFilter = [];
        } else {

            var key = 'Collection';

            filter(key, this.collection, collFilter);

        }

        if(this.design == 'All') { 
            designFilter = [];
        } else {

            var key = 'Fancy_Color_Intensity';

            filter(key, this.design, designFilter);

        }

        // Hide all the bad guys, show any good guys
        for (var i = this.products.length - 1; i >= 0; i--) {
            var product = this.products[i];

            if(catFilter.indexOf(product) > -1) {
                product.On_The_Web = "0";
            } else if(collFilter.indexOf(product) > -1) {
                product.On_The_Web = "0";
            } else if(designFilter.indexOf(product) > -1) {
                product.On_The_Web = "0";
            } else {
                product.On_The_Web = "1";
            }               


        };

        function filter(key, active, array) {

            for (var i = parent.products.length - 1; i >= 0; i--) {
                var product = parent.products[i];

                if(product[key] != active) {
                    array.push(product);
                }

            };

        }

    }

}

});


new Vue({

el: '#product-filter',

created() {

    this.fetchCategories();
    this.fetchCollections();
    this.fetchDesigns();

},

methods: {

    fetchCategories: function() {

        this.$http.get('api/categories', function(categories) {

            this.$set('categories', categories);

        });

    },

    fetchCollections: function() {

        this.$http.get('api/collections', function(collections) {

            this.$set('collections', collections);

        });

    },

    fetchDesigns: function() {

        this.$http.get('api/designs', function(designs) {

            this.$set('designs', designs);

        });

    }

},

events: {

    'filter-update': function(data) {

        if(data[0] == "categories") {
            this.$broadcast('push-category', data[1]);
        } else if (data[0] == "collections") {
            this.$broadcast('push-collection', data[1]);
        } else {
            this.$broadcast('push-design', data[1]);
        }

    }

}

});

My markup:

<div id="product-filter">

<filter :list="categories" name="categories"></filter>

<filter :list="collections" name="collections"></filter>

<filter :list="designs" name="designs"></filter>

<div class="clearfix"></div>

<products></products>


<!-- Vue Templates -->

<template id="filter-template">
    <div class="form-group col-md-2">
        <label>@{{ name }}</label>
        <select v-model="selected" class="form-control input-small">
            <option selected>All</option>
            <option value="@{{ option.navision_id }}" v-for="option in list">@{{ option.name }}</option>
        </select>
        <span>Selected: @{{ selected }}</span>
    </div>
</template>

<template id="product-template">
    <div class="row mix-grid thumbnails">
        <h1>@{{ total }}</h1>
        <div v-for="product in products" v-if="product.On_The_Web == '1'" class="col-md-3 col-sm-6 mix category_1">
            <a href="/products/@{{ product.id }}">
                <div class="mix-inner">
                    <img class="img-responsive" src="https://s3-us-west-1.amazonaws.com/sg-retail/images/products/@{{ product.No }}.jpg" alt="">
                    <div class="prod-details">
                        <h3>@{{ product.No }}</h3>
                        <p></p>
                        <span class="price"></span>
                    </div>
                </div>
            </a>
            <div class="prod-ui">
            </div>
        </div>
    </div>
</template>

</div>

Solution

  • If the query takes more than a few seconds I would implement the server side filtering, but really thats preference for you. If you go with client-side filters, you can use the built in filterBy filter:

    <div v-for="product in products | filterBy category in 'Item_Disc_Group' | filterBy collection in 'Collection' | filterBy design in 'Fancy_Color_Intensity'">
    

    Just make the default value '' instead of All so that if no filter is selected then the list won't be filtered at all.

    <option value="" selected>All</option>
    

    Then you can also use a pagination filter to further page the results (borrowed from this fiddle):

    data:function(){
        return {
            currentPage: 0,
            itemsPerPage: 1,
            resultCount: 0
        }
    },
    computed: {
        totalPages: function() {
          return Math.ceil(this.resultCount / this.itemsPerPage)
        }
    },
    methods: {
        setPage: function(pageNumber) {
          this.currentPage = pageNumber
        }
    },
    filters: {
        paginate: function(list) {
            this.resultCount = list.length
            if (this.currentPage >= this.totalPages) {
              this.currentPage = this.totalPages - 1
            }
            var index = this.currentPage * this.itemsPerPage
            return list.slice(index, index + this.itemsPerPage)
        }
    }
    

    That would be added last after the filtering:

    <div v-for="product in products | filterBy category in 'Item_Disc_Group' | filterBy collection in 'Collection' | filterBy design in 'Fancy_Color_Intensity' | paginate">
    

    You'd also want to add buttons to support pagination, ie:

    <ul>
      <li v-repeat="pageNumber: totalPages">
        <a href="#" v-on="click: setPage(pageNumber)" v-class="current: currentPage === pageNumber">{{ pageNumber+1 }}</a>
      </li>
    </ul>