Skip to content

Commit

Permalink
feat(searchBox): Add wrapInput option
Browse files Browse the repository at this point in the history
Fixes: #352

BREAKING CHANGE: The `input` used by the search-box widget is now
wrapped in a `<div class="ais-search-box">` by default. This can be
turned off with `wrapInput: false`.

This PR is a bit long, I had to do some minor refactoring to keep the
new code understandable. I simply split the large `init` method into
calls to smaller methods.

There is some vanilla JS DOM manipulation involved to handle all the
possible cases: targeting an `input` or a `div`, adding or not the
`poweredBy`, adding or not the wrapping div.

Note that there is no `targetNode.insertAfter(newNode)` method, so
I had to resort to the old trick of `parentNode.insertBefore(newNode,
targetNode.nextSibling)`.
  • Loading branch information
pixelastic committed Oct 27, 2015
1 parent 4e3d04d commit b327dbc
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 30 deletions.
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,13 @@ instantsearch({
/**
* Instantiate a searchbox
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
* @param {string} [options.placeholder='Search here'] Input's placeholder
* @param {Object} [options.cssClass] CSS classes to add to the input
* @param {string} [options.placeholder] Input's placeholder
* @param {Object} [options.cssClasses] CSS classes to add
* @param {string} [options.cssClasses.root] CSS class to add to the wrapping div (if wrapInput set to `true`)
* @param {string} [options.cssClasses.input] CSS class to add to the input
* @param {string} [options.cssClasses.poweredBy] CSS class to add to the poweredBy element
* @param {boolean} [poweredBy=false] Show a powered by Algolia link below the input
* @param {boolean} [wrapInput=true] Wrap the input in a div.ais-search-box
* @param {boolean|string} [autofocus='auto'] autofocus on the input
* @return {Object}
*/
Expand All @@ -342,7 +346,6 @@ search.addWidget(
instantsearch.widgets.searchBox({
container: '#search-box',
placeholder: 'Search for products',
cssClass: 'form-control',
poweredBy: true
})
);
Expand All @@ -351,15 +354,18 @@ search.addWidget(
#### Styling

```html
<input class="ais-search-box--input">
<!-- With poweredBy: true -->
<div class="ais-search-box--powered-by">
Powered by
<a class="ais-search-box--powered-by-link">Algolia</a>
<div class="ais-search-box">
<input class="ais-search-box--input">
<div class="ais-search-box--powered-by">
Powered by
<a class="ais-search-box--powered-by-link">Algolia</a>
</div>
</div>
```

```css
.ais-search-box {
}
.ais-search-box--input {
}
.ais-search-box--powered-by {
Expand Down
2 changes: 2 additions & 0 deletions themes/default.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* SEARCH BOX */
.ais-search-box {
}
.ais-search-box--input {
}
.ais-search-box--powered-by {
Expand Down
48 changes: 47 additions & 1 deletion widgets/search-box/__tests__/search-box-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,52 @@ describe('search-box()', () => {
});
});

context('wraps the input in a div', () => {
it('when targeting a div', () => {
// Given
container = document.createElement('div');
widget = searchBox({container});

// When
widget.init(initialState, helper);

// Then
var wrapper = container.querySelectorAll('div.ais-search-box')[0];
var input = container.querySelectorAll('input')[0];

expect(wrapper.contains(input)).toEqual(true);
expect(wrapper.getAttribute('class')).toEqual('ais-search-box');
});

it('when targeting an input', () => {
// Given
container = createHTMLNodeFromString('<input />');
widget = searchBox({container});

// When
widget.init(initialState, helper);

// Then
var wrapper = container.parentNode;
expect(wrapper.getAttribute('class')).toEqual('ais-search-box');
});

it('can be disabled with wrapInput:false', () => {
// Given
container = document.createElement('div');
widget = searchBox({container, wrapInput: false});

// When
widget.init(initialState, helper);

// Then
var wrapper = container.querySelectorAll('div.ais-search-box');
var input = container.querySelectorAll('input')[0];
expect(wrapper.length).toEqual(0);
expect(container.firstChild).toEqual(input);
});
});

context('adds a PoweredBy', () => {
beforeEach(() => {
container = document.createElement('div');
Expand All @@ -124,7 +170,7 @@ describe('search-box()', () => {
var input;
beforeEach(() => {
container = document.createElement('div');
input = document.createElement('input');
input = createHTMLNodeFromString('<input />');
input.focus = sinon.spy();
});

Expand Down
72 changes: 51 additions & 21 deletions widgets/search-box/search-box.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ var cx = require('classnames');
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
* @param {string} [options.placeholder] Input's placeholder
* @param {Object} [options.cssClasses] CSS classes to add
* @param {string} [options.cssClasses.root] CSS class to add to the wrapping div (if wrapInput set to `true`)
* @param {string} [options.cssClasses.input] CSS class to add to the input
* @param {string} [options.cssClasses.poweredBy] CSS class to add to the poweredBy element
* @param {boolean} [poweredBy=false] Show a powered by Algolia link below the input
* @param {boolean} [wrapInput=true] Wrap the input in a div.ais-search-box
* @param {boolean|string} [autofocus='auto'] autofocus on the input
* @return {Object}
*/
Expand All @@ -21,10 +23,11 @@ function searchBox({
placeholder = '',
cssClasses = {},
poweredBy = false,
wrapInput = true,
autofocus = 'auto'
}) {
if (!container) {
throw new Error('Usage: searchBox({container[, placeholder, cssClasses.{input,poweredBy}, poweredBy, autofocus]})');
throw new Error('Usage: searchBox({container[, placeholder, cssClasses.{input,poweredBy}, poweredBy, wrapInput, autofocus]})');
}

container = utils.getContainerNode(container);
Expand All @@ -35,14 +38,21 @@ function searchBox({
}

return {
// Hook on an existing input, or add one if none targeted
getInput: function() {
// Returns reference to targeted input if present, or create a new one
if (container.tagName === 'INPUT') {
return container;
}
return container.appendChild(document.createElement('input'));
return document.createElement('input');
},
init: function(initialState, helper) {
wrapInput: function(input) {
// Wrap input in a .ais-search-box div
var wrapper = document.createElement('div');
wrapper.classList.add(cx(bem(null), cssClasses.root));
wrapper.appendChild(input);
return wrapper;
},
addDefaultAttributesToInput: function(input, query) {
var defaultAttributes = {
autocapitalize: 'off',
autocomplete: 'off',
Expand All @@ -51,9 +61,8 @@ function searchBox({
role: 'textbox',
spellcheck: 'false',
type: 'text',
value: initialState.query
value: query
};
var input = this.getInput();

// Overrides attributes if not already set
forEach(defaultAttributes, (value, key) => {
Expand All @@ -65,36 +74,57 @@ function searchBox({

// Add classes
input.classList.add(cx(bem('input'), cssClasses.input));
},
addPoweredBy: function(input) {
var PoweredBy = require('../../components/PoweredBy/PoweredBy.js');
var poweredByContainer = document.createElement('div');
input.parentNode.insertBefore(poweredByContainer, input.nextSibling);
var poweredByCssClasses = {
root: cx(bem('powered-by'), cssClasses.poweredBy),
link: bem('powered-by-link')
};
ReactDOM.render(
<PoweredBy
cssClasses={poweredByCssClasses}
/>,
poweredByContainer
);
},
init: function(initialState, helper) {
var isInputTargeted = container.tagName === 'INPUT';
var input = this.getInput();

// Add all the needed attributes and listeners to the input
this.addDefaultAttributesToInput(input, initialState.query);
input.addEventListener('keyup', () => {
helper.setQuery(input.value).search();
});

if (isInputTargeted) {
// To replace the node, we need to create an intermediate node
var placeholderNode = document.createElement('div');
input.parentNode.insertBefore(placeholderNode, input);
let parentNode = input.parentNode;
let wrappedInput = wrapInput ? this.wrapInput(input) : input;
parentNode.replaceChild(wrappedInput, placeholderNode);
} else {
let wrappedInput = wrapInput ? this.wrapInput(input) : input;
container.appendChild(wrappedInput);
}

// Optional "powered by Algolia" widget
if (poweredBy) {
var PoweredBy = require('../../components/PoweredBy/PoweredBy.js');
var poweredByContainer = document.createElement('div');
input.parentNode.appendChild(poweredByContainer);
var poweredByCssClasses = {
root: cx(bem('powered-by'), cssClasses.poweredBy),
link: bem('powered-by-link')
};
ReactDOM.render(
<PoweredBy
cssClasses={poweredByCssClasses}
/>,
poweredByContainer
);
this.addPoweredBy(input);
}

// Update value when query change outside of the input
helper.on('change', function(state) {
if (input !== document.activeElement && input.value !== state.query) {
input.value = state.query;
}
});

if (autofocus === true ||
autofocus === 'auto' && helper.state.query === '') {
if (autofocus === true || autofocus === 'auto' && helper.state.query === '') {
input.focus();
}
}
Expand Down

0 comments on commit b327dbc

Please sign in to comment.