Backend DevelopmentFrontend DevelopmentVueJs Tutorials

Building a Simple Real estate App With Laravel and Vuejs 3 Part10

Building a Simple Real estate App With Laravel and Vuejs 3

In this part of this series we will continue working on the search functionality and we will process the search and connect it with the backend.

 

 

Search Composables

Let’s create two composable files for the search. Each composable responsible for handing specific task as below:

  • useSearchFilters.js: This composable file contains all the reactive properties and handlers for all the inputs in the search form.
  • useSubmitSearch.js: This composable file handles the submission of the search form to the search Api.

resources/js/composables/useSearchFilters.js

import {useStore} from "vuex";
import {computed} from "vue";

import {
    REMOVE_SEARCH_FEATURE,
    SET_SEARCH_CITY,
    SET_SEARCH_COUNTRY,
    SET_SEARCH_FEATURE,
    SET_SEARCH_KEYWORD,
    SET_SEARCH_PROPERTY_FINALIZING,
    SET_SEARCH_RANGE,
    SET_SEARCH_RENT_PER,
    SET_SEARCH_STATE,
    SET_SEARCH_STATUS
} from "../store/types";

export const useSearchFilters = () => {
    const store = useStore();

    const country = computed({get: () => store.state.search.search_params.country, set: (value) => {
            store.commit(`search/${SET_SEARCH_COUNTRY}`, value);
            store.commit(`search/${SET_SEARCH_STATE}`, 0);
            store.commit(`search/${SET_SEARCH_CITY}`, 0);
        }
    });

    const state = computed({get: () => store.state.search.search_params.state, set: (value) => {
            store.commit(`search/${SET_SEARCH_STATE}`, value);
            store.commit(`search/${SET_SEARCH_CITY}`, 0);
        }
    });
    const city = computed({get: () => store.state.search.search_params.city, set: (value) => store.commit(`search/${SET_SEARCH_CITY}`, value)});

    const keyword = computed({get: () => store.state.search.search_params.keyword, set: (value) => store.commit(`search/${SET_SEARCH_KEYWORD}`, value)});
    const status = computed({get: () => store.state.search.search_params.status, set: (value) => store.commit(`search/${SET_SEARCH_STATUS}`, value)});
    const rentPer = computed({get: () => store.state.search.search_params.rent_amount_per, set: (value) => store.commit(`search/${SET_SEARCH_RENT_PER}`, value)});
    const propertyFinalizing = computed({get: () => store.state.search.search_params.property_finalizing, set: (value) => store.commit(`search/${SET_SEARCH_PROPERTY_FINALIZING}`, value)});
    const price = computed(() => store.getters["search/range"]('price'));
    const rent = computed(() => store.getters["search/range"]('rent'));
    const beds = computed(() => store.getters["search/range"]('bedrooms'));
    const baths = computed(() => store.getters["search/range"]('bathrooms'));
    const area = computed(() => store.getters["search/range"]('area'));
    const rooms = computed(() => store.getters["search/range"]('rooms'));
    const garages = computed(() => store.getters["search/range"]('garages'));
    const units = computed(() => store.getters["search/range"]('units'));
    const floor_numbers = computed(() => store.getters["search/range"]('floor_number'));
    const year_built = computed(() => store.getters["search/range"]('year_built'));

    const onRangeChange = (field, data) => {
        store.commit(`search/${SET_SEARCH_RANGE}`, {field: field, value: {min: data[0], max: data[1]}});
    }

    const onSelectFeature = val => {
        store.commit(`search/${SET_SEARCH_FEATURE}`, val);
    }

    const onUnselectFeature = val => {
        store.commit(`search/${REMOVE_SEARCH_FEATURE}`, val);
    }

    return {
        country,
        state,
        city,
        keyword,
        status,
        rentPer,
        baths,
        price,
        rent,
        beds,
        area,
        rooms,
        garages,
        units,
        floor_numbers,
        year_built,
        propertyFinalizing,
        onRangeChange,
        onSelectFeature,
        onUnselectFeature
    };
}

As you see here i declared several computed properties that read and write to Vuex. In the Composition Api to declare a computed property we can use the computed({}) function which accepts an options object. In the options object we can define the getter and setter of the computed property just the same as the Options Api, for example the country computed property:

const country = computed({get: () => store.state.search.search_params.country, set: (value) => {
            store.commit(`search/${SET_SEARCH_COUNTRY}`, value);
            store.commit(`search/${SET_SEARCH_STATE}`, 0);
            store.commit(`search/${SET_SEARCH_CITY}`, 0);
        }
    });

There are computed properties that i declared a getter only like price, rent, beds, etc. Because those properties uses the custom component <NumberRange /> so i make a special handler for this which is onRangeChange() custom event which accepts two arguments (field, data).

The onSelectFeature() and onUnselectFeature() event handlers working the same on <ICheckInput /> custom component.

Now include this composable in the SearchBar.vue

resources/js/components/partials/SearchSidebar.vue

<template>
    <div class="col-md-3 p0 padding-top-40">
        <div class="blog-asside-right pr0">
            <div class="panel panel-default sidebar-menu wow fadeInRight animated" >
                <div class="panel-heading">
                    <h3 class="panel-title">Smart search</h3>
                </div>
                <div class="panel-body search-widget">
                    <form action="#" class=" form-inline">
                        <fieldset>
                            <div class="row">
                                <div class="col-xs-12">
                                    <input type="text" class="form-control" name="keyword" placeholder="Key word" v-model="keyword" />
                                </div>
                            </div>
                        </fieldset>

                        <fieldset>
                            <div class="row">
                                <div class="col-xs-12">

                                  <SelectPicker title="Select your country" :options="countries" name="country" :selected="country" @onChange="onCountryChange" />

                                </div>
                                <div class="col-xs-12">

                                  <SelectPicker title="Select your State" :options="states" name="state" :selected="state" @onChange="onStateChange" />

                                </div>
                                <div class="col-xs-12">

                                  <SelectPicker title="Select your City" :options="cities" name="city" :selected="city" @onChange="onCityChange" />

                                </div>

                                <div class="col-xs-12" style="margin-top: 7px">

                                    <select name="status" class=" show-tick form-control" v-model="status">
                                        <option value=""> -Type- </option>
                                        <option v-for="type in propertyTypes" v-bind:key="type" :value="type">{{type}} </option>
                                    </select>
                                </div>
                            </div>
                        </fieldset>

                        <fieldset class="padding-5">
                            <div class="row">
                                <div class="col-xs-12" v-show="status === 'Sale'">
                                    <NumberRange label="Price range ($)" id="price-range" :min="0" :max="100000000" :step="1" :slider-value="price" @onChange="onRangeChange('price', $event)" />
                                </div>
                                <div class="col-xs-12" v-show="status === 'Rent'">
                                    <NumberRange label="Rent range ($)" id="rent-range" :min="0" :max="100000000" :step="1" :slider-value="rent" @onChange="onRangeChange('rent', $event)" />

                                </div>
                                <div class="col-xs-12" v-show="status === 'Rent'">
                                    <select name="rent_amount_per" class="form-control" v-model="rentPer">
                                        <option value="">- Rent Per -</option>
                                        <option v-for="rPer in rentAmountPerList" :key="rPer.id" :value="rPer.id">{{rPer.name}}</option>
                                    </select>
                                </div>
                            </div>
                        </fieldset>

                        <fieldset class="padding-5">
                            <div class="row">
                                <div class="col-xs-12">
                                    <NumberRange label="Min baths" id="min-baths" :min="0" :max="50" :step="1" :slider-value="baths" @onChange="onRangeChange('bathrooms', $event)" />
                                </div>

                                <div class="col-xs-12">
                                    <NumberRange label="Min bed" id="min-bed" :min="0" :max="50" :step="1" :slider-value="beds" @onChange="onRangeChange('bedrooms', $event)" />
                                </div>

                                <div class="col-xs-12">
                                    <NumberRange label="Min area" id="min-area" :min="0" :max="500" :step="1" :slider-value="area" @onChange="onRangeChange('area', $event)" />
                                </div>

                                <div class="col-xs-12">
                                    <NumberRange label="Min rooms" id="min-rooms" :min="0" :max="50" :step="1" :slider-value="rooms" @onChange="onRangeChange('rooms', $event)" />
                                </div>

                                <div class="col-xs-12">
                                    <NumberRange label="Min garages" id="min-garages" :min="0" :max="10" :step="1" :slider-value="garages" @onChange="onRangeChange('garages', $event)" />
                                </div>

                                <div class="col-xs-12">
                                    <NumberRange label="Min units" id="min-units" :min="0" :max="1000" :step="1" :slider-value="units" @onChange="onRangeChange('units', $event)" />
                                </div>

                                <div class="col-xs-12">
                                    <NumberRange label="Min floors" id="min-floor_number" :min="0" :max="100" :step="1" :slider-value="floor_numbers" @onChange="onRangeChange('floor_number', $event)" />
                                </div>

                                <div class="col-xs-12">
                                    <NumberRange label="Min year built" id="min-year_built" :min="0" :max="(new Date()).getFullYear()" :step="1" :slider-value="year_built" @onChange="onRangeChange('year_built', $event)" />
                                </div>

                                <div class="col-xs-12" style="margin-top: 7px">

                                    <select name="property_finalizing" class="form-control" v-model="propertyFinalizing">
                                        <option value="">- Property Finalizing -</option>
                                        <option v-for="finalizing in propertyFinalizingList" :key="finalizing" :value="finalizing">{{finalizing}}</option>
                                    </select>
                                </div>
                            </div>
                        </fieldset>

                        <fieldset class="padding-5">
                            <div class="row">
                                <div class="col-xs-12" v-for="feature in features" v-bind:key="feature.id">
                                    <ICheckInput type="checkbox" :name="'feature'+feature.id" :value="feature.id" :text="feature.title" @onChecked="onSelectFeature" @onUnchecked="onUnselectFeature" :default-checked="this.$store.state.search.search_params.features.find(i => i == feature.id) !== undefined" />
                                </div>

                            </div>
                        </fieldset>

                        <fieldset>
                            <div class="row">
                                <div class="col-xs-12">
                                    <input class="button btn largesearch-btn" value="Search" type="submit">
                                </div>
                            </div>
                        </fieldset>

                    </form>
                </div>
            </div>

        </div>
    </div>
</template>

<script>
import {useStore} from "vuex";


import ICheckInput from "./ICheckInput";
import NumberRange from "./NumberRange";
import SelectPicker from "./SelectPicker";

import {useSearchFilters} from "../../composables/useSearchFilters";
import {useLoadStaticData} from "../../composables/useLoadStaticData";
import {SET_SEARCH_CITY, SET_SEARCH_COUNTRY, SET_SEARCH_STATE} from "../../store/types";

export default {
    name: "SearchSidebar",
    components: {SelectPicker, NumberRange, ICheckInput},
    setup() {
        const store = useStore();

      const {country, state, city, keyword, status, baths, beds, price, rent, area,
        rooms, garages, units, floor_numbers, year_built, rentPer,
        propertyFinalizing, onRangeChange, onSelectFeature, onUnselectFeature} = useSearchFilters();

      const {rentAmountPerList, propertyFinalizingList, propertyTypes,
        features, countries, states, cities, handleSelectCountry, handleSelectState,
        handleSelectCity, selectedCountry, selectedState, selectedCity} = useLoadStaticData({selectedCountryId: country, selectedStateId: state, selectedCityId: city});

      const onCountryChange = e => {
        handleSelectCountry(e);

        store.commit(`search/${SET_SEARCH_COUNTRY}`, selectedCountry.value);
        store.commit(`search/${SET_SEARCH_STATE}`, 0);
        store.commit(`search/${SET_SEARCH_CITY}`, 0);
      }

      const onStateChange = e => {
        handleSelectState(e);
        store.commit(`search/${SET_SEARCH_STATE}`, selectedState.value);
        store.commit(`search/${SET_SEARCH_CITY}`, 0);
      }

      const onCityChange = e => {
        handleSelectCity(e);
        store.commit(`search/${SET_SEARCH_CITY}`, selectedCity.value);
      }


        return {
            propertyFinalizingList,
            rentAmountPerList,
            propertyTypes,
            features,
            countries,
            states,
            cities,
            country,
            state,
            city,
            keyword,
            status,
            rentPer,
            baths,
            beds,
            price,
            rent,
            area,
            rooms,
            garages,
            units,
            floor_numbers,
            year_built,
            propertyFinalizing,
            onRangeChange,
            onSelectFeature,
            onUnselectFeature,
            onCountryChange,
            onStateChange,
            onCityChange
        }
    }
}
</script>

