Advanced Configuration

Extending templates

Overview
Using the Vue SDK package it is possible to apply templates to the used components and in this way to provide a custom layout. This feature allows minimal effort updates to create a whole different level of presentation and experience.

Steps to extend

  1. Open the widget initialization file (the one with HawksearchVue.createWidget)
  2. Import the component which is needed to be extended (e.g. ResultItem)
import { ResultItem } from '@hawksearch/vue';
  1. Set the template to a custom-created markup. This is a reference to a <script> element id.
ResultItem.templateOverride = "#custom-result-item";
  1. Create the markup for the component template override. It should be included anywhere on the page that includes the widget. This could probably be around the widget root element definition. Apply all necessary layout modifications.
<script id="custom-result-item" type="x-template">
    <div class="result-item">
        <p class="result-item-title">{{ getField('title') }}</p>
        <p class="result-item-description">{{ getField('description') }}</p>
    </div>
</script>
  1. Save, re-build and preview the page.

📘

In the case of ResultItem, this component has an access property getField() which exposes the result data fields.

Overriding components

Overview
All components existing in the Vue SDK are exposed and can be extended or overridden. This allows the logic and structure to be adjusted to needs of the widgets and to customize the overall behavior. Compared to extending templates, this is the more advanced approach to modify the component that includes not only layout, but also functionality and structure modifications.

Steps to extend components

  1. Create a components folder in the projects vue directory on the same level as the widgets folder
  2. Create a component file with .vue extension (e.g. ResultItem.vue) and open for edit. This file should follow the structure of Vue.js single file components
  3. In the
import { ResultItem } from '@hawksearch/vue';
  1. Make the required adjustments. For example creating a computed properties for some of the fields
export default {
    extends: ResultItem,
    computed: {
        title: function () {
            return this.getField('title');
        },
        content: function () {
            return this.getField('content');
        }
    }
};

Note that the package component is passed to the extends field

  1. Save the file. Now we can directly use the computed properties in the template
<div><h3>{{ title }}</h3></div>
<div>{{ content }}</div>
  1. Go through the steps for creating a widget (e.g. SPA)
  2. On top of the usual configuration, import the newly created component
import ResultItem from '../components/ResultItem';
  1. The ResultItem component is included in the ResultListing. At this point the used component is from the package and should be swapped with the new extended one
    1. Import the ResultListing from the package
    import { ResultListing } from '@hawksearch/vue';
    
    ii. Change the component’s reference
ResultListing.components.ResultItem = ResultItem;
  1. From here, the new custom component will be rendered instead of the default. Save, re-build and preview the page.

Synchronized widgets

Overview
Implementing a connection to the Hawksearch service with the Vue SDK presents a platform specific front-end behavior that enhances the structure of the page layout. More specifically, to this point point we created widgets that are self contained (i.e. SPA) or used through redirect (searchbox and results). Vue.js allows to create interconnected widgets on a single page even if they are created as entirely separate entities (different files, definition of root elements etc.). This so called common data layer allows the component to propagate data and events between them, in spite of their separation.

📘

Requirements

Several widget instances - for this demo will be used two searchboxes and two result sets

Steps to configure
Create four widgets: two search boxes and two result widgets. With them we will create two separate data layers searchbox-one → results-one and searchbox-two → results-two. The difference between the two data layers will be the targeted index name (e.g. index1, index2)

a. Template

<div class="search-header">
    <div style="display: inline-block;">
        <div id="searchbox-one">
            <search-box></search-box>
        </div>
    </div>

    <div style="display: inline-block;">
        <div id="searchbox-two">
            <search-box></search-box>
        </div>
    </div>
</div>

<div class="search-results">
    <div style="display: inline-block;">
        <div id="results-one">
            <search-box></search-box>
        </div>
    </div>

    <div style="display: inline-block;">
        <div id="results-two">
            <search-box></search-box>
        </div>
    </div>
</div>

Note there is a difference in the structure we’ve used so far. Instead of one root element with embedded components, there are for separate root elements. Placing them in one file doesn’t look like much of a difference, but this layout may be the product of a template engine or a CMS widget designer.

b. Widgets initialization

The widgets initialization can be separate, but for demo purposes we will create them all together

const config1 = {
    clientGuid: '9c2a78v6bc9977w0929w88rf576y4476',
    apiUrl: 'https://searchapi.hawksearch.net/api/v2/search/',
    dashboardUrl: 'https://dev.hawksearch.net/',
    indexName: 'index1'
}

