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:
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.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
).onUpdate
: Callback envoked when a component’s state is updated. It is triggered by a call tosetState
.
We do not expose an
unMount()
or aremove()
.
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:
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 theUniversal Results
key in global storage. When said key changes,setState
is called on theUniversalResults
component. The component is unmounted and then mounted again with the new results.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:
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:
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.)
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: