diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index d43610bb7..000000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.circleci/config.yml b/.circleci/config.yml index 1dbe8422b..2110b1f49 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -93,11 +93,14 @@ jobs: subdirectory: '/${CIRCLE_TAG}' cache-control: 'max-age=31536000' - run: - name: "Get minor version substring" + name: "Get major and minor version substrings" command: | echo "export MINOR_VERSION="$(echo "${CIRCLE_TAG}" | cut -d '.' -f 1,2)"" >> $BASH_ENV + echo "export MAJOR_VERSION="$(echo "${CIRCLE_TAG}" | cut -d '.' -f 1)"" >> $BASH_ENV - deploy-to-aws: subdirectory: '/${MINOR_VERSION}' + - deploy-to-aws: + subdirectory: '/${MAJOR_VERSION}' workflows: version: 2 build_and_deploy: diff --git a/.gitignore b/.gitignore index 073e678a5..5058fbbff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ app/ -dist/ \ No newline at end of file +dist/ +**/.DS_Store \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..6d3791e82 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, Yext +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 3877c6902..98599256b 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,17 @@ Answers Javascript API Library. Outline: 1. [Install / Setup](#install-and-setup) - - [Configuration Options](#configuration-options) - - [Navigation Configuration](#navigation-configuration) - - [Template Helpers](#template-helpers) -2. [Component Usage](#component-usage) +2. [ANSWERS.init Configuration Options](#answersinit-configuration-options) + - [Vertical Pages Configuration](#vertical-pages-configuration) + - [Search Configuration](#search-configuration) + - [Vertical No Results Configuration](#vertical-no-results-configuration) + - [onVerticalSearch Configuration](#onverticalsearch-configuration) + - [onUniversalSearch Configuration](#onuniversalsearch-configuration) +3. [Component Usage](#component-usage) - [Base Component Configuration](#base-component-configuration) - - [Adding a Component](#adding-a-component) - - [Using a Custom Renderer](#using-a-custom-renderer) - - [Custom Data Formatting](#custom-data-formatting) - - [Custom Data Transforms](#custom-data-transforms) - - [Creating Custom Components](#creating-custom-components) - - [Using a Custom Template](#using-a-custom-template) + - [Adding a Component](#adding-a-component-to-your-page) - [Removing Components](#removing-components) -3. [Types of Components](#types-of-components) +4. [Types of Built-in Components](#types-of-built-in-components) - [SearchBar Component](#searchbar-component) - [DirectAnswer Component](#direct-answer-component) - [UniversalResults Component](#universal-results-component) @@ -29,54 +27,106 @@ Outline: - [SpellCheck Component](#spell-check-component) - [LocationBias Component](#location-bias-component) - [SortOptions Component](#sort-options-component) -4. [Analytics](#analytics) - - [Click Analytics](#click-analytics) + - [Map Component](#map-component) +5. [Customizing Components](#customizing-components) + - [Using a Custom Renderer](#using-a-custom-renderer) + - [Custom Data Formatting](#custom-data-formatting) + - [Custom Data Transforms](#custom-data-transforms) + - [Using a Custom Template for a Component](#using-a-custom-template-for-a-component) + - [Creating Custom Components](#creating-custom-components) +6. [Template Helpers](#template-helpers) +7. [Analytics](#analytics) + - [Custom Analytics Using JavaScript](#custom-analytics-using-javascript) + - [Custom Analytics Using Data Attributes](#custom-analytics-using-data-attributes) + - [Conversion Tracking](#conversion-tracking) + - [On-Search Analytics](#on-search-analytics) +8. [Rich Text Formatting](#rich-text-formatting) + # Install and Setup -To include the answers base CSS (optional). +The Answers Javascript API Library does not need to be installed locally. Instead, it can be used +with script tags on a webpage. The instructions below explain how to do this; they will walk you through +adding the Answers stylesheet, JS library, and an intialization script to an HTML page. +After doing this, you can view your page in the browser. + +Include the Answers CSS ```html ``` -Adding the Javascript library +Add the Javascript library and placeholder elements for [Answers components](#component-usage). ```html - + + +
+
``` +Add an initialization script with an apiKey, experienceKey and onReady function. In the example below, we've initialized two +basic components: [SearchBar](#searchbar-component) and [UniversalResults](#universal-results-component). ```js function initAnswers() { ANSWERS.init({ - apiKey: '', + apiKey: '', // See [1] experienceKey: '', onReady: function() { - // Component creation logic here + ANSWERS.addComponent('SearchBar', { + container: '#SearchBarContainer', + }); + + ANSWERS.addComponent('UniversalResults', { + container: '#UniversalResultsContainer', + }); } }) } ``` -Learn more about [getting you API key](https://developer.yext.com/docs/guides/get-started/). - -## Configuration Options -Below is a list of configuration options that can be used during initialization. - -| option | type | description | required | -|-----------|------------|-------------------------------------------|---------------| -| apiKey | string | Your API key | required | -| experienceKey | string | The key used for your answers experience | required | -| businessId | number | Knowledge Graph businessId to use for analytics, required to send analytics events | optional -| onReady | function | Invoked when the Answers component library is loaded/ready | required | -| onStateChange | function | Invoked when the sate changes | optional | -| useTemplates | boolean | default: `true`. If false, don't fetch pre-made templates. Only use this if you plan to implement custom renders for every component! | optional | -| templateBundle | object | Provide the precompiled templates | optional | -| search | object | Search specific settings, see [search configuration](#search-configuration) below | optional | -| locale | string | The locale of the configuration. The locale will affect how queries are interpreted and the results returned. The default locale value is 'en'. | optional | -| experienceVersion | string or number | The Answers Experience version to use for api requests | optional | -| debug | boolean | Prints full Answers error details when set to `true` | optional | -| sessionTrackingEnabled | boolean | default: `true`. If true, the search session is tracked. If false, there is no tracking. | optional | -| navigation | object | Provide navigation configuration including tab configurations | optional | -| onVerticalSearch | function | analytics callback after a vertical search | optional | -| onUniversalSearch | function | analytics callback after a universal search | optional | +[1] Learn more about [getting your API key](https://developer.yext.com/docs/guides/get-started/). + +# ANSWERS.init Configuration Options + +The configuration provided here is configuration that is shared across components. + +```js +function initAnswers() { + ANSWERS.init({ + // Required, your Yext Answers API key + apiKey: '', + // Required, the key used for your Answers experience + experienceKey: '', + // Required, initialize components here, invoked when the Answers component library is loaded/ready + onReady: function() {}, + // Optional*, Yext businessId, *required to send analytics events + businessId: 'businessId', + // Optional, if false, the library will not fetch pre-made templates. Only use change this to false if you provide a + // template bundle in the `templateBundle` config option or implement custom renders for every component + useTemplates: true, + // Optional, additional templates to register with the renderer + templateBundle: {}, + // Optional, provide configuration for each vertical that is shared across components, see Vertical Pages Configuration below + verticalPages: [], + // Optional, search specific settings, see Search Configuration below + search: {}, + // Optional, vertical no results settings, see Vertical No Results below + search: {}, + // Optional, the locale will affect how queries are interpreted and the results returned. Defaults to 'en'. + locale: 'en', + // Optional, the Answers Experience version to use for api requests + experienceVersion: 'PRODUCTION', + // Optional, prints full Answers error details when set to `true`. Defaults to false. + debug: false, + // Optional, If true, the search session is tracked. If false, there is no tracking. Defaults to true. + sessionTrackingEnabled: true, + // Optional, invoked when the state of any component changes + onStateChange: function() {}, + // Optional, analytics callback after a vertical search, see onVerticalSearch Configuration for additional details + onVerticalSearch: function() {}, + // Optional, analytics callback after a universal search, see onUniversalSearch Configuration for additional details + onUniversalSearch: function() {}, + }) +} +``` ## Vertical Pages Configuration Below is a list of configuration options related to vertical pages in navigation and no results, used in the [base configuration](#configuration-options) above. @@ -84,55 +134,65 @@ Below is a list of configuration options related to vertical pages in navigation ```js verticalPages: [ { - label: 'Home', // The label used for the navigation element - url: './index.html', // The link for the navigation element - isFirst: true, // optional, will always show this item first - isActive: true, // optional, will add a special class to the item - icon: 'star', // optional, the icon to use in no results and universal results, defaults to star - hideInNavigation: true // optional, hide this tab in the navigation component if it’s been added, defaults to false - + // Required, the label for this page + label: 'Home', + // Required, the link to this page + url: './index.html', + // Optional*, the verticalKey, *required for vertical pages (must omit this property for universal) + verticalKey: 'locations', + // Optional, the icon name to use in no results, defaults to no icon + icon: 'star', + // Optional, the URL icon to use in no results, defaults to no icon + iconUrl: '', + // Optional, if true, will show this page first in the Navigation Component, defaults to false + isFirst: false, + // Optional, if true, will add a special styling to this page in the Navigation Component, defaults to false + isActive: false, + // Optional, if true, hide this tab in the Navigation Component, defaults to false + hideInNavigation: false, }, - { - verticalKey: 'locations', // optional, the vertical search config id - label: 'Location', // The label used for the navigation element - url: 'locations.html' // The link for the navigation element - } + ... ] ``` ## Search Configuration Below is a list of configuration options related to search, used in the [base configuration](#configuration-options) above. -| option | type | description | required | -|-----------|------------|-------------------------------------------|---------------| -| verticalKey | string | The vertical key to use for searches | optional | -| limit | number | The number of results to display per page | optional | -| defaultInitialSearch | string | A default search to use on initialization for vertical searchers, when the user has't provided a query | optional | +```js + { + // Optional, the vertical key to use for searches + verticalKey: 'verticalKey', + // Optional, the number of results to display per page, defaults to 20 + limit: 20, + // Optional, Vertical Pages only, a default search to use on page load when the user hasn't provided a query + defaultInitialSearch: 'What is Yext Answers?', + }, +``` -## Template Helpers -When using handlebars templates, Answers ships with a bunch of pre-built template helpers that you can use. You can learn more about them [here](https://github.com/jonschlinkert/template-helpers). +## Vertical No Results Configuration +Below is a list of configuration options related to no results on Vertical Pages, used in the [base configuration](#configuration-options) above. -If you want to register custom template helpers to the handlebars render, you can do so like this: ```js -ANSWERS.registerHelper('noop', function(options) { - return options.fn(this); -}) + { + // Optional, whether to display all results for the Vertical when a query has no results, defaults to false + displayAllResults: false, + // Optional, a custom template for the no results card + template: '', + }, ``` -## onSearch Analytics +## onVerticalSearch Configuration + +The onVerticalSearch Configuration is a function, used in the [base configuration](#configuration-options) above. -Both onVerticalSearch and onUniversalSearch allow you to send an analytics event each time a search is run. -These options expect functions that take in one parameter, which contains information about the search, -and also return the desired analytics event. +It allows you to send an analytics event each time a search is run on a Vertical page. This function should take in one parameter, `searchParams`, which contains information about the search, and return the desired analytics event. -The search information exposed to both options is shown below. +Like all Answers Javascript API Library analytics, this will only work if there is a businessId in the ANSWERS.init. -#### onVerticalSearch +The search information exposed in `searchParams` is shown below. ```js -ANSWERS.addComponent('SearchBar', { - container: '.search-container', - onVerticalSearch: searchParams => { +function (searchParams) => { /** * Vertical key used for the search. * @type {string} @@ -155,23 +215,34 @@ ANSWERS.addComponent('SearchBar', { * Either 'normal' or 'no-results'. * @type {string} */ - const resultsContext = searchParams.resultsCount - const analyticsEvent = { - type: 'ANALYTICS_EVENT_TYPE', + const resultsContext = searchParams.resultsContext; + + let analyticsEvent = new ANSWERS.AnalyticsEvent('ANALYTICS_EVENT_TYPE'); + analyticsEvent.addOptions({ label: 'Sample analytics event', - query: queryString - }; + searcher: 'VERTICAL', + query: queryString, + resultsCount: resultsCount, + resultsContext: resultsContext, + }); return analyticsEvent; }, -}) +}), ``` -#### onUniversalSearch +## onUniversalSearch Configuration + +The onUniversalSearch Configuration is a function, used in the [base configuration](#configuration-options) above. + +It allows you to send an analytics event each time a search is run +on a Universal page. This function should take in one parameter, `searchParams`, which contains information about the +search, and return the desired analytics event. + +Like all Answers Javascript API Library analytics, this will only work if there is a businessId in the ANSWERS.init. +The search information exposed in `searchParams` is shown below. ```js -ANSWERS.addComponent('SearchBar', { - container: '.search-container', - onVerticalSearch: searchParams => { +function (searchParams) => { /** * The string being searched for. * @type {string} @@ -183,42 +254,57 @@ ANSWERS.addComponent('SearchBar', { * @type {number} */ const sectionsCount = searchParams.sectionsCount; - const analyticsEvent = { + + let analyticsEvent = new ANSWERS.AnalyticsEvent('ANALYTICS_EVENT_TYPE'); + analyticsEvent.addOptions({ type: 'ANALYTICS_EVENT_TYPE', label: 'Sample analytics event', + searcher: 'UNIVERSAL', query: queryString - }; + sectionsCount: sectionsCount, + }); return analyticsEvent; }, -}) +}), ``` -You can learn more about the interface for registering helpers by taking a look at the [Handlebars Block Helpers](https://handlebarsjs.com/block_helpers.html) documentation. - # Component Usage The Answers Component Library exposes an easy to use interface for adding and customizing various types of UI components on your page. -Every component requires a containing HTML element. +## What is a Component? + +At a high level, components are the individual pieces of an Answers page. The Answers Javascript API Library comes with many types of components. Each component is an independent, reusable piece of code. A component fills an HTML element container that the implementer provides on the page. Components are updated from their config, the config from the ANSWERS.init, and potentially an API response. + +Each type of Component has its own custom configurations. Additionally, all components share the base configuration options defined above. We will provide a brief description below of what each component does, along with describing how it can be configured. ## Base Component Configuration Every component has the same base configuration options. - -| option | type | description | required | -|-----------|------------|-------------------------------------------|---------------| -| name | string | a unique name, if using multiple components of the same type | optional | -| container | string | the CSS selector to append the component. | required | -| class | string | a custom class to apply to the component | optional | -| template | string | override internal handlebars template | optional | -| render | function | override render function. data provided | optional | -| transformData | function | A hook for transforming data before it gets sent to render | optional | -| onMount | function | invoked when the HTML is mounted to the DOM | optional | -| analyticsOptions | object | additional properties to send with every analytics event | optional | +```js + { + // Required, the selector for the container element where the component will be injected + container: 'container', + // Optional, a unique name for the component + name: 'name', + // Optional, a custom HTML classname for the component + class: 'class', + // Optional, handlebars template or HTML to override built-in handlebars template for the component + template: 'template', + // Optional, override render function + render: function(data) {}, + // Optional, a hook for transforming data before it gets sent to render + transformData: function(data) {}, + // Optional, invoked when the HTML is mounted to the DOM, note, this overrides any built-in onMount function for a component + onMount: function(data) {}, + // Optional, additional properties to send with every analytics event + analyticsOptions: {}, + } +``` -## Adding a Component +## Adding a Component to Your Page Adding a component to your page is super easy! You can add many different [types](#types-of-components) of components to your page. Each component supports the base configuration options above, as well as their own unique configurations. @@ -231,168 +317,27 @@ To start, every component requires an HTML container. Then, you can add a component to your page through the ANSWERS add interface. You need to call `addComponent` from `onReady`. -This is an example of the `SearchBar`. See [Types of Components](#types-of-components) below. - -```js -ANSWERS.addComponent('SearchBar', { - container: '.search-container', - // -- other options -- -}) -``` - -## Using a Custom Renderer - -If you want to use a use your own template language (e.g. soy, mustache, groovy, etc), -you should NOT use the template argument. Instead, you can provide a custom render function to the component. +This is an example of the `SearchBar`. See [Types of Built-in Components](#types-of-built-in-components) below. ```js ANSWERS.addComponent('SearchBar', { - container: '.search-container', - render: function(data) { - // Using native ES6 templates -- but you can replace this with soy, - // or any other templating language as long as it returns a string. - return `` - } + container: '.search-container' }) ``` -## Custom Data Formatting - -You can format specific entity fields using `fieldFormatters`. -These formatters are applied before the `transformData` step. - -Each formatter takes in an object with the following properties : -- `entityProfileData` -- `entityFieldValue` -- `highlightedEntityFieldValue` -- `verticalId` -- `isDirectAnswer` - -Below is an example usage. -```js -ANSWERS.init({ - apiKey: '', - experienceKey: '', - fieldFormatters: { - 'name': (formatterObject) => formatterObject.entityFieldValue.toUpperCase(), - 'description' : (formatterObject) => formatterObject.highlightedEntityFieldValue - } -}); -``` - -## Custom Data Transforms - -If you want to mutate the data thats provided to the render/template before it gets rendered, -you can use the `transformData` hook. - -All properties and values that you return from here will be accessible from templates. - - -```js -ANSWERS.addComponent('SearchBar', { - container: '.search-container', - transformData: (data) => { - // Extend/overide the data object - return Object.assign({}, data, { - title: data.title.toLowerCase() - }) - }, - render: function(data) { - // Using native ES6 templates -- but you can replace this with soy, - // or any other templating language as long as it returns a string. - return `` - } -}) -``` - -## Creating Custom Components - -You can create custom Answers components with the same power of the builtin components. First, create a subtype of ANSWERS.Component: - -```js -// ES6 -class MyCustomComponent extends ANSWERS.Component { - constructor (config) { - super(config); - - this.myProperty = config.myProperty; - } - - static defaultTemplateName () { - return 'default'; - } - - static areDuplicateNamesAllowed () { - return false; - } - - static get type () { - return 'MyCustomComponent'; - } -} - -// ES5 -function MyCustomComponent (config) { - ANSWERS.Component.call(this, config); - - this.myProperty = config.myProperty; -} - -MyCustomComponent.prototype = Object.create(ANSWERS.Component.prototype); -MyCustomComponent.prototype.constructor = MyCustomComponent; -MyCustomComponent.defaultTemplateName = function () { return 'default' }; -MyCustomComponent.areDuplicateNamesAllowed = function () { return false }; -Object.defineProperty(MyCustomComponent, 'type', { get: function () { return 'MyCustomComponent' } }); -``` - -Then, you can register your custom component with Answers: - -```js -ANSWERS.registerComponentType(MyCustomComponent); -``` - -Now you can use your custom component like any builtin component: - -```js -ANSWERS.addComponent('MyCustomComponent', { - container: '.my-component-container', - myProperty: 'my property' -}); -``` - -## Using a Custom Template -All component templates are written using handlebars. - -It's easy to override these templates with your own templates. -Keep in mind, that you must provide valid handlebars syntax here. +## Removing Components +If you'd like to remove a component and all of its children, you can do it. Simply `ANSWERS.removeComponent()`: ```js -// Use handlebars syntax to create a template string -let customTemplate = `` - ANSWERS.addComponent('SearchBar', { container: '.search-container', - template: customTemplate + name: 'MySearchBar' }) -``` - -## Removing Components -If you'd like to remove a component and all of its children, you can use `ANSWERS.removeComponent()`: - -```js -ANSWERS.addComponent('FilterSearch', { - container: '.filter-search-container', - verticalKey: 'myvertical', - name: 'my-filter-search' -}); -ANSWERS.removeComponent('my-filter-search'); +ANSWERS.removeComponent('MySearchBar'); ``` -# Types of Components - -Each type of Component has it's own custom configurations. However, all components share the -base configuration options defined above. +# Types of Built-in Components ## SearchBar Component @@ -403,46 +348,60 @@ types their query, as well as the autocomplete behavior.
``` -There are two types of search experiences. Universal Search and Vertical Search. -Each provide a different way of auto complete. - -### For Universal Search: - -```js -ANSWERS.addComponent('SearchBar', { - container: '.search-query-container', - title: 'Search my Brand', // optional, title is not present by default - query: 'query', // optional, the initial query string to use for the input box - labelText: 'What are you looking for?', // optional, defaults to 'Conduct a search' - submitText: 'Submit', // optional, used for labeling the submit button, also provided to the template - clearText: 'Clear', // optional, used for labeling the clear button, also provided to the template - submitIcon: 'iconName', // optional, used to specify a different built-in icon for the submit button - customIconUrl: 'path/to/icon', // optional, a url for a custom icon for the submit button - promptHeader: 'Header', // optioanl, the query text to show as the first item for auto complete - placeholderText: 'Start typing...', // optional, no default - autoFocus: true, // optional, defaults to false - autoCompleteOnLoad: false, // optional, when auto focus on load, optionally open the autocomplete - searchCooldown: 2000, // optional, defaults to 300ms (0.3 seconds) - promptForLocation: true, // optional, asks the user for their geolocation when "near me" intent is detected - clearButton: true, // optional, displays an "x" button to clear the current query when true - redirectUrl: 'path/to/url', // optional, redirect search query to url - formSelector: 'form', // optional, defaults to native form node within container - inputEl: '.js-yext-query' // optional, the input element used for searching and wires up the keyboard interaction -}) -``` +If the `verticalKey` config option is omitted, the SearchBar will perform Universal searches. Universal +searches return results across multiple Verticals; Vertical searches search within one Vertical. Additionally, Universal search and Vertical search provide a different way of auto complete. -### For Vertical Search: ```js ANSWERS.addComponent('SearchBar', { + // Required, the selector for the container element where the component will be injected container: '.search-query-container', - verticalKey: '' // required + // Required* for Vertical pages, omit for Universal pages + verticalKey: '', + // Optional, title is not present by default + title: 'Search my Brand', + // Optional, the initial query string to use for the input box + query: 'query', + // Optional, defaults to 'Conduct a search' + labelText: 'What are you looking for?', + // Optional, used for labeling the submit button, also provided to the template + submitText: 'Submit', + // Optional, used for labeling the clear button, also provided to the template + clearText: 'Clear', + // Optional, used to specify a different built-in icon for the submit button + submitIcon: 'iconName', + // Optional, a url for a custom icon for the submit button + customIconUrl: 'path/to/icon', + // Optional, the query text to show as the first item for auto complete + promptHeader: 'Header', + // Optional, no default + placeholderText: 'Start typing...', + // Optional, defaults to false + autoFocus: true, + // Optional, when auto focus on load, open the autocomplete + autoCompleteOnLoad: false, + // Optional, defaults to 300ms (0.3 seconds) + searchCooldown: 2000, + // Optional, asks the user for their geolocation when "near me" intent is detected + promptForLocation: true, + // Optional, displays an "x" button to clear the current query when true + clearButton: true, + // Optional, redirect search query to url + redirectUrl: 'path/to/url', + // Optional, defaults to native form node within container + formSelector: 'form', + // Optional, defaults to true. When true, a form is used as the query submission context. + // Note that WCAG compliance is not guaranteed if a form is not used as the context. + useForm: 'true', + // Optional, the input element used for searching and wires up the keyboard interaction + inputEl: '.js-yext-query' }) ``` ## Direct Answer Component -The Direct Answer Component will render the BEST result, if found, -based on the query. +This component is for Universal pages only. + +The Direct Answer Component will render the BEST result, if found, based on the query. ```html
@@ -450,14 +409,22 @@ based on the query. ```js ANSWERS.addComponent('DirectAnswer', { + // Required, the selector for the container element where the component will be injected container: '.direct-answer-container', - formEl: '.js-directAnswer-feedback-form', // optional, the form used for submitting the feedback - thumbsUpSelector: '.js-directAnswer-thumbUp', // optional, the selector to bind ui interaction to for reporting - thumbsDownSelector: '.js-directAnswer-thumbDown', // optional, the selector to bind ui interaction to for reporting - viewDetailsText: 'View Details', // optional, the display text for the View Details click to action link - positiveFeedbackSrText: 'This answered my question', //optional, the screen reader text for positive feedback on the answer - negativeFeedbackSrText: 'This did not answer my question', //optional, the screen reader text for negative feedback on the answer - footerTextOnSubmission: 'Thank you for your feedback!' //optional, the footer text to display on submission of feedback + // Optional, the selector for the form used for submitting the feedback + formEl: '.js-directAnswer-feedback-form', + // Optional, the selector to bind ui interaction to for reporting + thumbsUpSelector: '.js-directAnswer-thumbUp', + // Optional, the selector to bind ui interaction to for reporting + thumbsDownSelector: '.js-directAnswer-thumbDown', + // Optional, the display text for the View Details click to action link + viewDetailsText: 'View Details', + // Optional, the screen reader text for positive feedback on the answer + positiveFeedbackSrText: 'This answered my question', + // Optional, the screen reader text for negative feedback on the answer + negativeFeedbackSrText: 'This did not answer my question', + // Optional, the footer text to display on submission of feedback + footerTextOnSubmission: 'Thank you for your feedback!' }) ``` @@ -472,104 +439,37 @@ The most complex component has a ton of overridable configuration options.
``` -### Basic Component - -```js -ANSWERS.addComponent('UniversalResults', { - container: '.universal-results-container', - // The max number of search results to return, defaults to 10 - limit: 5, -}) -``` - -### Custom Render for ALL Result Items - -You can override the render function for EACH item in the result list, -as apposed to the entire component. - - -```js -ANSWERS.addComponent('UniversalResults', { - container: '.universal-results-container', - renderItem: function(data) { - return `my item ${data.name}` - } -}) -``` - -### Custom Template for ALL Result Items - -You can override the handlebars template for EACH item in the result list, -as apposed to the entire component. - -```js -ANSWERS.addComponent('UniversalResults', { - container: '.universal-results-container', - itemTemplate: `my item {{name}}` -}) -``` - -### Custom Render For Specific Vertical Result Items - -You can override the render function for a particular section within the results list, -by providing a vertical search config id as the context, and using the same options as above. - ```js ANSWERS.addComponent('UniversalResults', { + // Required, the selector for the container element where the component will be injected container: '.universal-results-container', + // Optional, configuration for each vertical's results config: { - 'locations': { // The vertical search config id - renderItem: function(data) { - return `my item ${data.name}`; - } - } - } -}) -``` - -### Custom options for specific Vertical Results - -You can also provide several config options to each vertical. -These are the supported options: - -```js -ANSWERS.addComponent('UniversalResults', { - container: '.universal-results-container', - config: { - 'locations': { // The vertical search config id - renderItem: function(data) { - return `my item ${data.name}`; - }, - // Specific text for the view all button, which links to the vertical search for this vertical. - // Default is no text. + 'locations': { // The verticalKey + // Optional, whether to use the AccordionResults component instead of VerticalResults for this vertical + useAccordion: false, + // Optional, text for the view all links to the vertical page for this vertical. Default is no text viewAllText: "Go to this vertical's search", - // Whether to include a map with this vertical's results. + // Optional, whether to include a map with this vertical's results, defaults to false includeMap: true, - // If includeMap is true, mapconfig that contains a mapProvider and apiKey is required + // Optional*, if includeMap is true, this is required mapConfig: { - // Either 'mapbox' or 'google' + // Required, either 'mapbox' or 'google', not case sensitive mapProvider: 'google', - // Api key for the map provider + // Required, API key for the map provider apiKey: '<<< enter your api key here >>>', - } - } - } -}) -``` - -### Custom Template For Specific Vertical Result Items - -You can override the handlebars template for a particular section within the results list, -by providing a vertical search config id as the context, and using the same options as above. - -```js -ANSWERS.addComponent('UniversalResults', { - container: '.universal-results-container', - config: { - 'locations': { // The vertical search config id - itemTemplate: `my item {{name}}` + // ... Optional, any other config for the Map Component, find more info in the section "Map Component" + }, + // Optional, override the render function for each result in this vertical + renderItem: function(data) {}, + // Optional, override the handlebars template for each item in this vertical + itemTemplate: `my item {{name}}`, } - } + }, + // Optional, override the render function for each item in the result list + renderItem: function(data) {}, + // Optional, override the handlebars template for each item in the result list + itemTemplate: `my item {{name}}`, }) ``` @@ -585,31 +485,32 @@ You define all the options at the top level object. ```js ANSWERS.addComponent('VerticalResults', { + // Required, the selector for the container element where the component will be injected container: '.vertical-results-container', - // Optional: function to give each result item custom rendering + // Optional, function to give each result item custom rendering renderItem: () => {}, - // Optional: string to give custom template to result item + // Optional, string to give custom template to result item itemTemplate: `
Custom template
`, - // Set a maximum number of columns that will display at the widest breakpoint. - // Possible values are 1, 2, 3 or 4. defaults to 1 - maxNumberOfColumns: 3, - // Whether to display the total number of results, default true + // Optional, set a max number of columns to display at the widest breakpoint. Possible values are 1, 2, 3 or 4, defaults to 1 + maxNumberOfColumns: 1, + // Optional, whether to display the total number of results, default true showResultCount: true, - // The card used to display each individual result, see the Cards section for more details, + // Optional, a modifier that will be appended to a class on the results list like this `yxt-Results--{modifier}` + modifier: '', + // Optional, the card used to display each individual result, see the Cards section for more details, card: { - // Optional: The type of card, currently only 'Standard', 'Accordion', and 'Legacy' are supported, defaults to 'Standard' + // Optional, The type of card, built-in types are: 'Standard', 'Accordion', and 'Legacy'. Defaults to 'Standard' cardType: 'Standard', - // Required, see Data Mappings for more details + // Optional, see Data Mappings for more details dataMappings: () => {}, - // Optional, used as configuration for any calls to action buttons on the page, see Calls To Action for more details + // Optional, see Calls To Action for more details callsToAction: () => [] }, - // Configuration for what to display when no results are found. + // Optional, configuration for what to display when no results are found. noResults: { - // You can specify a custom template for the no results card, otherwise will use the built-in template. + // Optional, used to specify a custom template for the no results card, defaults to a built-in template. template: '
No results found! Try again?
', - // Whether to display all results in the vertical when no results are found. Defaults to false. In which - // case only the no results card will be shown. + // Optional, whether to display all results in the vertical when no results are found. Defaults to false, in which case only the no results card will be shown. displayAllResults: false } }) @@ -644,7 +545,8 @@ Note: A CTA without both a label and icon will not be rendered. const callsToAction = [{ // Label below the CTA icon, default null label: 'cta label', - // Icon name for the CTA that is one of the SDK icons, default to undefined (no icon) + // Icon name for the CTA that is one of the built-in icons, defaults to undefined (no icon). If your icon + // is not recognized it will default to 'star'. icon: 'star', // URL to a custom icon for the cta. This takes priority over icon if both are present, default is // no icon url. @@ -665,7 +567,7 @@ const callsToAction = [{ // 'BOOK_APPOINTMENT', // 'RSVP' analytics: 'CTA_CLICK', - // The target attribute for the CTA link, defaults to '_self'. To open in a new window use '_blank' + // The target attribute for the CTA link, defaults to '_blank'. To open in a new window use '_blank' target: '_blank', // The eventOptions needed for the event to fire. Either a valid json string, an object, or a function that // takes in the result data response. @@ -793,7 +695,7 @@ ANSWERS.addComponent('VerticalResults', { ## Standard Card -The data mappings for a standard card has these attributes +The data mappings for a Standard Card has these attributes ```js const dataMappings = item => { @@ -828,7 +730,7 @@ const dataMappings = item => { ## Accordion Card -The data mappings for an accordion card has these attributes +The data mappings for an Accordion Card has these attributes ```js const dataMappings = item => { @@ -848,7 +750,8 @@ const dataMappings = item => { ## Legacy Card The Legacy Card is very similar to the Standard Card, but with the legacy DOM structure and class names -from before v0.13.0. New users should not use the Legacy Card; instead, use the Standard Card. +from before v0.13.0. New users should not use the Legacy Card; instead, use the Standard Card. Features +added after v0.13.0 may not work with the Legacy Card. The data mappings for a legacy card has these attributes @@ -876,7 +779,9 @@ const dataMappings = item => { ## Pagination Component -The Pagination component allows users to page through vertical search results. Pagination requires verticalKey to be provided in the [base configuration](#configuration-options). +This component is only for Vertical pages. + +The Pagination component allows users to page through vertical search results. ```html
@@ -884,18 +789,23 @@ The Pagination component allows users to page through vertical search results. P ```js ANSWERS.addComponent('Pagination', { + // Required, the selector for the container element where the component will be injected container: '.pagination-component', - // Display a double arrow allowing users to jump to the first page of results + // Required*, the vertical for pagination, *if omitted, will fall back to the search base config + verticalKey: 'verticalKey', + // Optional, display a double arrow allowing users to jump to the first page of results showFirst: true, - // Display a double arrow allowing users to jump to the last page of results + // Optional, display a double arrow allowing users to jump to the last page of results showLast: true, - // Label for a page of results + // Optional, label for a page of results pageLabel: 'Page' }); ``` ## FilterBox Component +This component is only for Vertical pages. + The FilterBox component shows a list of filters to apply to a search. ```html @@ -904,8 +814,9 @@ The FilterBox component shows a list of filters to apply to a search. ```js ANSWERS.addComponent('FilterBox', { + // Required, the selector for the container element where the component will be injected container: '.filters-container', - // List of filter component configurations + // Required, list of filter component configurations filters: [ { type: 'FilterOptions', @@ -931,33 +842,33 @@ ANSWERS.addComponent('FilterBox', { ], // Required, the vertical key for the search, default null verticalKey: 'verticalKey', - // Title to display above the filter + // Optional, title to display above the filter title: 'Filters', - // Show number of results for each filter + // Optional, show number of results for each filter showCount: true, - // Execute a new search whenever a filter selection changes + // Optional, execute a new search whenever a filter selection changes searchOnChange: false, - // Show a reset button per filter group + // Optional, show a reset button per filter group resetFilter: false, - // The label to use for the reset button above + // Optional, the label to use for the reset button above resetFilterLabel: 'reset', - // Show a reset-all button for the filter control + // Optional, show a reset-all button for the filter control resetFilters: true, - // The label to use for the reset-all button above + // Optional, the label to use for the reset-all button above resetFiltersLabel: 'reset-all', - // Allow collapsing excess filter options after a limit + // Optional, allow collapsing excess filter options after a limit showMore: true, - // The max number of filter to show before collapsing extras + // Optional, the max number of filter to show before collapsing extras showMoreLimit: 5, - // The label to show for displaying more filter + // Optional, the label to show for displaying more filter showMoreLabel: 'show more', - // The label to show for displaying less filter + // Optional, the label to show for displaying less filter showLessLabel: 'show less', - // Allow expanding and collapsing entire groups of filters + // Optional, allow expanding and collapsing entire groups of filters expand: true, - // Show the number of applied filter when a group is collapsed + // Optional, show the number of applied filter when a group is collapsed showNumberApplied: true, - // The label to show on the apply button + // Optional, the label to show on the apply button applyLabel: 'apply', // Optional, whether or not this filterbox contains dynamic filters, default false isDynamic: true @@ -966,7 +877,9 @@ ANSWERS.addComponent('FilterBox', { ## Facets Component -The Facets component displays filters relevant to the current search, configured on the server, automatically. Facets are only available for vertical searches. +This component is only for Vertical pages. + +The Facets component displays filters relevant to the current search, configured on the server, automatically. ```html
@@ -974,34 +887,37 @@ The Facets component displays filters relevant to the current search, configured ```js ANSWERS.addComponent('Facets', { + // Required, the selector for the container element where the component will be injected container: '.facets-container', - // Title to display above the facets + // Required + verticalKey: '', + // Optional, title to display above the facets title: 'Filters', - // Show number of results for each facet + // Optional, show number of results for each facet showCount: true, - // Execute a new search whenever a facet selection changes + // Optional, execute a new search whenever a facet selection changes searchOnChange: false, - // Show a reset button per facet group + // Optional, show a reset button per facet group resetFacet: false, - // The label to use for the reset button above + // Optional, the label to use for the reset button above resetFacetLabel: 'reset', - // Show a reset-all button for the facets control + // Optional, show a reset-all button for the facets control resetFacets: true, - // The label to use for the reset-all button above + // Optional, the label to use for the reset-all button above resetFacetsLabel: 'reset-all', - // Allow collapsing excess facet options after a limit + // Optional, allow collapsing excess facet options after a limit showMore: true, - // The max number of facets to show before collapsing extras + // Optional, the max number of facets to show before collapsing extras showMoreLimit: 5, - // The label to show for displaying more facets + // Optional, the label to show for displaying more facets showMoreLabel: 'show more', - // The label to show for displaying less facets + // Optional, the label to show for displaying less facets showLessLabel: 'show less', - // Allow expanding and collapsing entire groups of facets + // Optional, allow expanding and collapsing entire groups of facets expand: true, - // Show the number of applied facets when a group is collapsed + // Optional, show the number of applied facets when a group is collapsed showNumberApplied: true, - // The label to show on the apply button + // Optional, the label to show on the apply button applyLabel: 'apply' }); ``` @@ -1016,10 +932,13 @@ The FilterSearch component provides a text input box for users to type a query a ```js ANSWERS.addComponent('FilterSearch', { + // Required, the selector for the container element where the component will be injected container: '.filter-search-container', - verticalKey: '', // required - placeholderText: 'Start typing...', // optional, no default - // If true, the selected filter is saved and used for the next search, + // Required + verticalKey: '', + // Optional, no default + placeholderText: 'Start typing...', + // Optional, if true, the selected filter is saved and used for the next search, // but does not trigger a search itself. Defaults to false. storeOnChange: true, // Optional, defaults to native form node within container @@ -1065,56 +984,49 @@ FilterOptions displays a set of filters with either checkboxes or radio buttons. ```js ANSWERS.addComponent('FilterOptions', { + // Required, the selector for the container element where the component will be injected container: '.filter-container', - // Control type, singleoption or multioption + // Required, control type: 'singleoption' or 'multioption' control: 'singleoption', - // If true, the filter value is saved on change and sent with the next search. - // Defaults to false. - storeOnChange: true, - // List of options + // Required, list of options options: [ { - // Label to show next to the filter option + // Required, label to show next to the filter option label: 'Open Now', - // The api field to filter on, configured on the Yext platform + // Required, the field's API name to filter on, configured in the Yext platform field: 'c_openNow', - // The value for the above field to filter by - value: true + // Required, the value for the above field to filter by + value: true, + // Optional, whether the option is selected by default + selected: true, }, - { - label: 'Dog Friendly', - field: 'c_dogFriendly', - value: true - }, - { - label: 'Megastores', - field: 'c_storeType', - value: 'Megastore' - } + ... ], - // Optional, the selector used for options in the template - optionSelector: '.js-option', + // Optional, if true, the filter value is saved on change and sent with the next search. Defaults to false. + storeOnChange: false, + // Optional, the selector used for options in the template, defaults to '.js-yext-filter-option' + optionSelector: '.js-yext-filter-option', // Optional, if true, triggers a search on each change to a filter, default false searchOnChange: true, - // Show a reset button + // Optional, if true, show a reset button showReset: false, - // The label to use for the reset button + // Optional, the label to use for the reset button, defaults to 'reset' resetLabel: 'reset', - // Allow collapsing excess filter options after a limit + // Optional, allow collapsing excess filter options after a limit, defaults to true showMore: true, - // The max number of filter options to show before collapsing extras + // Optional, the max number of filter options to show before collapsing extras, defaults to 5 showMoreLimit: 5, - // The label to show for displaying more options + // Optional, the label to show for displaying more options, defaults to 'show more' showMoreLabel: 'show more', - // The label to show for displaying less options + // Optional, the label to show for displaying less options, defaults to 'show less' showLessLabel: 'show less', - // Allow expanding and collapsing the filter + // Optional, allow expanding and collapsing the filter, defaults to true showExpand: true, - // Show the number of applied options when a group is collapsed + // Optional, show the number of applied options when a group is collapsed, defaults to true showNumberApplied: true, // Optional, the callback function to call when changed onChange: function() {}, - // Optional, the label to be used in the legend + // Optional, the label to be used in the legend, defaults to 'Filters' label: 'Filters' }); ``` @@ -1129,22 +1041,23 @@ Displays two numeric inputs for selecting a number range. ```js ANSWERS.addComponent('RangeFilter', { + // Required, the selector for the container element where the component will be injected container: '.range-filter-container', - // The api field to filter on + // Required, the API name of the field to filter on field: 'outdoorPoolCount', - // Title to display for the range + // Optional, title to display for the range control, defaults to empty legend title: 'Number of Outdoor Pools', - // The label to show next to the min value, optional + // Optional, the label to show next to the min value, defaults to no label minLabel: 'At Least', - // The placeholder text for the min value, optional + // Optional, the placeholder text for the min value, defaults to 'Min' minPlaceholderText: 'Min', - // The label to show next to the max value, optional + // Optional, the label to show next to the max value, defaults to no label maxLabel: 'Not More Than', - // The placeholder text for the max value, optional + // Optional, the placeholder text for the max value, defaults to 'Max' maxPlaceholderText: 'Max', - // The initial min value to show, defaults to 0 + // Optional, the initial min value to show, defaults to 0 initialMin: 1, - // The initial max value to show, defaults to 10 + // Optional, the initial max value to show, defaults to 10 initialMax: 5, // Optional, the callback function to call when changed onChange: function() {} @@ -1161,20 +1074,23 @@ Displays two date inputs for selecting a range of dates. ```js ANSWERS.addComponent('DateRangeFilter', { + // Required, the selector for the container element where the component will be injected container: '.date-range-filter-container', - // The api field to filter on + // Required, the API name of the field to filter on field: 'time.start', - // Title to display for the range + // Optional, title to display for the range, defaults to empty legend title: 'Event Start Date', - // The label to show next to the min date, optional + // Optional, the label to show next to the min date, defaults to no label minLabel: 'Earliest', - // The label to show next to the max date, optional + // Optional, the label to show next to the max date, defaults to no label maxLabel: 'Latest', - // The initial min date to show in yyyy-mm-dd format, defaults to today + // Optional, the initial min date to show in yyyy-mm-dd format, defaults to today initialMin: '2019-08-01', - // The initial max date to show in yyyy-mm-dd format, defaults to today + // Optional, the initial max date to show in yyyy-mm-dd format, defaults to today initialMax: '2019-09-01', - // If true, this filter represents an exclusive range, rather than an inclusive one + // Optional, whether to store the filter on change to input + storeOnChange: true, + // Optional, if true, this filter represents an exclusive range, rather than an inclusive one, defaults to false isExclusive: false, // Optional, the callback function to call when changed onChange: function() {} @@ -1189,8 +1105,11 @@ Displays a "Use My Location" button that filters results to a radius around the
``` +For all optional config in the example, unless otherwise specified, the default is the example value. + ```js ANSWERS.addComponent('GeoLocationFilter', { + // Required, the selector for the container element where the component will be injected container: '.geolocation-filter-container', // Optional, the vertical key to use verticalKey: 'verticalKey', @@ -1200,7 +1119,7 @@ ANSWERS.addComponent('GeoLocationFilter', { enabledText: 'Disable My Location', // Optional, the text to show ehn loading the user's location loadingText: 'Loading', - // The label to show when unable to get the user's location + // Optional, The label to show when unable to get the user's location errorText: 'Unable To Use Location', // Optional, CSS selector of the button buttonSelector: '.js-yxt-GeoLocationFilter-button', @@ -1235,7 +1154,7 @@ The Navigation Component adds a dynamic experience to your pages navigation expe When using multiple vertical searches in a universal search, the navigation ordering will be automatically updated based on the search results. By default, tabs that do not fit in the container will go inside a dropdown menu. -Tab configurations should be provided in initial configuration. +Vertical configurations should be provided the ANSWERS.init's `verticalPages` configuration. Find more info in the [Vertical Pages Configuration](#vertical-pages-configuration) section. ```html @@ -1244,11 +1163,16 @@ Tab configurations should be provided in initial configuration. ```js ANSWERS.addComponent('Navigation', { + // Required, the selector for the container element where the component will be injected container: '.navigation-container', - mobileOverflowBehavior: 'COLLAPSE' // optional, controls if navigation shows a scroll bar or dropdown for mobile. Options are COLLAPSE and INNERSCROLL - ariaLabel: 'Search Page Navigation' // optional, the aria-label to set on the navigation - overflowLabel: 'More', // optional, the label to display on the dropdown menu button when it overflows - overflowIcon: null // optional, name of the icon to show on the dropdown button instead when it overflows + // Optional, controls if navigation shows a scroll bar or dropdown for mobile. Options are COLLAPSE and INNERSCROLL + mobileOverflowBehavior: 'COLLAPSE', + // Optional, the aria-label to set on the navigation, defaults to 'Search Page Navigation' + ariaLabel: 'Search Page Navigation', + // Optional, the label to display on the dropdown menu button when it overflows, defaults to 'More' + overflowLabel: 'More', + // Optional, name of the icon to show on the dropdown button instead when it overflows + overflowIcon: null, }) ``` @@ -1262,34 +1186,45 @@ when a search query is run. ``` ```js -// Unless noted, fields are optional and show default values ANSWERS.addComponent('QASubmission', { - container: '.question-submission-container', // Required. This is the class of the target HTML element the component will be mounted to - formSelector: '.js-form', // Defaults to native form node within container - nameLabel: 'Name', // Label for name input - emailLabel: 'Email', // Label for email input - questionLabel: 'Question', // Label for question input - sectionTitle: 'Ask a question', // Title displayed for the form + // Required, the selector for the container element where the component will be injected + container: '.question-submission-container', + // Required. Set this to the Entity ID of the organization entity in the Knowledge Graph + entityId: 123, + // Required. Defaults to '' + privacyPolicyUrl: 'https://mybiz.com/policy', + // Optional, defaults to native form node within container + formSelector: '.js-form', + // Optional, ;abel for name input + nameLabel: 'Name', + // Optional, label for email input + emailLabel: 'Email', + // Optional, label for question input + questionLabel: 'Question', + // Optional, title displayed for the form + sectionTitle: 'Ask a question', + // Optional, teaser displayed for the form, next to the title teaser: 'Can\'t find what you’re looking for? Ask a question below.', - // Teaser displayed for the form, next to the title + // Optional, description for the form description: 'Enter your question and contact information, and we\'ll get back to you with a response shortly.' - // Description for the form + // Optional, text before the privacy policy link privacyPolicyText: 'By submitting my email address, I consent to being contacted via email at the address provided.', - // Text before the privacy policy link - privacyPolicyUrlLabel: 'Learn more here.', // Label for the privacy policy url - privacyPolicyUrl: 'https://mybiz.com/policy', // Required. Defaults to '' + // Optional, label for the privacy policy url + privacyPolicyUrlLabel: 'Learn more here.', + // Optional, error message displayed when the privacy policy is not selected privacyPolicyErrorText: '* You must agree to the privacy policy to submit feedback.', - // Error message displayed when the privacy policy is not selected + // Optional, error message displayed when an invalid email is not submitted emailFormatErrorText: '* Please enter a valid email address.' - // Error message displayed when an invalid email is not submitted - requiredInputPlaceholder: '(required)', // Placeholder displayed in all required fields + // Optional, placeholder displayed in all required fields + requiredInputPlaceholder: '(required)', + // Optional, confirmation displayed once a question is submitted questionSubmissionConfirmationText: 'Thank you for your question!', - // Confirmation displayed once a question is submitted - buttonLabel: 'Submit', // Label displayed on the button to submit a question - entityId: 123, // Required. Set this to the Entity ID of the organization entity in the Knowledge Graph - expanded: true, // Set this to whether or not the form is expanded by default when a user arrives on the page + // Optional, label displayed on the button to submit a question + buttonLabel: 'Submit', + // Optional, set this to whether or not the form is expanded by default when a user arrives on the page + expanded: true, + // Optional, error message displayed when there is an issue with the QA Submission request networkErrorText: 'We\'re sorry, an error occurred.' - // Error message displayed when there is an issue with the QA Submission request }) ``` @@ -1303,8 +1238,10 @@ The spell check component shows spell check suggestions/autocorrect. ```js ANSWERS.addComponent('SpellCheck', { + // Required, the selector for the container element where the component will be injected container: '.spell-check-container', - suggestionHelpText: 'Did you mean:' // Optional, the help text to display when suggesting a query + // Optional, the help text to display when suggesting a query + suggestionHelpText: 'Did you mean:', }) ``` @@ -1318,12 +1255,18 @@ The location bias component shows location that used for location bias and allow ```js ANSWERS.addComponent('LocationBias', { + // Required, the selector for the container element where the component will be injected container: '.location-bias-container', - verticalKey: 'verticalKey', // Optional, the vertical key for the search, default null - updateLocationEl: '.js-locationBias-update-location', // Optional, the element used for updating location - ipAccuracyHelpText: 'based on your internet address', // Optional, help text to inform someone their IP was used for location - deviceAccuracyHelpText: 'based on your device', // Optional, help text to inform someone their device was used for location - updateLocationButtonText: 'Update your location' // Optional, text used for the button to update location + // Optional, the vertical key for the search, default null + verticalKey: 'verticalKey', + // Optional, the element used for updating location + updateLocationEl: '.js-locationBias-update-location', + // Optional, help text to inform someone their IP was used for location + ipAccuracyHelpText: 'based on your internet address', + // Optional, help text to inform someone their device was used for location + deviceAccuracyHelpText: 'based on your device', + // Optional, text used for the button to update location + updateLocationButtonText: 'Update your location' }) ``` @@ -1340,6 +1283,7 @@ Currently, there may be only one sort options component per page. // note: showExpand and showNumberApplied options are explicitly not included: // sorting will always be exposed to the user if added. ANSWERS.addComponent('SortOptions', { + // Required, the selector for the container element where the component will be injected container: '.sort-options-container', // Optional: The label used for the “default” sort (aka sort the order provided by the config), defaults to 'Best Match' defaultSortLabel: 'Best Match', @@ -1395,9 +1339,212 @@ ANSWERS.addComponent('SortOptions', { }); ``` +## Map Component +The Map component displays a map with a pin for each result that has Yext display coordinates. + +```html +
+``` + +```js +ANSWERS.addComponent('Map', { + // Required. This is the class of the target HTML element the component will be mounted to + container: '.map-container', + // Required. Supported map providers include: `google` or `mapBox`, not case-sensitive + mapProvider: 'mapBox', + // Required*. The API Key used for interacting with the map provider; (*except for Google Maps if provided `clientId`) + apiKey: '', + // Optional, can be used for Google Maps in place of the API key + clientId: '', + // Optional, determines whether or not to collapse pins at the same lat/lng + collapsePins: false, + // Optional, the zoom level of the map, defaults to 14 + zoom: 14, + // Optional, the default coordinates to display if there are no results returned used if showEmptyMap is set to true + defaultPosition: { lat: 37.0902, lng: -95.7129 }, + // Optional, determines if an empty map should be shown when there are no results + showEmptyMap: false, + // Optional, callback to invoke when a pin is clicked. The clicked item(s) are passed to the callback + onPinClick: null, + // Optional, callback to invoke once the Javascript is loaded + onLoaded: function () {}, + // Optional, configuration for the map's behavior when a query returns no results + noResults: { + // Optional, whether to display all results in the vertical when no results are found. Defaults to false, in which case only the no results card will be shown. + displayAllResults: true + }, + // Optional, the custom configuration override to use for the map markers, function + pin: function () { + return { + icon: { + anchor: null, // e.g. { x: 1, y: 1 } + svg: null, + url: null, + scaledSize: null // e.g. { w: 20, h: 20 } + }, + labelType: 'numeric' + }; + }, +}; + +# Customizing Components + +## Using a Custom Renderer + +If you want to use a use your own template language (e.g. soy, mustache, groovy, etc), +you should NOT use the template argument. Instead, you can provide a custom render function to the component. + +```js +ANSWERS.addComponent('SearchBar', { + container: '.search-container', + render: function(data) { + // Using native ES6 templates -- but you can replace this with soy, + // or any other templating language as long as it returns a string. + return `` + } +}) +``` + +## Custom Data Formatting + +You can format specific entity fields using `fieldFormatters`. +These formatters are applied before the `transformData` step. + +Each formatter takes in an object with the following properties : +- `entityProfileData` +- `entityFieldValue` +- `highlightedEntityFieldValue` +- `verticalId` +- `isDirectAnswer` + +Below is an example usage. +```js +ANSWERS.init({ + apiKey: '', + experienceKey: '', + fieldFormatters: { + 'name': (formatterObject) => formatterObject.entityFieldValue.toUpperCase(), + 'description' : (formatterObject) => formatterObject.highlightedEntityFieldValue + } +}); +``` + +## Custom Data Transforms + +If you want to mutate the data thats provided to the render/template before it gets rendered, +you can use the `transformData` hook. + +All properties and values that you return from here will be accessible from templates. + + +```js +ANSWERS.addComponent('SearchBar', { + container: '.search-container', + transformData: (data) => { + // Extend/overide the data object + return Object.assign({}, data, { + title: data.title.toLowerCase() + }) + }, + render: function(data) { + // Using native ES6 templates -- but you can replace this with soy, + // or any other templating language as long as it returns a string. + return `` + } +}) +``` + +## Using a Custom Template for a Component +All component templates are written using [Handlebars templates](https://handlebarsjs.com/). + +It's easy to override these templates with your own templates. +Keep in mind, that you must provide valid handlebars syntax here. + +```js +// Use handlebars syntax to create a template string +let customTemplate = `` + +ANSWERS.addComponent('SearchBar', { + container: '.search-container', + template: customTemplate +}) +``` + +## Creating Custom Components +You can create custom Answers components with the same power of the builtin components. First, create +a subtype of ANSWERS.Component and register it. + +For ES6: +```js +class MyCustomComponent extends ANSWERS.Component { + constructor (config) { + super(config); + this.myProperty = config.myProperty; + } + + static defaultTemplateName () { + return 'default'; + } + + static areDuplicateNamesAllowed () { + return false; + } + + static get type () { + return 'MyCustomComponent'; + } +} +ANSWERS.registerComponentType(MyCustomComponent); // Register the component with the library +``` + +For ES5: +```js +function MyCustomComponent (config) { + ANSWERS.Component.call(this, config); + + this.myProperty = config.myProperty; +} + +MyCustomComponent.prototype = Object.create(ANSWERS.Component.prototype); +MyCustomComponent.prototype.constructor = MyCustomComponent; +MyCustomComponent.defaultTemplateName = function () { return 'default' }; +MyCustomComponent.areDuplicateNamesAllowed = function () { return false }; +Object.defineProperty(MyCustomComponent, 'type', { get: function () { return 'MyCustomComponent' } }); + +ANSWERS.registerComponentType(MyCustomComponent); // Register the component with the library +``` + +Now you can use your custom component like any built-in component: + +```js +ANSWERS.addComponent('MyCustomComponent', { + container: '.my-component-container', + template: `
{{_config.myProperty}}
`, + myProperty: 'my property' +}); +``` + +# Template Helpers + +When using handlebars templates, Answers ships with a bunch of pre-built template helpers that you can use. You can learn more about them [here](https://github.com/jonschlinkert/template-helpers). + +If you want to register custom template helpers to the handlebars render, you can do so like this: +```js +ANSWERS.registerHelper('noop', function(options) { + return options.fn(this); +}) +``` + +You can learn more about the interface for registering helpers by taking a look at the [Handlebars Block Helpers](https://handlebarsjs.com/block_helpers.html) documentation. + # Analytics -Answers will track some basic interaction analytics automatically, such as search bar impressions and Call-To-Action clicks. You may add additional, custom analytic events to templates using certain data attributes, explained below. You may also send analytics from external code with the below interface. +If a businessId is supplied in the config, Answers will track some basic interaction analytics automatically, such as search bar impressions and Call-To-Action clicks. +If you would like to add custom analytics on top of the built-in ones, use the following: + +## Custom Analytics Using JavaScript + +You may send analytics from external code with the below interface. ```js const event = new ANSWERS.AnalyticsEvent('CUSTOM'); @@ -1405,9 +1552,9 @@ Answers will track some basic interaction analytics automatically, such as searc ANSWERS.AnalyticsReporter.report(event) ``` -## Click Analytics +## Custom Analytics Using Data Attributes -Click analytics can be attached to an element by adding the `data-eventtype` attribute to the element you want to track clicks for. The provided string should be the type of the analytics event. You can optionally include metadata inside the `data-eventoptions` attribute, in a JSON format. Whenever the element is clicked, an analtyics event with that data will be sent to the server. +You may add additional, custom analytic events to templates using certain data attributes. Click analytics can be attached to an element by adding the `data-eventtype` attribute to the element you want to track clicks for. The provided string should be the type of the analytics event. You can optionally include metadata inside the `data-eventoptions` attribute, in a JSON format. Whenever the element is clicked, an analtyics event with that data will be sent to the server. ```html