const config2 = {
    clientGuid: '9c2a78v6bc9977w0929w88rf576y4476',
    apiUrl: 'https://searchapi.hawksearch.net/api/v2/search/',
    dashboardUrl: 'https://dev.hawksearch.net/',
    indexName: 'index2'
}

HawksearchVue.createWidget(document.getElementById('searchbox-one'), { config: config1, dataLayer: 'index1' });
HawksearchVue.createWidget(document.getElementById('results-one'), { config: config1, dataLayer: 'index1' });
HawksearchVue.createWidget(document.getElementById('searchbox-two'), { config: config2, dataLayer: 'index2' });
HawksearchVue.createWidget(document.getElementById('results-two'), { config: config2, dataLayer: 'index2'  });

Two different configurations are created for each data layer. After that the createWidget method is invoked on the dedicated elements and the widgets are paired two by two.

  1. Save, re-build and preview the page. Performing a query on the left search box will update only the left results widget and the right search box - only the right results.

📘

The same approach can be used for various use cases of the root components (<search-box>, <results>, <facet-list>)

Examples:

  • Separate widgets for search field, results and facets. Same behavior as SPA, but could be structurally more dynamic
  • Page with multiple result sections (i.e. FAQ) with several coupled search fields to results
  • Using duplicating widgets shown on condition - Facets in sidebar section on large resolution and in footer for smaller resolutions.

Usage of getField() method in ResultItem

Overview
The ResultItem component included in the Vue SDK is the one that usually gets extended to match the layout of the result data. The data is retrieved using the getField() method and here are some insights on how to use it more efficiently.

Standard usage
This the most basic usage of the getField() method. It can be used either in the ).

<h3>{{ getField('title') }}</h3>

This invocation also handles languages, if available and configured for the instance. For instance, the field name in the response for the title with enabled Spanish language will be returned in the response as title_es or title_fr for French. Regardless of the language used, the getField() requires only the stem of the field name, so it will retrieve the correct data. In this scenario keep the structure the same as if there isn’t a suffix for the fields.

<h3>{{ getField('title_en') }}</h3> // Not correct!

<h3>{{ getField('title') }}</h3> // Correct. Will fetch title_<language> data.

If the value is required to be used as an attribute value, rather than inline text, the Vue.js syntax applies.

<a :href="getField('url')">Read more</a>

Additional options

Multiple values

In most cases the retrieved data is a single value (title, url, etc.). In some scenarios the field has multiple values and the data must be parsed as an array to be displayed properly. This example will display the list of images from the thumbnails field.

<img v-for="imageSrc in getField('thumbnails', { parseAsArray: true })" src="imageSrc" />

Truncate long text
For fields that may contain unnecessary long text for this display, the getField() method can apply a truncate function.

// summary: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'

<span>{{ getField('summary', { truncateTo: 20 }) }}</span> // -> Lorem ipsum dolor ...

The truncateTo option gets the maximum allowed limit. If it is exceeded, the value is truncated to that limit, breaking on a whole word and adding ellipses points at the end.

Synchronous methods and hooks

Overview
The Vue SDK presents several methods and hooks that can be used for attaching additional behavior. Here is a list of them with a short sample on how to use them.

Track method
Each widget created via the HawksearchVue.createWidget() has an instance of the track event object. Every triggered tracking event is passed through the object’s track method. This usually happens asynchronously but in some cases, a set of actions must be executed exclusively after the tracking has been registered. For instance, clicking on a result item triggers a tracking event and opens the details page.

if (this.trackEvent) {
    this.trackEvent.track('click', {
        event: e,
        uniqueId: this.result.DocId,
        trackingId: this.getResponseField('TrackingId'),
        url: link
    });
}

location.assign(link);

Implementing the redirect to be immediately after the tracking method will result in the following modification:

if (this.trackEvent) {
    this.trackEvent.track('click', {
        event: e,
        uniqueId: this.result.DocId,
        trackingId: this.getResponseField('TrackingId'),
        url: link
    }).then(() => { location.assign(link); })
}

In essence - the track method returns a Promise which resolves at successful tracking event triggering.

Extending the Vuex store