In this component i imported and included the useSearchFilters composable then i returned the data from the setup() function which then is bound in each of the form elements.

 

The second step is to submit the search filters to the backend which is done in the other composable file.

resources/js/composables/useSubmitSearch.js

import {useStore} from "vuex";
import {useRoute, useRouter} from "vue-router";
import {computed} from "vue";

import {getSearchParamsFromStore} from "../api/helpers";
import {SET_LAYOUT, SET_PAGINATE_PARAMS, SET_SORTING_PARAMS} from "../store/types";


export const useSubmitSearch = () => {
    const store = useStore();
    const route = useRoute();
    const router = useRouter();

    const submitSearch = () => {
        store.dispatch('search/resetPaginateSortParams');

        const params = doSearch();

        updateUrlQueryParams(params, true);
    }

    const doSearch = () => {
        const params = getSearchParamsFromStore({...store.state.search});

        store.dispatch('search/search', params);

        return params;
    }

    const handleLayout = layout => {
        store.commit(`search/${SET_LAYOUT}`, layout);

        let query = Object.assign({}, route.query);
        query = {...query, layout};
        router.replace({query});
    }

    const handlePerPage = perPage => {
        store.commit(`search/${SET_PAGINATE_PARAMS}`, {page: 1, per_page: perPage});

        const params = doSearch();

        updateUrlQueryParams(params);
    }

    const handleSorting = sortObj => {
        store.commit(`search/${SET_SORTING_PARAMS}`, {...sortObj});
        store.commit(`search/${SET_PAGINATE_PARAMS}`, {page: 1, per_page: store.state.search.paginate_params.per_page});

        const params = doSearch();

        updateUrlQueryParams(params);
    }

    const handlePaginate = page => {
        store.commit(`search/${SET_PAGINATE_PARAMS}`, {page, per_page: store.state.search.paginate_params.per_page});

        const params = doSearch();

        updateUrlQueryParams(params);
    }

    const updateUrlQueryParams = (params, reset = false) => {
        let query = Object.assign({}, route.query);

        if(reset) {
            if(Object.entries(params).length) {
                query = {...params};
                router.replace({query});
            } else {
                router.replace({});
            }

            return;
        }

        if(Object.entries(params).length) {
            query = {...query, ...params};
            router.replace({query});
        } else {
            router.replace({});
        }
    }

    const properties = computed(() => store.state.search.properties);
    const loading = computed(() => store.state.search.loading);
    const sortingParams = computed(() => store.state.search.sorting_params);
    const paginateParams = computed(() => store.state.search.paginate_params);
    const defaultLayout = computed(() => store.state.search.defaultLayout);


    return {
        submitSearch,
        properties,
        loading,
        sortingParams,
        paginateParams,
        defaultLayout,
        handleLayout,
        handlePerPage,
        handleSorting,
        handlePaginate,
        doSearch
    }
}

