UNPKG

vue-funnelback-component

Version:

A basic Funnelback implementation using Vue

341 lines (285 loc) 10.3 kB
import '../styles/index.scss'; import Vue from 'vue'; import VueRouter from 'vue-router'; import _ from 'lodash'; import axios from 'axios'; Vue.use(VueRouter); // Event listener API window.EventAPI = new Vue(); // The search component const FBSearch = Vue.component('funnelback-search', { data: function () { return { page: 1, nextStart: '0', searchTerm: this.$route.query.query, searchCount: 0, searchTotal: 0, noResults: false, fbParams: {}, urlParams: {}, } }, props: { url: { type: String, required: true, }, perPage: { type: String, default: 100, }, collection: { type: String, required: true, } }, created() { // Listen for end of screen scroll // TODO: look into v-scroll window.addEventListener('scroll', this.lazyLoad); // Recieve the apply filters event EventAPI.$on('apply-filters', this.search); // Fire the search if a term is supplied if (!_.isEmpty(this.searchTerm)) { this.search('onload'); } }, methods: { // ================================= // Fire the search very half a second // ================================= // on enter = new search - event (enter is pressed) // on scroll = lazy load - no param // on filter = new query // on page load search: function(event, filters = {}) { let self = this; let fbQuery = ''; let newSearch = (_.isUndefined(event)) ? false : true; let filteredSearch = (_.isEmpty(filters)) ? false : true; // Make sure there are more than 2 chars if (self.searchTerm.length < 3) { // Produce error self.noResults = true; return; } // For new & filtered... if (newSearch || filteredSearch) { // Set a fresh start self.nextStart = 1; // Build the search query let params = { query: this.searchTerm, num_ranks: this.perPage, start_rank: this.nextStart, collection: this.collection }; // Add/remove the filters self.fbParams = _.merge(params, filters); } // Make into a url string fbQuery = Object.keys(self.fbParams).map(key => key + '=' + self.fbParams[key]).join('&'); // Make a request to Funnelback axios.get(this.url + '?' + fbQuery) // handle success .then(function (response) { // Store the results let results = response.data.response; // Set the summary data let resultsSummary = results.resultPacket.resultsSummary; // Set the total self.searchTotal = resultsSummary.totalMatching; // Set the next start self.nextStart = resultsSummary.nextStart; // If there are no results if (self.searchTotal === 0) { // Set the page self.page = 1; // Set noResults to true self.noResults = true; // Empty the display EventAPI.$emit('display-results', {}); // If there are results } else { // Set noResults to false self.noResults = false; // Update the url self.updateUrl(fbQuery); // New search if (newSearch) { // Display the facets EventAPI.$emit('display-facets', results.facets); // Set the count self.searchCount = results.resultPacket.results.length; // Display the results EventAPI.$emit('display-results', results.resultPacket, false); // Filtered search } else if (filteredSearch) { // Set the count self.searchCount = results.resultPacket.results.length; // Display the results EventAPI.$emit('display-results', results.resultPacket, false); // Lazy loadind or page load } else { // Incremeant the page self.page++; // Display the results EventAPI.$emit('display-results', results.resultPacket, true); // Update the count self.searchCount = self.searchCount + results.resultPacket.results.length; } } }) .catch(function (error) { // handle error console.log(error); }); }, // ================================= // Lazy load: debounce // ================================= lazyLoad: _.debounce(function () { // Account for the footer and some const offSet = 1500; // Do nothing if there aren't more results if( _.isNull(this.nextStart) ) return; // Update the start rank to get next content this.fbParams.start_rank = this.nextStart; // When nearing the end of the screen load more results if((window.innerHeight + window.scrollY + offSet) >= document.body.offsetHeight) { this.search(); } }, 800), // ================================= // Update the URL // ================================= updateUrl(query) { // TODO: build a better url query // Update the router this.$router.push({ query: this.fbParams }).catch(err => { // console.log(err); console.log('solve this later'); }); }, }, template: ` <div class="fb-search" :class="{ 'no-results': noResults }"> <input class="form-control form-control-lg" type="search" placeholder="search" autocomplete="off" @keyup.enter="search" v-model.trim="searchTerm"> <span v-if="searchTotal" class="fb-search-count">Showing {{searchCount}} of {{searchTotal}} results</span> </div> ` }); // The result component Vue.component('funnelback-results', { data: function () { return { results: [], } }, created() { EventAPI.$on('display-results', this.displayResults); }, template: ` <transition-group name="list" class="fb-results" tag="div"> <article v-for="result in results" :key="result.rank" class="card result"> <div class="card-body"> <h4>{{result.title}}</h4> <p v-if="result.summary"> {{result.summary}} </p> <a :href="result.displayUrl">{{result.displayUrl}}</a> </div> </article> </transition-group> `, methods: { displayResults: function(data, concat) { if (_.isEmpty(data)) { this.results = []; } else { if (!concat) { this.results = []; } // Add the results to the display object data.results.forEach(result => this.results.push(result)); } } } }); // The factes Vue.component('funnelback-facets', { data: function () { return { filters: [], selectedFilters: [], } }, created() { EventAPI.$on('display-facets', this.displayFacets); }, template: ` <div class="fb-facets"> <div v-for="(filter, key) in filters"> <template v-if="filter.selectionType === 'SINGLE'"> <h4>{{filter.name}}</h4> <select class="form-control" @change="onChange($event)"> <option :selected="isSelected(filter)" :value="filter.unselectAllUrl">All</option> <option v-for="option in filter.allValues" :value="option.toggleUrl" :selected="option.selected"> {{option.label}} <span class="badge">{{option.count}}</span> </option> </select> </template> </div> </div> `, methods: { displayFacets(facets) { // Build a better facet data model this.filters = facets; }, // ================================= // Listen for facet change // ================================= onChange(event) { let urlParams = {}; let selectedValue = event.target.value.replace('?', '').split('&'); selectedValue.forEach( (item) => { let param = item.split('='); let key = decodeURIComponent(param[0]); let value = param[1]; urlParams[key] = value; }); // Apply the filters EventAPI.$emit('apply-filters', null, urlParams); }, // ================================= // If nothing else is selected // ================================= isSelected(filter) { filter.allValues.forEach((value) => { if(value.selected) { return false; } else { return true; } }); } }, }); // The init function export default function init() { // The routes const routes = [ { path: '/', component: FBSearch}, ] // Set up the vue router const router = new VueRouter({ routes // short for `routes: routes` }); // The main vue inatance new Vue({ el: '#funnelback_app', router, }); }