Laravel+Vue SPA Catalog Filters

  • filter1

  • filter2

  • filter3


Source code: https://github.com/sunnygreentea/laravel-vue-catalog-filter-properties


A single-page application (SPA) is a web application or website that interacts with the user by dynamically rewriting the current web page with new data from the web server, instead of the default method of the browser loading entire new pages. This means the browser loads an initial web page, navigating to new pages do not require the browser to unload the page and load a new page, instead, any data required for new pages can be loaded asynchronously with AJAX.

The advantages of SPA:

1. SPAs are fast since most resources (HTML + CSSS + Scripts) and DOM components are only loaded once throughout the entire cycle of an application.

2. It is easier to make a mobile application because the developer can reuse the same backend code for web application and native mobile application.

3. SPA can cache any local storage effectively.

This demo shows how multi filters and counters work when searching real estate properties, especially how Vue manipulate and watch changes of data.

As shown in the screenshots, the left column is the filter of properties. It has three filters: price, property type and cities. Each checkbox has a count number. When users select one or multiple filters, the right column shows the right selected properties.

The magic points of this application are:

  • when users check or uncheck one or more filters, the corresponding count numbers change automatically.
  • the properties shown at the right column also change automatically according to different selected filters.
  • all this happens in s single page, we do not need to reload the page.

Thanks to Vue we can accomplish this magic. Comparing to jQuery, another popular JavaScript library, Vue has its own big pros: while jQuery deals with DOM elements such as adding, removing, or modifying tags, Vue manages data such as variables and properties. This demo is a perfect example to use Vue to manipulate and watch changes of data.

In a Vue template we define the HTML structure, put the part we want to change as dynamic data, here are filter counting numbers and property list. For each filter we have a v-model attached so we can watch the changes of selected filters.

<template>
	<div class="container">
		<div class="row">
			<div class="col-md-3 mb-4">
				<h2 class="mt-4">Filters</h2>
                
                <hr>
                <h3 class="mt-2">Prices</h3>
                <div class="form-check" v-for="(price, index) in prices">
                    <input class="form-check-input" type="checkbox" :value="index" :id="'price'+index" v-model="selected.prices">
                    <label class="form-check-label" :for="'price' + index">
                        {{ price.name }} ({{ price.listings_count }})
                    </label>
                </div>

                <hr>
                <h3 class="mt-2">Types</h3>
                <div class="form-check" v-for="(propertytype, index) in propertytypes">
                    <input class="form-check-input" type="checkbox" :value="propertytype.id" :id="'propertytype'+index" v-model="selected.propertytypes">
                    <label class="form-check-label" :for="'propertytype' + index">
                        {{ propertytype.name }} ({{ propertytype.listings_count }})
                    </label>
                </div>

                <hr>
                <h3 class="mt-2">Cities</h3>
                <div class="form-check" v-for="(city, index) in cities">
                    <input class="form-check-input" type="checkbox" :value="city.id" :id="'city'+index" v-model="selected.cities">
                    <label class="form-check-label" :for="'city' + index">
                        {{ city.name }} ({{ city.listings_count }})
                    </label>
                </div>
			</div>
			<div class="col-md-9 mb-4">
				<div class="row mt-4">
					<div class="col-12">
						<h1>Properties</h1>
					</div>
                    <div class="col-lg-4 col-md-6 mb-4" v-for="listing in listings">
                        <div class="card h-100 border-primary">
                            <a href="#">
                                <img class="card-img-top" v-bind:src="'img/listing-' + listing.id+'.jpg'" alt="">
                            </a>
                            <div class="card-body">
                                <h4 class="card-title" style="height:50px;">
                                    <a href="#">{{ listing.name }}</a>
                                </h4>
                                <h5>$ {{ listing.price }}</h5>
                                <p class="card-text"><b>{{ listing.propertytype }} - {{ listing.city }}</b></p>                                
                            </div>
                        </div>
                    </div>
                </div>
			</div>
		</div>
	</div>
</template>


When the page is loaded, we load prices, property types, cities, and all properties. Then we watch the changes of selected filters; and reload everything again. For example, for cities, the function fetchCities loads cities with all parameters except selected cities. Same thing for property types and price. Then we set selected filter data as parameters to fetch properties.

<script>
    export default {
        data: function () {
            return {
                prices: [],
                cities: [],
                propertytypes: [],
                listings: [],
                loading: true,
                selected: {
                    prices: [],
                    cities: [],
                    propertytypes: []
                }
            }
        },

        mounted() {
        	this.fetchCities();
        	this.fetchPropertytypes();
            this.fetchPrices();
            this.fetchListings();

        },

        watch: {
        	selected: {
            	handler() {
                    this.fetchCities();
                    this.fetchPropertytypes();
                    this.fetchPrices();
                    this.fetchListings();
                },
                deep: true
            }
        },

        methods: {
           
            fetchListings () {
            	axios.get('api/listings', {
            		params: this.selected
            	})
                .then(res => {
                    this.listings = res.data.data;
                })
                .catch(err => console.log(err));
            },

            fetchCities () {
            	axios.get('api/cities', {
                        params: _.omit(this.selected, 'cities')
                    })
                .then(res => {
                    this.cities = res.data.data;
                })
                .catch(err => console.log(err));
            },

            fetchPropertytypes () {
            	axios.get('api/propertytypes', {
            		params: _.omit(this.selected, 'propertytypes')
            	})
                .then(res => {
                    this.propertytypes = res.data.data;
                })
                .catch(err => console.log(err));
            },

            fetchPrices () {
                axios.get('api/prices', {
                    params: _.omit(this.selected, 'prices')
                })
                .then(res => {
                    this.prices = res.data;
                })
                .catch(err => console.log(err));
            },
        }
    }
</script>