In this composable i added many functions. The submitSearch() function triggered when the user submits the search form. In this function we dispatches “search/resetPaginateSortParams” action, then we invoke another function doSearch() which in turn retrieves the search params from store and then dispatches “search/search” to make an http request to the search endpoint, and the end we update the url query params.

The handleLayout(), handlePerPage(), handleSorting() we will see later which fires on the <SortingBar /> component respectively.

The handlePaginate() function fires when clicking on any pagination links.

Then i defined some computed vars for the properties, loading, sortingParams, paginateParams, defaultLayout.

Open resources/js/api/helpers.js and add this function:

export const getSearchParamsFromStore = function (data) {
    let params = {};

    const stateData = {...data};
    const {search_params, sorting_params, paginate_params} = stateData;

    search_params.range_fields = {...search_params.range_fields};
    search_params.features = [...search_params.features];

    // check for status and delete the appropriate data
    if(search_params['status'] === 'Sale') {
        delete search_params['range_fields']['rent'];
        delete search_params['rent_per'];
    } else if(search_params['status'] === 'Rent') {
        delete search_params['range_fields']['price'];
    } else {
        delete search_params['range_fields']['rent'];
        delete search_params['range_fields']['price'];
    }

    for (let key in search_params) {

        if(key !== "range_fields" && key !== "features") {
            if(search_params[key]) {
                params[key] = encodeURIComponent(search_params[key]);
            }
        } else if (key === "range_fields") {
            for(let subkey in search_params.range_fields) {
                if(search_params.range_fields[subkey].min || search_params.range_fields[subkey].max) {
                    params[subkey] = search_params.range_fields[subkey].min + ',' + search_params.range_fields[subkey].max;
                }
            }
        } else if (key === "features" && search_params.features.length > 0) {
            params[key] = search_params.features.join(",");
        }
    }

    params['sorting_field'] = sorting_params.sortBy;
    params['sorting_order'] = sorting_params.sortOrder;

    params['per_page'] = paginate_params.per_page;
    params['page'] = paginate_params.page;

    return params;
}

This function simply reads the search store state and construct the search parameters object that will be sent to the search endpoint.

Let’s update our search store actions with the real code:

Open resources/js/store/search.js and update the actions like so:

