diff --git a/src/components/PoweredBy/PoweredBy.js b/src/components/PoweredBy/PoweredBy.js
deleted file mode 100644
index f881d8396d..0000000000
--- a/src/components/PoweredBy/PoweredBy.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-
-class PoweredBy extends React.Component {
- shouldComponentUpdate() {
- return false;
- }
-
- render() {
- return (
-
- );
- }
-}
-
-PoweredBy.propTypes = {
- cssClasses: React.PropTypes.shape({
- root: React.PropTypes.string,
- link: React.PropTypes.string
- }),
- link: React.PropTypes.string.isRequired
-};
-
-export default PoweredBy;
diff --git a/src/components/PoweredBy/__tests__/PoweredBy-test.js b/src/components/PoweredBy/__tests__/PoweredBy-test.js
deleted file mode 100644
index 69ae33cb5c..0000000000
--- a/src/components/PoweredBy/__tests__/PoweredBy-test.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/* eslint-env mocha */
-
-import React from 'react';
-import expect from 'expect';
-import TestUtils from 'react-addons-test-utils';
-import PoweredBy from '../PoweredBy';
-
-import expectJSX from 'expect-jsx';
-expect.extend(expectJSX);
-
-describe('PoweredBy', () => {
- let renderer;
-
- beforeEach(() => {
- let {createRenderer} = TestUtils;
- renderer = createRenderer();
- });
-
- it('should render ', () => {
- let out = render({
- cssClasses: {
- root: 'pb-root',
- link: 'pb-link'
- },
- link: 'www.google.com'
- });
- expect(out).toEqualJSX(
- );
- });
-
- function render(extraProps = {}) {
- renderer.render();
- return renderer.getRenderOutput();
- }
-
- function getProps(extraProps = {}) {
- return {
- cssClasses: {},
- ...extraProps
- };
- }
-});
diff --git a/src/widgets/search-box/__tests__/search-box-test.js b/src/widgets/search-box/__tests__/search-box-test.js
index 78abe75059..d0bf59b255 100644
--- a/src/widgets/search-box/__tests__/search-box-test.js
+++ b/src/widgets/search-box/__tests__/search-box-test.js
@@ -182,25 +182,205 @@ describe('searchBox()', () => {
});
});
- context('adds a PoweredBy', () => {
+ context('poweredBy', () => {
+ let defaultInitOptions;
+ let defaultWidgetOptions;
+ let $;
+
beforeEach(() => {
container = document.createElement('div');
+ $ = container.querySelectorAll.bind(container);
+ defaultWidgetOptions = {container};
+ defaultInitOptions = {state, helper, onHistoryChange};
});
- it('do not add the poweredBy if not specified', () => {
- widget = searchBox({container});
- widget.init({state, helper, onHistoryChange});
- expect(container.querySelector('.ais-search-box--powered-by')).toBe(null);
+ it('should not add the element with default options', () => {
+ // Given
+ widget = searchBox(defaultWidgetOptions);
+
+ // When
+ widget.init(defaultInitOptions);
+
+ // Then
+ expect($('.ais-search-box--powered-by').length).toEqual(0);
});
- it('adds the poweredBy if specified', () => {
- widget = searchBox({container, poweredBy: true});
- widget.init({state, helper, onHistoryChange});
- const poweredBy = container.querySelector('.ais-search-box--powered-by');
- const poweredByLink = poweredBy.querySelector('a');
- const expectedLink = `https://www.algolia.com/?utm_source=instantsearch.js&utm_medium=website&utm_content=${location.hostname}&utm_campaign=poweredby`;
- expect(poweredBy).toNotBe(null);
- expect(poweredByLink.getAttribute('href')).toBe(expectedLink);
+ it('should not add the element with poweredBy: false', () => {
+ // Given
+ widget = searchBox({
+ ...defaultWidgetOptions,
+ poweredBy: false
+ });
+
+ // When
+ widget.init(defaultInitOptions);
+
+ // Then
+ expect($('.ais-search-box--powered-by').length).toEqual(0);
+ });
+
+ it('should add the element with poweredBy: true', () => {
+ // Given
+ widget = searchBox({
+ ...defaultWidgetOptions,
+ poweredBy: true
+ });
+
+ // When
+ widget.init(defaultInitOptions);
+
+ // Then
+ expect($('.ais-search-box--powered-by').length).toEqual(1);
+ });
+
+ it('should contain a link to Algolia with poweredBy: true', () => {
+ // Given
+ widget = searchBox({
+ ...defaultWidgetOptions,
+ poweredBy: true
+ });
+
+ // When
+ widget.init(defaultInitOptions);
+
+ // Then
+ let actual = $('.ais-search-box--powered-by-link');
+ let url = `https://www.algolia.com/?utm_source=instantsearch.js&utm_medium=website&utm_content=${location.hostname}&utm_campaign=poweredby`;
+ expect(actual.length).toEqual(1);
+ expect(actual[0].tagName).toEqual('A');
+ expect(actual[0].innerHTML).toEqual('Algolia');
+ expect(actual[0].getAttribute('href')).toEqual(url);
+ });
+
+ it('should let user add its own CSS classes with poweredBy.cssClasses', () => {
+ // Given
+ widget = searchBox({
+ ...defaultWidgetOptions,
+ poweredBy: {
+ cssClasses: {
+ root: 'myroot',
+ link: 'mylink'
+ }
+ }
+ });
+
+ // When
+ widget.init(defaultInitOptions);
+
+ // Then
+ let root = $('.myroot');
+ let link = $('.mylink');
+ expect(root.length).toEqual(1);
+ expect(link.length).toEqual(1);
+ expect(link[0].tagName).toEqual('A');
+ expect(link[0].innerHTML).toEqual('Algolia');
+ });
+
+ it('should still apply default CSS classes even if user provides its own', () => {
+ // Given
+ widget = searchBox({
+ ...defaultWidgetOptions,
+ poweredBy: {
+ cssClasses: {
+ root: 'myroot',
+ link: 'mylink'
+ }
+ }
+ });
+
+ // When
+ widget.init(defaultInitOptions);
+
+ // Then
+ let root = $('.ais-search-box--powered-by');
+ let link = $('.ais-search-box--powered-by-link');
+ expect(root.length).toEqual(1);
+ expect(link.length).toEqual(1);
+ });
+
+ it('should let the user define its own string template', () => {
+ // Given
+ widget = searchBox({
+ ...defaultWidgetOptions,
+ poweredBy: {
+ template: 'Foobar
'
+ }
+ });
+
+ // When
+ widget.init(defaultInitOptions);
+
+ // Then
+ expect(container.innerHTML).toContain('Foobar');
+ });
+
+ it('should let the user define its own Hogan template', () => {
+ // Given
+ widget = searchBox({
+ ...defaultWidgetOptions,
+ poweredBy: {
+ template: 'Foobar--{{url}}
'
+ }
+ });
+
+ // When
+ widget.init(defaultInitOptions);
+
+ // Then
+ expect(container.innerHTML).toContain('Foobar--https://www.algolia.com/');
+ });
+
+ it('should let the user define its own function template', () => {
+ // Given
+ widget = searchBox({
+ ...defaultWidgetOptions,
+ poweredBy: {
+ template: (data) => {
+ return `Foobar--${data.url}
`;
+ }
+ }
+ });
+
+ // When
+ widget.init(defaultInitOptions);
+
+ // Then
+ expect(container.innerHTML).toContain('Foobar--https://www.algolia.com/');
+ });
+
+ it('should gracefully handle templates with leading spaces', () => {
+ // Given
+ widget = searchBox({
+ ...defaultWidgetOptions,
+ poweredBy: {
+ template: `
+
+ Foobar
`
+ }
+ });
+
+ // When
+ widget.init(defaultInitOptions);
+
+ // Then
+ expect(container.innerHTML).toContain('Foobar');
+ });
+
+ it('should handle templates not wrapped in a node', () => {
+ // Given
+ widget = searchBox({
+ ...defaultWidgetOptions,
+ poweredBy: {
+ template: 'Foobar '
+ }
+ });
+
+ // When
+ widget.init(defaultInitOptions);
+
+ // Then
+ expect(container.innerHTML).toContain('Foobar');
+ expect($('.should-be-found').length).toEqual(1);
});
});
@@ -363,7 +543,6 @@ describe('searchBox()', () => {
expect(container.value).toBe('iphone');
});
-
it('handles external updates', () => {
container = document.body.appendChild(document.createElement('input'));
container.value = 'initial';
diff --git a/src/widgets/search-box/defaultTemplates.js b/src/widgets/search-box/defaultTemplates.js
new file mode 100644
index 0000000000..bcbe288535
--- /dev/null
+++ b/src/widgets/search-box/defaultTemplates.js
@@ -0,0 +1,7 @@
+export default {
+ poweredBy: `
+`
+};
diff --git a/src/widgets/search-box/search-box.js b/src/widgets/search-box/search-box.js
index 705ec69153..f82ebfdf17 100644
--- a/src/widgets/search-box/search-box.js
+++ b/src/widgets/search-box/search-box.js
@@ -1,12 +1,13 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
import {
bemHelper,
getContainerNode
} from '../../lib/utils.js';
import forEach from 'lodash/collection/forEach';
+import isString from 'lodash/lang/isString';
+import isFunction from 'lodash/lang/isFunction';
import cx from 'classnames';
-import PoweredBy from '../../components/PoweredBy/PoweredBy.js';
+import Hogan from 'hogan.js';
+import defaultTemplates from './defaultTemplates.js';
let bem = bemHelper('ais-search-box');
const KEY_ENTER = 13;
@@ -17,7 +18,11 @@ const KEY_SUPPRESS = 8;
* @function searchBox
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
* @param {string} [options.placeholder] Input's placeholder
- * @param {boolean} [options.poweredBy=false] Show a powered by Algolia link below the input
+ * @param {boolean|Object} [options.poweredBy=false] Define if a "powered by Algolia" link should be added near the input
+ * @param {function|string} [options.poweredBy.template] Template used for displaying the link. Can accept a function or a Hogan string.
+ * @param {number} [options.poweredBy.cssClasses] CSS classes to add
+ * @param {string|string[]} [options.poweredBy.cssClasses.root] CSS class to add to the root element
+ * @param {string|string[]} [options.poweredBy.cssClasses.link] CSS class to add to the link element
* @param {boolean} [options.wrapInput=true] Wrap the input in a `div.ais-search-box`
* @param {boolean|string} [autofocus='auto'] autofocus on the input
* @param {boolean} [options.searchOnEnterKeyPressOnly=false] If set, trigger the search
@@ -26,18 +31,18 @@ const KEY_SUPPRESS = 8;
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the
* wrapping div (if `wrapInput` set to `true`)
* @param {string|string[]} [options.cssClasses.input] CSS class to add to the input
- * @param {string|string[]} [options.cssClasses.poweredBy] CSS class to add to the poweredBy element
* @param {function} [options.queryHook] A function that will be called everytime a new search would be done. You
* will get the query as first parameter and a search(query) function to call as the second parameter.
* This queryHook can be used to debounce the number of searches done from the searchBox.
* @return {Object}
*/
+
const usage = `Usage:
searchBox({
container,
[ placeholder ],
[ cssClasses.{input,poweredBy} ],
- [ poweredBy ],
+ [ poweredBy=false || poweredBy.{template, cssClasses.{root,link}} ],
[ wrapInput ],
[ autofocus ],
[ searchOnEnterKeyPressOnly ],
@@ -72,6 +77,11 @@ function searchBox({
autofocus = 'auto';
}
+ // Convert to object if only set to true
+ if (poweredBy === true) {
+ poweredBy = {};
+ }
+
return {
getInput: function() {
// Returns reference to targeted input if present, or create a new one
@@ -113,25 +123,46 @@ function searchBox({
CSSClassesToAdd.forEach(cssClass => input.classList.add(cssClass));
},
addPoweredBy: function(input) {
- let poweredByContainer = document.createElement('div');
- input.parentNode.insertBefore(poweredByContainer, input.nextSibling);
- let poweredByCssClasses = {
- root: cx(bem('powered-by'), cssClasses.poweredBy),
- link: bem('powered-by-link')
+ // Default values
+ poweredBy = {
+ cssClasses: {},
+ template: defaultTemplates.poweredBy,
+ ...poweredBy
};
- let link = 'https://www.algolia.com/?' +
+
+ let poweredByCSSClasses = {
+ root: cx(bem('powered-by'), poweredBy.cssClasses.root),
+ link: cx(bem('powered-by-link'), poweredBy.cssClasses.link)
+ };
+
+ let url = 'https://www.algolia.com/?' +
'utm_source=instantsearch.js&' +
'utm_medium=website&' +
`utm_content=${location.hostname}&` +
'utm_campaign=poweredby';
- ReactDOM.render(
- ,
- poweredByContainer
- );
+ let templateData = {
+ cssClasses: poweredByCSSClasses,
+ url
+ };
+
+ let template = poweredBy.template;
+ let stringNode;
+
+ if (isString(template)) {
+ stringNode = Hogan.compile(template).render(templateData);
+ }
+ if (isFunction(template)) {
+ stringNode = template(templateData);
+ }
+
+ // Crossbrowser way to create a DOM node from a string. We wrap in
+ // a `span` to make sure we have one and only one node.
+ let tmpNode = document.createElement('div');
+ tmpNode.innerHTML = `${stringNode.trim()}`;
+ let htmlNode = tmpNode.firstChild;
+
+ input.parentNode.insertBefore(htmlNode, input.nextSibling);
},
init: function({state, helper, onHistoryChange}) {
let isInputTargeted = container.tagName === 'INPUT';