instantsearch.js
Version:
InstantSearch.js is a JavaScript library for building performant and instant search experiences with Algolia.
613 lines (482 loc) • 27.2 kB
JavaScript
"use strict";
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _algoliasearchHelper = _interopRequireDefault(require("algoliasearch-helper"));
var _events = _interopRequireDefault(require("@algolia/events"));
var _index = _interopRequireWildcard(require("../widgets/index/index.js"));
var _version = _interopRequireDefault(require("./version.js"));
var _createHelpers = _interopRequireDefault(require("./createHelpers.js"));
var _index2 = require("./utils/index.js");
var _createRouterMiddleware = require("../middlewares/createRouterMiddleware.js");
var _createMetadataMiddleware = require("../middlewares/createMetadataMiddleware.js");
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var withUsage = (0, _index2.createDocumentationMessageGenerator)({
name: 'instantsearch'
});
function defaultCreateURL() {
return '#';
} // this purposely breaks typescript's type inference to ensure it's not used
// as it's used for a default parameter for example
// source: https://github.com/Microsoft/TypeScript/issues/14829#issuecomment-504042546
/**
* The actual implementation of the InstantSearch. This is
* created using the `instantsearch` factory function.
* It emits the 'render' event every time a search is done
*/
var InstantSearch = /*#__PURE__*/function (_EventEmitter) {
_inherits(InstantSearch, _EventEmitter);
var _super = _createSuper(InstantSearch);
function InstantSearch(options) {
var _this;
_classCallCheck(this, InstantSearch);
_this = _super.call(this);
_defineProperty(_assertThisInitialized(_this), "client", void 0);
_defineProperty(_assertThisInitialized(_this), "indexName", void 0);
_defineProperty(_assertThisInitialized(_this), "insightsClient", void 0);
_defineProperty(_assertThisInitialized(_this), "onStateChange", null);
_defineProperty(_assertThisInitialized(_this), "helper", void 0);
_defineProperty(_assertThisInitialized(_this), "mainHelper", void 0);
_defineProperty(_assertThisInitialized(_this), "mainIndex", void 0);
_defineProperty(_assertThisInitialized(_this), "started", void 0);
_defineProperty(_assertThisInitialized(_this), "templatesConfig", void 0);
_defineProperty(_assertThisInitialized(_this), "renderState", {});
_defineProperty(_assertThisInitialized(_this), "_stalledSearchDelay", void 0);
_defineProperty(_assertThisInitialized(_this), "_searchStalledTimer", void 0);
_defineProperty(_assertThisInitialized(_this), "_isSearchStalled", void 0);
_defineProperty(_assertThisInitialized(_this), "_initialUiState", void 0);
_defineProperty(_assertThisInitialized(_this), "_initialResults", void 0);
_defineProperty(_assertThisInitialized(_this), "_createURL", void 0);
_defineProperty(_assertThisInitialized(_this), "_searchFunction", void 0);
_defineProperty(_assertThisInitialized(_this), "_mainHelperSearch", void 0);
_defineProperty(_assertThisInitialized(_this), "middleware", []);
_defineProperty(_assertThisInitialized(_this), "sendEventToInsights", void 0);
_defineProperty(_assertThisInitialized(_this), "scheduleSearch", (0, _index2.defer)(function () {
if (_this.started) {
_this.mainHelper.search();
}
}));
_defineProperty(_assertThisInitialized(_this), "scheduleRender", (0, _index2.defer)(function () {
if (!_this.mainHelper.hasPendingRequests()) {
clearTimeout(_this._searchStalledTimer);
_this._searchStalledTimer = null;
_this._isSearchStalled = false;
}
_this.mainIndex.render({
instantSearchInstance: _assertThisInitialized(_this)
});
_this.emit('render');
}));
_defineProperty(_assertThisInitialized(_this), "onInternalStateChange", (0, _index2.defer)(function () {
var nextUiState = _this.mainIndex.getWidgetUiState({});
_this.middleware.forEach(function (_ref) {
var instance = _ref.instance;
instance.onStateChange({
uiState: nextUiState
});
});
}));
var _options$indexName = options.indexName,
indexName = _options$indexName === void 0 ? null : _options$indexName,
numberLocale = options.numberLocale,
_options$initialUiSta = options.initialUiState,
initialUiState = _options$initialUiSta === void 0 ? {} : _options$initialUiSta,
_options$routing = options.routing,
routing = _options$routing === void 0 ? null : _options$routing,
searchFunction = options.searchFunction,
_options$stalledSearc = options.stalledSearchDelay,
stalledSearchDelay = _options$stalledSearc === void 0 ? 200 : _options$stalledSearc,
_options$searchClient = options.searchClient,
searchClient = _options$searchClient === void 0 ? null : _options$searchClient,
_options$insightsClie = options.insightsClient,
insightsClient = _options$insightsClie === void 0 ? null : _options$insightsClie,
_options$onStateChang = options.onStateChange,
onStateChange = _options$onStateChang === void 0 ? null : _options$onStateChang;
if (indexName === null) {
throw new Error(withUsage('The `indexName` option is required.'));
}
if (searchClient === null) {
throw new Error(withUsage('The `searchClient` option is required.'));
}
if (typeof searchClient.search !== 'function') {
throw new Error("The `searchClient` must implement a `search` method.\n\nSee: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js/");
}
if (typeof searchClient.addAlgoliaAgent === 'function') {
searchClient.addAlgoliaAgent("instantsearch.js (".concat(_version.default, ")"));
}
process.env.NODE_ENV === 'development' ? (0, _index2.warning)(insightsClient === null, "`insightsClient` property has been deprecated. It is still supported in 4.x releases, but not further. It is replaced by the `insights` middleware.\n\nFor more information, visit https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/how-to/send-click-and-conversion-events-with-instantsearch/js/") : void 0;
if (insightsClient && typeof insightsClient !== 'function') {
throw new Error(withUsage('The `insightsClient` option should be a function.'));
}
process.env.NODE_ENV === 'development' ? (0, _index2.warning)(!options.searchParameters, "The `searchParameters` option is deprecated and will not be supported in InstantSearch.js 4.x.\n\nYou can replace it with the `configure` widget:\n\n```\nsearch.addWidgets([\n configure(".concat(JSON.stringify(options.searchParameters, null, 2), ")\n]);\n```\n\nSee ").concat((0, _index2.createDocumentationLink)({
name: 'configure'
}))) : void 0;
_this.client = searchClient;
_this.insightsClient = insightsClient;
_this.indexName = indexName;
_this.helper = null;
_this.mainHelper = null;
_this.mainIndex = (0, _index.default)({
indexName: indexName
});
_this.onStateChange = onStateChange;
_this.started = false;
_this.templatesConfig = {
helpers: (0, _createHelpers.default)({
numberLocale: numberLocale
}),
compileOptions: {}
};
_this._stalledSearchDelay = stalledSearchDelay;
_this._searchStalledTimer = null;
_this._isSearchStalled = false;
_this._createURL = defaultCreateURL;
_this._initialUiState = initialUiState;
_this._initialResults = null;
if (searchFunction) {
_this._searchFunction = searchFunction;
}
_this.sendEventToInsights = _index2.noop;
if (routing) {
var routerOptions = typeof routing === 'boolean' ? undefined : routing;
_this.use((0, _createRouterMiddleware.createRouterMiddleware)(routerOptions));
}
if ((0, _createMetadataMiddleware.isMetadataEnabled)()) {
_this.use((0, _createMetadataMiddleware.createMetadataMiddleware)());
}
return _this;
}
/**
* Hooks a middleware into the InstantSearch lifecycle.
*/
_createClass(InstantSearch, [{
key: "use",
value: function use() {
var _this2 = this;
for (var _len = arguments.length, middleware = new Array(_len), _key = 0; _key < _len; _key++) {
middleware[_key] = arguments[_key];
}
var newMiddlewareList = middleware.map(function (fn) {
var newMiddleware = _objectSpread({
subscribe: _index2.noop,
unsubscribe: _index2.noop,
onStateChange: _index2.noop
}, fn({
instantSearchInstance: _this2
}));
_this2.middleware.push({
creator: fn,
instance: newMiddleware
});
return newMiddleware;
}); // If the instance has already started, we directly subscribe the
// middleware so they're notified of changes.
if (this.started) {
newMiddlewareList.forEach(function (m) {
m.subscribe();
});
}
return this;
}
/**
* Removes a middleware from the InstantSearch lifecycle.
*/
}, {
key: "unuse",
value: function unuse() {
for (var _len2 = arguments.length, middlewareToUnuse = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
middlewareToUnuse[_key2] = arguments[_key2];
}
this.middleware.filter(function (m) {
return middlewareToUnuse.includes(m.creator);
}).forEach(function (m) {
return m.instance.unsubscribe();
});
this.middleware = this.middleware.filter(function (m) {
return !middlewareToUnuse.includes(m.creator);
});
return this;
} // @major we shipped with EXPERIMENTAL_use, but have changed that to just `use` now
}, {
key: "EXPERIMENTAL_use",
value: function EXPERIMENTAL_use() {
process.env.NODE_ENV === 'development' ? (0, _index2.warning)(false, 'The middleware API is now considered stable, so we recommend replacing `EXPERIMENTAL_use` with `use` before upgrading to the next major version.') : void 0;
return this.use.apply(this, arguments);
}
/**
* Adds a widget to the search instance.
* A widget can be added either before or after InstantSearch has started.
* @param widget The widget to add to InstantSearch.
*
* @deprecated This method will still be supported in 4.x releases, but not further. It is replaced by `addWidgets([widget])`.
*/
}, {
key: "addWidget",
value: function addWidget(widget) {
process.env.NODE_ENV === 'development' ? (0, _index2.warning)(false, 'addWidget will still be supported in 4.x releases, but not further. It is replaced by `addWidgets([widget])`') : void 0;
return this.addWidgets([widget]);
}
/**
* Adds multiple widgets to the search instance.
* Widgets can be added either before or after InstantSearch has started.
* @param widgets The array of widgets to add to InstantSearch.
*/
}, {
key: "addWidgets",
value: function addWidgets(widgets) {
if (!Array.isArray(widgets)) {
throw new Error(withUsage('The `addWidgets` method expects an array of widgets. Please use `addWidget`.'));
}
if (widgets.some(function (widget) {
return typeof widget.init !== 'function' && typeof widget.render !== 'function';
})) {
throw new Error(withUsage('The widget definition expects a `render` and/or an `init` method.'));
}
this.mainIndex.addWidgets(widgets);
return this;
}
/**
* Removes a widget from the search instance.
* @deprecated This method will still be supported in 4.x releases, but not further. It is replaced by `removeWidgets([widget])`
* @param widget The widget instance to remove from InstantSearch.
*
* The widget must implement a `dispose()` method to clear its state.
*/
}, {
key: "removeWidget",
value: function removeWidget(widget) {
process.env.NODE_ENV === 'development' ? (0, _index2.warning)(false, 'removeWidget will still be supported in 4.x releases, but not further. It is replaced by `removeWidgets([widget])`') : void 0;
return this.removeWidgets([widget]);
}
/**
* Removes multiple widgets from the search instance.
* @param widgets Array of widgets instances to remove from InstantSearch.
*
* The widgets must implement a `dispose()` method to clear their states.
*/
}, {
key: "removeWidgets",
value: function removeWidgets(widgets) {
if (!Array.isArray(widgets)) {
throw new Error(withUsage('The `removeWidgets` method expects an array of widgets. Please use `removeWidget`.'));
}
if (widgets.some(function (widget) {
return typeof widget.dispose !== 'function';
})) {
throw new Error(withUsage('The widget definition expects a `dispose` method.'));
}
this.mainIndex.removeWidgets(widgets);
return this;
}
/**
* Ends the initialization of InstantSearch.js and triggers the
* first search. This method should be called after all widgets have been added
* to the instance of InstantSearch.js. InstantSearch.js also supports adding and removing
* widgets after the start as an **EXPERIMENTAL** feature.
*/
}, {
key: "start",
value: function start() {
var _this3 = this;
if (this.started) {
throw new Error(withUsage('The `start` method has already been called once.'));
} // This Helper is used for the queries, we don't care about its state. The
// states are managed at the `index` level. We use this Helper to create
// DerivedHelper scoped into the `index` widgets.
// In Vue InstantSearch' hydrate, a main helper gets set before start, so
// we need to respect this helper as a way to keep all listeners correct.
var mainHelper = this.mainHelper || (0, _algoliasearchHelper.default)(this.client, this.indexName);
mainHelper.search = function () {
// This solution allows us to keep the exact same API for the users but
// under the hood, we have a different implementation. It should be
// completely transparent for the rest of the codebase. Only this module
// is impacted.
return mainHelper.searchOnlyWithDerivedHelpers();
};
if (this._searchFunction) {
// this client isn't used to actually search, but required for the helper
// to not throw errors
var fakeClient = {
search: function search() {
return new Promise(_index2.noop);
}
};
this._mainHelperSearch = mainHelper.search.bind(mainHelper);
mainHelper.search = function () {
var mainIndexHelper = _this3.mainIndex.getHelper();
var searchFunctionHelper = (0, _algoliasearchHelper.default)(fakeClient, mainIndexHelper.state.index, mainIndexHelper.state);
searchFunctionHelper.once('search', function (_ref2) {
var state = _ref2.state;
mainIndexHelper.overrideStateWithoutTriggeringChangeEvent(state);
_this3._mainHelperSearch();
}); // Forward state changes from `searchFunctionHelper` to `mainIndexHelper`
searchFunctionHelper.on('change', function (_ref3) {
var state = _ref3.state;
mainIndexHelper.setState(state);
});
_this3._searchFunction(searchFunctionHelper);
return mainHelper;
};
} // Only the "main" Helper emits the `error` event vs the one for `search`
// and `results` that are also emitted on the derived one.
mainHelper.on('error', function (_ref4) {
var error = _ref4.error;
// If an error is emitted, it is re-thrown by events. In previous versions
// we emitted {error}, which is thrown as:
// "Uncaught, unspecified \"error\" event. ([object Object])"
// To avoid breaking changes, we make the error available in both
// `error` and `error.error`
// @MAJOR emit only error
error.error = error;
_this3.emit('error', error);
});
this.mainHelper = mainHelper;
this.middleware.forEach(function (_ref5) {
var instance = _ref5.instance;
instance.subscribe();
});
this.mainIndex.init({
instantSearchInstance: this,
parent: null,
uiState: this._initialUiState
});
if (this._initialResults) {
var originalScheduleSearch = this.scheduleSearch; // We don't schedule a first search when initial results are provided
// because we already have the results to render. This skips the initial
// network request on the browser on `start`.
this.scheduleSearch = (0, _index2.defer)(_index2.noop); // We also skip the initial network request when widgets are dynamically
// added in the first tick (that's the case in all the framework-based flavors).
// When we add a widget to `index`, it calls `scheduleSearch`. We can rely
// on our `defer` util to restore the original `scheduleSearch` value once
// widgets are added to hook back to the regular lifecycle.
(0, _index2.defer)(function () {
_this3.scheduleSearch = originalScheduleSearch;
})();
} // We only schedule a search when widgets have been added before `start()`
// because there are listeners that can use these results.
// This is especially useful in framework-based flavors that wait for
// dynamically-added widgets to trigger a network request. It avoids
// having to batch this initial network request with the one coming from
// `addWidgets()`.
// Later, we could also skip `index()` widgets and widgets that don't read
// the results, but this is an optimization that has a very low impact for now.
else if (this.mainIndex.getWidgets().length > 0) {
this.scheduleSearch();
} // Keep the previous reference for legacy purpose, some pattern use
// the direct Helper access `search.helper` (e.g multi-index).
this.helper = this.mainIndex.getHelper(); // track we started the search if we add more widgets,
// to init them directly after add
this.started = true;
}
/**
* Removes all widgets without triggering a search afterwards. This is an **EXPERIMENTAL** feature,
* if you find an issue with it, please
* [open an issue](https://github.com/algolia/instantsearch.js/issues/new?title=Problem%20with%20dispose).
* @return {undefined} This method does not return anything
*/
}, {
key: "dispose",
value: function dispose() {
this.scheduleSearch.cancel();
this.scheduleRender.cancel();
clearTimeout(this._searchStalledTimer);
this.removeWidgets(this.mainIndex.getWidgets());
this.mainIndex.dispose(); // You can not start an instance two times, therefore a disposed instance
// needs to set started as false otherwise this can not be restarted at a
// later point.
this.started = false; // The helper needs to be reset to perform the next search from a fresh state.
// If not reset, it would use the state stored before calling `dispose()`.
this.removeAllListeners();
this.mainHelper.removeAllListeners();
this.mainHelper = null;
this.helper = null;
this.middleware.forEach(function (_ref6) {
var instance = _ref6.instance;
instance.unsubscribe();
});
}
}, {
key: "scheduleStalledRender",
value: function scheduleStalledRender() {
var _this4 = this;
if (!this._searchStalledTimer) {
this._searchStalledTimer = setTimeout(function () {
_this4._isSearchStalled = true;
_this4.scheduleRender();
}, this._stalledSearchDelay);
}
}
}, {
key: "setUiState",
value: function setUiState(uiState) {
if (!this.mainHelper) {
throw new Error(withUsage('The `start` method needs to be called before `setUiState`.'));
} // We refresh the index UI state to update the local UI state that the
// main index passes to the function form of `setUiState`.
this.mainIndex.refreshUiState();
var nextUiState = typeof uiState === 'function' ? uiState(this.mainIndex.getWidgetUiState({})) : uiState;
var setIndexHelperState = function setIndexHelperState(indexWidget) {
var nextIndexUiState = nextUiState[indexWidget.getIndexId()] || {};
if (process.env.NODE_ENV === 'development') {
(0, _index2.checkIndexUiState)({
index: indexWidget,
indexUiState: nextIndexUiState
});
}
indexWidget.getHelper().setState(indexWidget.getWidgetSearchParameters(indexWidget.getHelper().state, {
uiState: nextIndexUiState
}));
indexWidget.getWidgets().filter(_index.isIndexWidget).forEach(setIndexHelperState);
};
setIndexHelperState(this.mainIndex);
this.scheduleSearch();
this.onInternalStateChange();
}
}, {
key: "getUiState",
value: function getUiState() {
if (this.started) {
// We refresh the index UI state to make sure changes from `refine` are taken in account
this.mainIndex.refreshUiState();
}
return this.mainIndex.getWidgetUiState({});
}
}, {
key: "createURL",
value: function createURL() {
var nextState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (!this.started) {
throw new Error(withUsage('The `start` method needs to be called before `createURL`.'));
}
return this._createURL(nextState);
}
}, {
key: "refresh",
value: function refresh() {
if (!this.mainHelper) {
throw new Error(withUsage('The `start` method needs to be called before `refresh`.'));
}
this.mainHelper.clearCache().search();
}
}]);
return InstantSearch;
}(_events.default);
var _default = InstantSearch;
exports.default = _default;