actions: {
        async search({commit}, params) {
            commit(SET_LOADING, true);
           // call the search api
            const response = await window.axios.get('/api/search', {params});

            commit(SET_LOADING, false);

            if(response.status === 200) {
                commit(SET_SEARCH_PROPERTIES, response.data.properties);
            }
        },
        bulkStoreUpdate({commit, state}, queryParams) {
            if(queryParams['keyword']) {
                commit(SET_SEARCH_KEYWORD, queryParams['keyword']);
            }

            if(queryParams['country']) {
                commit(SET_SEARCH_COUNTRY, parseInt(queryParams['country']));
            }

            if(queryParams['state']) {
                commit(SET_SEARCH_STATE, parseInt(queryParams['state']));
            }

            if(queryParams['city']) {
                commit(SET_SEARCH_CITY, parseInt(queryParams['city']));
            }

            if(queryParams['status']) {
                commit(SET_SEARCH_STATUS, queryParams['status']);
            }

            if(queryParams['rent_amount_per']) {
                commit(SET_SEARCH_RENT_PER, parseInt(queryParams['rent_amount_per']));
            }

            if(queryParams['property_finalizing']) {
                commit(SET_SEARCH_PROPERTY_FINALIZING, queryParams['property_finalizing']);
            }

            if(queryParams['features']) {
                commit(SET_SEARCH_FEATURES, queryParams['features'].split(",").map(v => parseInt(v)));
            }

            const rangeFields = ["price", "rent", "bathrooms", "bedrooms", "area", "rooms", "garages", "units", "floor_number", "year_built"];

            rangeFields.map(field => {
                if(queryParams[field]) {
                    const rangeVal = queryParams[field].split(",");
                    if(rangeVal[0] || rangeVal[1]) {
                        commit(SET_SEARCH_RANGE, {field, value: {min: parseInt(rangeVal[0]), max: parseInt(rangeVal[1])}});
                    }
                }
            });

            if(queryParams['layout']) {
                commit(SET_LAYOUT, queryParams['layout']);
            }

            if(queryParams['page'] || queryParams['per_page']) {
                commit(SET_PAGINATE_PARAMS, {page: queryParams['page'] ? queryParams['page'] : state.paginate_params.page,
                    per_page: queryParams['per_page'] ? queryParams['per_page'] : state.paginate_params.per_page });
            }

            if(queryParams['sorting_field'] && queryParams['sorting_order']) {
                commit(SET_SORTING_PARAMS, {sortBy: queryParams['sorting_field'], sortOrder: queryParams['sorting_order']});
            }
        },
        resetPaginateSortParams({commit}) {
            commit(SET_PAGINATE_PARAMS, {page: 1, per_page: 30});
            commit(SET_SORTING_PARAMS, {sortBy: 'updated_at', sortOrder: 'DESC'});
        },
        setSearchDefaults({commit, dispatch}) {
            dispatch('resetPaginateSortParams');

            commit(SET_SEARCH_KEYWORD, "");
            commit(SET_SEARCH_COUNTRY, "");
            commit(SET_SEARCH_STATE, "");
            commit(SET_SEARCH_CITY, "");
            commit(SET_SEARCH_STATUS, "");
            commit(SET_SEARCH_RENT_PER, "");
            commit(SET_SEARCH_PROPERTY_FINALIZING, "");
            commit(SET_SEARCH_FEATURES, []);

            const rangeFields = ["price", "rent", "bathrooms", "bedrooms", "area", "rooms", "garages", "units", "floor_number", "year_built"];
            rangeFields.map(field => {
                commit(SET_SEARCH_RANGE, {field, value: {min: 0, max: 0}});
            });
        }
    }

The “search” action performs the actual search by sending to the “/api/search” endpoint via axios and then update the properties state key using commit(SET_SEARCH_PROPERTIES, val).

The “bulkStoreUpdate” action will be used to update the store state from the url query string when for example refreshing the search page.

The “setSearchDefaults” action reset the search store, this action typically called before doing the search to reset the search state first before updating.

After making these updates add the final piece of buzzle in SearchSidebar.vue:

SearchSidebar.vue

First import useSubmitSearch.js

import {useSubmitSearch} from "../../composables/useSubmitSearch";

Then after those lines:

const onCityChange = e => {
        handleSelectCity(e);
        store.commit(`search/${SET_SEARCH_CITY}`, selectedCity.value);
      }

Add those lines:

const {submitSearch} = useSubmitSearch();

      const scrollTop = () => {
        document.querySelector('.properties-page').scrollIntoView({
          behavior: 'smooth'
        });
      }

And in the return statement return the submitSearch and scrollTop functions:

return {
            propertyFinalizingList,
            rentAmountPerList,
            propertyTypes,
            features,
            countries,
            states,
            cities,
            country,
            state,
            city,
            keyword,
            status,
            rentPer,
            baths,
            beds,
            price,
            rent,
            area,
            rooms,
            garages,
            units,
            floor_numbers,
            year_built,
            propertyFinalizing,
            onRangeChange,
            onSelectFeature,
            onUnselectFeature,
            onCountryChange,
            onStateChange,
            onCityChange,
            submitSearch,
            scrollTop
        }

Finally add on the template call the submitSearch() in the <form /> tag like so:

<form action="#" class=" form-inline" @submit.prevent="submitSearch(); scrollTop();">

Now we completed the search submission. In the next part we will display the search results.

Continue to part 11: Continue Search>>>

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments