vue-funnelback-component
Version:
A basic Funnelback implementation using Vue
341 lines (285 loc) • 10.3 kB
JavaScript
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" .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" ="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,
});
}