loading

Component Lifecycle and State

Components in the Answers Javascript Library have a certain lifecycle and react to the surrounding state; take, for example, the UniversalResults Component.

When the UniversalResults component is displayed for the first time, it is created and then mounted to the page. When a new set of results return, it is unmounted, then remounted to reflect the new results.

The behavior of UniversalResults described above is part of its component lifecycle. You can also see the component react to the environment around it (or the “state”), namely new results returning. A component’s lifecycle is influenced by said state.

Lifecycle methods

Definitions

Every component has the following three lifecycle methods. With the exception of onMount, these callbacks do not take any parameters:

  1. onCreate: Callback envoked when the component is created (the class for that component is instantiated). This creates a new instance of the component class. It is only fired once on a page.
  2. onMount: Callback envoked when a component is mounted to the DOM. Calls the render function and appends what is rendered to the DOM. This is passed a reference to the component (this).
  3. onUpdate: Callback envoked when a component’s state is updated. It is triggered by a call to setState.

We do not expose an unMount() or a remove().

Using Lifecycle Methods

The above lifecycle methods are exposed in all components’ configuration. These callbacks are additive; they are run following our internal lifecycle methods. For example, the below would print a console statement every time the UniversalResults was mounted to the page:

this.addComponent('UniversalResults', {
    container: '.results-container',
    onMount: function(data) {
        console.log(data);
    }
    //... other component config here
});

If you’d like to completely replace our lifecycle methods, you can use the following methods, which behave like their counterparts listed above: - onCreateOverride - onMountOverride - onUpdateOverride

We recommend using these methods sparingly, since you will impact the core functionality of the component. If you find yourself needing to use the overrides, we recommend looking at creating custom components.

State

As described above, components also react to their surrounding environment, called the state. Changes in state can trigger certain lifecycle methods, such as unMount and Mount.

Each component has a setState method. This is not exposed currently to implementers using the SDK’s built-in components, but is helpful context (and can be used when creating custom components).

When called, the setState method will re-render a component. Said method can be triggered in a few ways:

  1. If the component is listening for a change to something in global storage. Going back to our UniversalResults example; when the results update, this component will re-render to reflect the new results. Behind the scenes, the component is listening to changes to the Universal Results key in global storage. When said key changes, setState is called on the UniversalResults component. The component is unmounted and then mounted again with the new results.

  2. If the component has custom JS. For example, the Location Bias component has custom JS for disabling the geolocation prompt if a user has denied it. This calls setState to set the current display name and accuracy (which will likely be IP accuracy).

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.

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 `<div class="my-search">${data.title}</div>`
  }
})

Custom Data Transforms

Background

Sometimes, you might want to alter the data recieved by the Answers API before it’s passed along to the template for rendering. For example, say you’ve set a direct answer on the phone number field. Before the raw phone number is passed to the template, you’ll likely want to format it to add parenthesis and dashes.

If you want to mutate the data that’s provided to the render/template before it gets rendered, you can use the transformData hook.

Usage

All properties and values that you return from transformData will be accessible from templates.

ANSWERS.addComponent('SearchBar', {
  container: '.search-container',
  transformData: (data) => {
    // Extend/overide the data object
    return Object.assign({}, data, {
      title: data.title.toLowerCase()
    })
  }
})

Example

Here’s an example of using a custom data transform for the DirectAnswer component.

Code

Explanation

To start, our DirectAnswer component returns phone numbers that look like this:

Direct Answer Before Formatting

First, we’ll create a function that formats the DirectAnswer value for phone number, keying off of the fieldType:

function formatDirectAnswer(fieldName, fieldType, value) {

  //adapted from https://stackoverflow.com/a/8358185
  if (fieldType == "phone") {
    return value
      .replace(/\D+/g, "")
      .replace(/(\d{1})(\d{3})(\d{3})(\d{4})/, "$1 ($2) $3-$4");
  }
  return value;
}

Next, in the DirectAnswer component, we’ll call said function within transformData:

this.addComponent('DirectAnswer', {
    container: '.direct-answer-container',   
    transformData: (data) => {
        //data has fieldName, fieldType, value
        return {
            ...data,
            answer: {
                ...data.answer,
                value: formatDirectAnswer(data.answer.fieldName, data.answer.fieldType, data.answer.value)
            }
        }
    }
});