Overview
Each widget rendered on a page using the Hawksearch Vue SDK has an attached Vuex store to it that holds the data and manages the state and behavior. It also enables creating a common data layer among widgets and is the main access point to the service itself. Being a vital part of the SDK this store has a set of specific functionalities that enable the Hawksearch features, but in some custom cases requires additional behavior based on the target project. This article presents the process of extending the provided Vuex store.

Steps to extend

  • Follow the steps for creating a standard widget - Creating widgets with Vue SDK
  • Open the widget initialization file to edit. The following code gives the basic concept on the extending process:
import HawksearchVue from '@hawksearch/vue';
import '@hawksearch/vue/dist/vue-hawksearch.css';

window.addEventListener('load', () => {
    const config = {
        clientGuid: '9c2f60a5bc5941f0929f92ef257s8r03',
        apiUrl: 'https://searchapi-dev.hawksearch.net/api/v2/search/'
    };

    var storeOverrides = {
        state: {
            extendedSearchOutput: null
        },
        mutations: {
            updateExtendedSearchOutput(state, value) {
                state.extendedSearchOutput = value;
            }
        },
        actions: {
            fetchExtendedResults({ commit, state }, searchParams) {
                setTimeout(() => {
                    var searchOutput = Object.assign({}, state.searchOutput);

                    if (searchOutput) {
                        searchOutput.Results = searchOutput.Results.map(item => {
                            Object.assign(item.Document, { "custom": ["Custom"] });
                            return item;
                        });

                        commit('updateExtendedSearchOutput', searchOutput);
                    }
                }, 2000);
            }
        }
    };

    // Provide the store overrides to the store instances generator
    var store = HawksearchVue.generateStoreInstance(config, storeOverrides);

    // Attach to an existing mutation. This is only necessary for the specefic case.
    const unsubscribe = store.subscribe((mutation, state) => {
        if (mutation.type == 'updateResults') {
            store.dispatch('fetchExtendedResults', {});
        }
    });

    // Create a widget with the extended store
    var widget = HawksearchVue.createWidget('#hawk-sample', { config, store });

    HawksearchVue.initialSearch(widget);
});
  • The initialization of the widget is switched to the advanced type (with provided store in the createWidget method)
  • The store itself is created prior to the widget
var store = HawksearchVue.generateStoreInstance(config, storeOverrides);

Providing a seconf paramter in generateStoreInstance adds store configuration on top of the default one - state, mutations, actions, getters.

  • In this sample a storeOverrides object is created containing all required modifications (line 10). This modification will create a new state extendedSearchOutput which will be populated with data of a dummy asynchronous method on top of the default provided searchOutput state. That new state will be later availble for all components in the SDK.
  • The action created for this custom purpose may be dispatched in any of the components, but in this case it is attached to another from the store - updateResults (line 41).

Store Variables & Extending Examples
We use vuex store in our SDK to handle all the results and facets. Below are the variables being used inside a whole application. We can call these actions to extend our results

export default {
    storeId: null,
    config: {},
    searchOutput: null,
    prevSearchOutput: null,
    suggestions: null,
    pendingSearch: {
        Keyword: "",
        FacetSelections: {}
    },
    extendedSearchParams: {},
    searchError: false,
    loadingResults: false,
    loadingSuggestions: false,
    waitingForInitialSearch: true,
    searchCancelation: null,
    autocompleteCancelation: null,
    language: null,
    isFirstInitialSearch: true,
    initialSearchUrl: null
};
  • We can create the custom actions by following Extending the Vuex store | Steps to extend.
  • We can use these actions to extend the results, change the keyword/facets (programmatically), handle loading etc.
  • We can also add the extending results in the custom components Overriding components.

Example:
We can create a custom component like (Example.vue):

<template>
    <div>
        <div>
            {{ extendedSearchOutput }}
        </div>
    </div>
</template>

<script>
import { ResultItem } from '@hawksearch/vue';
export default {
        name: 'result-items',
        extends: ResultItem,
        computed: {
            searchOutput: function () {
                return this.$store.state.searchOutput.Results
            },
            extendedSearchOutput: function () {
                return this.$store.state.extendedSearchOutput
            },
            title: function () {
                return this.getField('title');
            },
            content: function () {
                return this.getField('content');
            }
        }
    };
</script>

And update the config file for custom component by adding

...
import { ResultListing } from '@hawksearch/vue';

window.addEventListener('load', () => {
    ResultListing.components.ResultItem = ResultItem
    ...

Now you can see the rendered data from searchOutput to our own created extendedSearchOutput inside a custom component.