Skip to content

Commit b327dbc

Browse files
committed
feat(searchBox): Add wrapInput option
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)`.
1 parent 4e3d04d commit b327dbc

File tree

4 files changed

+114
-30
lines changed

4 files changed

+114
-30
lines changed

README.md

+14-8
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,13 @@ instantsearch({
321321
/**
322322
* Instantiate a searchbox
323323
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
324-
* @param {string} [options.placeholder='Search here'] Input's placeholder
325-
* @param {Object} [options.cssClass] CSS classes to add to the input
324+
* @param {string} [options.placeholder] Input's placeholder
325+
* @param {Object} [options.cssClasses] CSS classes to add
326+
* @param {string} [options.cssClasses.root] CSS class to add to the wrapping div (if wrapInput set to `true`)
327+
* @param {string} [options.cssClasses.input] CSS class to add to the input
328+
* @param {string} [options.cssClasses.poweredBy] CSS class to add to the poweredBy element
326329
* @param {boolean} [poweredBy=false] Show a powered by Algolia link below the input
330+
* @param {boolean} [wrapInput=true] Wrap the input in a div.ais-search-box
327331
* @param {boolean|string} [autofocus='auto'] autofocus on the input
328332
* @return {Object}
329333
*/
@@ -342,7 +346,6 @@ search.addWidget(
342346
instantsearch.widgets.searchBox({
343347
container: '#search-box',
344348
placeholder: 'Search for products',
345-
cssClass: 'form-control',
346349
poweredBy: true
347350
})
348351
);
@@ -351,15 +354,18 @@ search.addWidget(
351354
#### Styling
352355

353356
```html
354-
<input class="ais-search-box--input">
355-
<!-- With poweredBy: true -->
356-
<div class="ais-search-box--powered-by">
357-
Powered by
358-
<a class="ais-search-box--powered-by-link">Algolia</a>
357+
<div class="ais-search-box">
358+
<input class="ais-search-box--input">
359+
<div class="ais-search-box--powered-by">
360+
Powered by
361+
<a class="ais-search-box--powered-by-link">Algolia</a>
362+
</div>
359363
</div>
360364
```
361365

362366
```css
367+
.ais-search-box {
368+
}
363369
.ais-search-box--input {
364370
}
365371
.ais-search-box--powered-by {

themes/default.css

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/* SEARCH BOX */
2+
.ais-search-box {
3+
}
24
.ais-search-box--input {
35
}
46
.ais-search-box--powered-by {

widgets/search-box/__tests__/search-box-test.js

+47-1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,52 @@ describe('search-box()', () => {
102102
});
103103
});
104104

105+
context('wraps the input in a div', () => {
106+
it('when targeting a div', () => {
107+
// Given
108+
container = document.createElement('div');
109+
widget = searchBox({container});
110+
111+
// When
112+
widget.init(initialState, helper);
113+
114+
// Then
115+
var wrapper = container.querySelectorAll('div.ais-search-box')[0];
116+
var input = container.querySelectorAll('input')[0];
117+
118+
expect(wrapper.contains(input)).toEqual(true);
119+
expect(wrapper.getAttribute('class')).toEqual('ais-search-box');
120+
});
121+
122+
it('when targeting an input', () => {
123+
// Given
124+
container = createHTMLNodeFromString('<input />');
125+
widget = searchBox({container});
126+
127+
// When
128+
widget.init(initialState, helper);
129+
130+
// Then
131+
var wrapper = container.parentNode;
132+
expect(wrapper.getAttribute('class')).toEqual('ais-search-box');
133+
});
134+
135+
it('can be disabled with wrapInput:false', () => {
136+
// Given
137+
container = document.createElement('div');
138+
widget = searchBox({container, wrapInput: false});
139+
140+
// When
141+
widget.init(initialState, helper);
142+
143+
// Then
144+
var wrapper = container.querySelectorAll('div.ais-search-box');
145+
var input = container.querySelectorAll('input')[0];
146+
expect(wrapper.length).toEqual(0);
147+
expect(container.firstChild).toEqual(input);
148+
});
149+
});
150+
105151
context('adds a PoweredBy', () => {
106152
beforeEach(() => {
107153
container = document.createElement('div');
@@ -124,7 +170,7 @@ describe('search-box()', () => {
124170
var input;
125171
beforeEach(() => {
126172
container = document.createElement('div');
127-
input = document.createElement('input');
173+
input = createHTMLNodeFromString('<input />');
128174
input.focus = sinon.spy();
129175
});
130176

widgets/search-box/search-box.js

+51-21
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ var cx = require('classnames');
1010
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
1111
* @param {string} [options.placeholder] Input's placeholder
1212
* @param {Object} [options.cssClasses] CSS classes to add
13+
* @param {string} [options.cssClasses.root] CSS class to add to the wrapping div (if wrapInput set to `true`)
1314
* @param {string} [options.cssClasses.input] CSS class to add to the input
1415
* @param {string} [options.cssClasses.poweredBy] CSS class to add to the poweredBy element
1516
* @param {boolean} [poweredBy=false] Show a powered by Algolia link below the input
17+
* @param {boolean} [wrapInput=true] Wrap the input in a div.ais-search-box
1618
* @param {boolean|string} [autofocus='auto'] autofocus on the input
1719
* @return {Object}
1820
*/
@@ -21,10 +23,11 @@ function searchBox({
2123
placeholder = '',
2224
cssClasses = {},
2325
poweredBy = false,
26+
wrapInput = true,
2427
autofocus = 'auto'
2528
}) {
2629
if (!container) {
27-
throw new Error('Usage: searchBox({container[, placeholder, cssClasses.{input,poweredBy}, poweredBy, autofocus]})');
30+
throw new Error('Usage: searchBox({container[, placeholder, cssClasses.{input,poweredBy}, poweredBy, wrapInput, autofocus]})');
2831
}
2932

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

3740
return {
38-
// Hook on an existing input, or add one if none targeted
3941
getInput: function() {
42+
// Returns reference to targeted input if present, or create a new one
4043
if (container.tagName === 'INPUT') {
4144
return container;
4245
}
43-
return container.appendChild(document.createElement('input'));
46+
return document.createElement('input');
4447
},
45-
init: function(initialState, helper) {
48+
wrapInput: function(input) {
49+
// Wrap input in a .ais-search-box div
50+
var wrapper = document.createElement('div');
51+
wrapper.classList.add(cx(bem(null), cssClasses.root));
52+
wrapper.appendChild(input);
53+
return wrapper;
54+
},
55+
addDefaultAttributesToInput: function(input, query) {
4656
var defaultAttributes = {
4757
autocapitalize: 'off',
4858
autocomplete: 'off',
@@ -51,9 +61,8 @@ function searchBox({
5161
role: 'textbox',
5262
spellcheck: 'false',
5363
type: 'text',
54-
value: initialState.query
64+
value: query
5565
};
56-
var input = this.getInput();
5766

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

6675
// Add classes
6776
input.classList.add(cx(bem('input'), cssClasses.input));
77+
},
78+
addPoweredBy: function(input) {
79+
var PoweredBy = require('../../components/PoweredBy/PoweredBy.js');
80+
var poweredByContainer = document.createElement('div');
81+
input.parentNode.insertBefore(poweredByContainer, input.nextSibling);
82+
var poweredByCssClasses = {
83+
root: cx(bem('powered-by'), cssClasses.poweredBy),
84+
link: bem('powered-by-link')
85+
};
86+
ReactDOM.render(
87+
<PoweredBy
88+
cssClasses={poweredByCssClasses}
89+
/>,
90+
poweredByContainer
91+
);
92+
},
93+
init: function(initialState, helper) {
94+
var isInputTargeted = container.tagName === 'INPUT';
95+
var input = this.getInput();
6896

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

103+
if (isInputTargeted) {
104+
// To replace the node, we need to create an intermediate node
105+
var placeholderNode = document.createElement('div');
106+
input.parentNode.insertBefore(placeholderNode, input);
107+
let parentNode = input.parentNode;
108+
let wrappedInput = wrapInput ? this.wrapInput(input) : input;
109+
parentNode.replaceChild(wrappedInput, placeholderNode);
110+
} else {
111+
let wrappedInput = wrapInput ? this.wrapInput(input) : input;
112+
container.appendChild(wrappedInput);
113+
}
114+
73115
// Optional "powered by Algolia" widget
74116
if (poweredBy) {
75-
var PoweredBy = require('../../components/PoweredBy/PoweredBy.js');
76-
var poweredByContainer = document.createElement('div');
77-
input.parentNode.appendChild(poweredByContainer);
78-
var poweredByCssClasses = {
79-
root: cx(bem('powered-by'), cssClasses.poweredBy),
80-
link: bem('powered-by-link')
81-
};
82-
ReactDOM.render(
83-
<PoweredBy
84-
cssClasses={poweredByCssClasses}
85-
/>,
86-
poweredByContainer
87-
);
117+
this.addPoweredBy(input);
88118
}
89119

120+
// Update value when query change outside of the input
90121
helper.on('change', function(state) {
91122
if (input !== document.activeElement && input.value !== state.query) {
92123
input.value = state.query;
93124
}
94125
});
95126

96-
if (autofocus === true ||
97-
autofocus === 'auto' && helper.state.query === '') {
127+
if (autofocus === true || autofocus === 'auto' && helper.state.query === '') {
98128
input.focus();
99129
}
100130
}

0 commit comments

Comments
 (0)