Since transformData is called before the data is passed to the template, it’ll format the phone number as stipulated. Now, when phone number is returned, it’ll look like this:

Direct Answer Before Formatting

Using a Custom Template for a Component

All component templates are written using Handlebars templates.

It’s easy to override these templates with your own templates. Keep in mind, that you must provide valid Handlebars syntax here. You can either define templates in-line, or use a registerTemplate method.

Define Templates In-Line

// Use Handlebars syntax to create a template string
let customTemplate = `<div class="my-search">{{title}}</div>`

ANSWERS.addComponent('SearchBar', {
  container: '.search-container',
  template: customTemplate
})

Define Templates with registerTemplate

You can also define templates with our registerTemplate method. This method takes two arguments, a name and the template string.

In order to override an existing component’s template, the name should match the default template. This is returned in the component’s defaultTemplateName method; you can find it in the JS files for the various components. For example, here’s the SearchBar.

ANSWERS.registerTemplate('search/search', `<div class="my-search">{{title}}</div>` );

ANSWERS.addComponent('SearchBar', {
  container: '.search-container'
})

Which Should I Use?

registerTemplate is helpful because it can be called at any point (whereas your addComponent can be called once), therefore allowing for dynamic templates. However, in most cases, defining the template inline will suffice. registerTemplate is most helpful for creating custom components.

Custom Data Formatting

Note there are known bugs with this feature, proceed with caution.

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.

ANSWERS.init({
  apiKey: '<API_KEY_HERE>',
  experienceKey: '<EXPERIENCE_KEY_HERE>',
  fieldFormatters: {
    'name': (formatterObject) => formatterObject.entityFieldValue.toUpperCase(),
    'description' : (formatterObject) => formatterObject.highlightedEntityFieldValue
  }
});

Creating Custom Components

You can create custom Answers components with the same power of the builtin components.

Creating a Custom Component

You’ll create a subtype of ANSWERS.Component and register it.

For ES6:

class MyCustomComponent extends ANSWERS.Component {
  constructor (config, systemConfig) {
    super(config, systemConfig);
    this.myProperty = config.myProperty;
  }

  static defaultTemplateName () {
    return 'default';
  }

  /**
  * Whether there can be two components with the same name. Recommend setting this 
  * to false.
  * @returns {boolean}
  */
  static areDuplicateNamesAllowed () {
    return false;
  }

  static get type () {
    return 'MyCustomComponent';
  }
}

ANSWERS.registerComponentType(MyCustomComponent); // Register the component with the library

For 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' } });

ANSWERS.registerComponentType(MyCustomComponent); // Register the component with the library

Adding the Custom Component

Now you can use your custom component like any built-in component:

ANSWERS.addComponent('MyCustomComponent', {
  container: '.my-component-container',
  template: `<div>{{_config.myProperty}}</div>`,
  myProperty: 'my property'
});

Listening to State

If your component needs to listen to state, give the component a moduleId with the relevant storage key. These keys can be found here.

For example, if a custom component should be listening to updates to vertical results, add this.moduleId = 'vertical-results' to its constructor:

class MyCustomComponent extends ANSWERS.Component {
  constructor (config, systemConfig) {
    super(config, systemConfig);
    this.myProperty = config.myProperty;
    this.moduleId = 'vertical-results';
  }

  //etc
}

When the vertical-results storage key updates, the component’s setState method will automatically be called.

Example

Putting it all together, let’s say you’d like to create a new template that displays the results count, and updates every time the results update. (We’ll use ES6 for this, but the same can easily be extended to ES5.)

Custom Component for Results Count

1. Define the Component’s Template

We’ll begin by defining the component’s template by using registerTemplate. The name of the template will be CustomResultsCountTemplate. To start, this template won’t display anything – just a hardcoded string (which we’ll replace later on). We’ll place this within our onReady.

ANSWERS.registerTemplate('CustomResultsCountTemplate', `<div class="myCustomResultsCount">Here's where results count will go!</div>` );

2. Create & Register the Component

Next, we’ll create our custom component. We’ll name it CustomResultsCount. We’ll start with the basics: the constructor, the default template (which we defined above), then areDuplicateNamesAllowed and type methods.

class CustomResultsCount extends ANSWERS.Component {
  constructor (config) {
    super(config);
  }

  static defaultTemplateName () {
    return 'CustomResultsCountTemplate';
  }

  static areDuplicateNamesAllowed () {
    return false;
    //can there be two components of the same name. Recommended false. 
  }

  static get type () {
    return 'CustomResultsCount';
  }
}

ANSWERS.registerComponentType(CustomResultsCount); // Register the component with the library

3. Add the Component

Finally, we’ll add our component to the page, and provide it with a container, .custom-results-count.

<div class=custom-results-count></div>
this.addComponent('CustomResultsCount', {
  container: '.custom-results-count'
});

At this point, here’s what our full page looks like:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <!-- JS Links -->
    <script src="https://assets.sitescdn.net/answers/v1/answerstemplates.compiled.min.js"></script>
    <script src="https://assets.sitescdn.net/answers/v1/answers.js"></script>
    <link
      rel="stylesheet"
      type="text/css"
      href="https://assets.sitescdn.net/answers/v1/answers.css"
    />

    <style>
      .answers-container {
        margin: auto;
        max-width: 698px;
      }
    </style>
  </head>

  <body>
    <div class="answers-container">
      <div class="search-bar-container"></div>
      <div class="custom-results-count"></div>
      <div class="results-container"></div>
    </div>

    <script>
      ANSWERS.init({
        apiKey: "3517add824e992916861b76e456724d9",
        experienceKey: "answers-js-docs",
        businessId: "3215760",
        experienceVersion: "PRODUCTION",
        verticalKey: "locations",
        search: {
          verticalKey: "locations",
          defaultInitialSearch:
            "locations that take mastercard and offer free wifi",
          limit: 20
        },
        onReady: function () {
          ANSWERS.registerTemplate(
            "CustomResultsCountTemplate",
            `<div class="myCustomResultsCount">Here's where results count will go!</div>`
          );

          class CustomResultsCount extends ANSWERS.Component {
            constructor(config, systemConfig) {
              super(config, systemConfig);
            }

            static defaultTemplateName() {
              return "CustomResultsCountTemplate";
            }

            static areDuplicateNamesAllowed() {
              return false;
            }

            static get type() {
              return "CustomResultsCount";
            }
          }

          ANSWERS.registerComponentType(CustomResultsCount); // Register the component with the library

          this.addComponent("CustomResultsCount", {
            container: ".custom-results-count"
          });

          //other components below
          this.addComponent("SearchBar", {
            container: ".search-bar-container",
            verticalKey: "locations"
          });

          this.addComponent("VerticalResults", {
            container: ".results-container",
            hideResultsHeader: true
          });
        }
      });
    </script>
  </body>
</html>

4. React to State

Now that we have a functioning custom component, we’ll need it to react to the state of the results updating.

First, we’ll update our constructor to listen to the vertical-results storage key by setting this.moduleId = 'vertical-results'; in the constructor. The storage keys can all be found here.

Next, we’ll need to add the setState method to our CustomResultsCount class. For now, we’ll log the data passed to setState, and pass it to the super’s setState method. This will help us define where the results count lives.

class CustomResultsCount extends ANSWERS.Component {

  constructor(config, systemConfig) {
    super(config, systemConfig);
    this.moduleId = 'vertical-results'; 
  }

  //what's returned here is what is made available to the component's template
  setState(data) {
    console.log(data.toString());
    super.setState({
      ...data
    });
  }
  // defaultTemplateName(), areDuplicateNamesAllowed(), get type() all defined here
}

The console prints the following

VerticalResults {searchState: "search-complete", verticalConfigId: "locations", resultsCount: 3, encodedState: "", appliedQueryFilters: Array(3), …}

We’ll therefore need to access data.resultsCount.

5. Add resultsCount

Now that we’re listening to state and we know where resultsCount is coming from, we’ll modify setState and our default template to reflect that resultsCount.

ANSWERS.registerTemplate('CustomResultsCountTemplate', `<div class="myCustomResultsCount">{{resultsCount}}</div>` ); //add resultsCount to the template

class CustomResultsCount extends ANSWERS.Component {
  
  constructor(config, systemConfig) {
    super(config, systemConfig);
    this.moduleId = 'vertical-results'; 
  }

  setState(data) {
    const resultsCount = data.resultsCount; //define results count
    super.setState({
      ...data,
      resultsCount //return it
    });
  }
  // defaultTemplateName(), areDuplicateNamesAllowed(), get type() all defined here
}

Here’s a final working example: