Upgrading Angular InstantSearch
Migration from v4 to InstantSearch.js
You can use InstantSearch.js directly from Angular, as described in the Using InstantSearch.js in an Angular app guide. Migrating to that approach can be done as follows:
Replace the root component
Instead of the <ais-instantsearch>
component, use the InstantSearch
class from InstantSearch.js and expose this from a service.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { Injectable } from '@angular/core';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import InstantSearch from 'instantsearch.js/es/lib/InstantSearch';
import type { IndexWidget, Widget } from 'instantsearch.js';
const searchClient = algoliasearch(
'YourApplicationID',
'YourSearchOnlyAPIKey'
);
@Injectable({
providedIn: 'root',
})
export class InstantSearchService {
public instantSearchInstance: InstantSearch;
constructor(router: Router) {
this.instantSearchInstance = new InstantSearch({
searchClient,
indexName: 'instant_search',
future: { preserveSharedStateOnUnmount: true },
});
}
start() {
this.instantSearchInstance.start();
}
addWidgets(widgets: Array<IndexWidget | Widget>) {
this.instantSearchInstance.addWidgets(widgets);
}
removeWidgets(widgets: Array<IndexWidget | Widget>) {
this.instantSearchInstance.removeWidgets(widgets);
}
}
This can then be used in your app:
1
2
3
4
5
6
7
8
9
10
11
12
13
import { Component } from '@angular/core';
import { InstantSearchService } from '../instant-search.service';
@Component({
selector: 'app-search',
standalone: true,
templateUrl: './search.component.html',
})
export class SearchComponent {
ngAfterContentInit() {
this.InstantSearchService.start();
}
}
Replace widgets
Instead of using Angular InstantSearch widgets, use InstantSearch.js connectors:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { Component } from '@angular/core';
import { InstantSearchService } from '../instant-search.service';
import { BaseHit } from 'instantsearch.js';
import { connectHits, connectSearchBox } from 'instantsearch.js/es/connectors';
@Component({
selector: 'app-search',
standalone: true,
templateUrl: './search.component.html',
})
export class SearchComponent {
public hits: BaseHit[] = [];
public refine: (query: string) => void;
public query: string;
constructor(private InstantSearchService: InstantSearchService) {
this.InstantSearchService.addWidgets([
connectSearchBox({ refine, query }) => {
this.refine = refine;
this.query = query;
})({
// ...widgetParameters
}),
connectHits(({ hits }) => {
this.hits = hits;
})({}),
]);
}
ngAfterContentInit() {
this.InstantSearchService.start();
}
public search(event: Event) {
this.refine!((event.target as HTMLInputElement).value);
}
}
Replace custom widgets
If you have custom widgets, you can use InstantSearch.js connectors to create them, replacing the Angular InstantSearch BaseWidget
class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
-import { Component, Inject, forwardRef, Optional } from '@angular/core'
-import {
- TypedBaseWidget,
- NgAisInstantSearch,
- NgAisIndex,
-} from 'angular-instantsearch'
+import { Component } from '@angular/core'
+import { InstantSearchService } from '../instant-search.service'
import connectMenu, {
- MenuConnectorParams,
MenuWidgetDescription,
} from 'instantsearch.js/es/connectors/menu/connectMenu'
@Component({
selector: 'app-menu-select',
standalone: true,
template: `
<select class="menu-select" (change)="state.refine($event.target.value)">
<option
*ngFor="let item of state.items"
[value]="item.value"
[selected]="item.isRefined"
>
{{ item.label }}
</option>
</select>
`,
})
-export class MenuSelectComponent extends TypedBaseWidget<
- MenuWidgetDescription,
- MenuConnectorParams
-> {
+export class MenuSelectComponent {
public state: MenuWidgetDescription['renderState'] = {
items: [],
refine: () => {},
createURL: () => '#',
canRefine: false,
isShowingMore: false,
canToggleShowMore: false,
toggleShowMore: () => {},
sendEvent: () => {},
}
- constructor(
- @Inject(forwardRef(() => NgAisIndex))
- @Optional()
- public parentIndex: NgAisIndex,
- @Inject(forwardRef(() => NgAisInstantSearch))
- public instantSearchInstance: NgAisInstantSearch
- ) {
- super('MenuSelect')
- }
-
- public ngOnInit() {
- this.createWidget(connectMenu, { attribute: 'categories' })
- super.ngOnInit()
+ constructor(private InstantSearchService: InstantSearchService) {
+ this.InstantSearchService.addWidgets([
+ connectMenu(state => {
+ this.state = state
+ })({ attribute: 'categories' }),
+ ])
}
}
Migration from v3 to v4
Upgrade Angular to 10.x.x or later
Angular InstantSearch follows the support policy of Angular. Support for Angular < 10 is deprecated.
Use InstantSearch.js types
Angular InstantSearch now fully relies on InstantSearch.js types. If you rely on the types exposed by Angular InstantSearch, you need to replace them with their InstantSearch.js equivalent.
1
2
- import type { CurrentRefinementsState } from 'angular-instantsearch';
+ import type { CurrentRefinementsRenderState } from 'instantsearch.js/es/current-refinements/connectCurrentRefinements';
InstantSearch.js is now a dependency
InstantSearch.js is no longer referenced as a peer dependency. Make sure to uninstall your version and use the one installed with Angular InstantSearch.
Favor using TypedBaseWidget
over BaseWidget
A type-capable alternative to BaseWidget
called TypedBaseWidget
was introduced. This is now the preferred way to create a custom widgets using the InstantSearch.js Connector API.
This is an optional change.
1
2
3
4
5
6
7
+ import type {
+ CurrentRefinementsWidgetDescription,
+ CurrentRefinementsConnectorParams,
+ } from 'instantsearch.js/es/current-refinements/connectCurrentRefinements';
- export class CustomCurrentRefinements extends BaseWidget { /* ... */ };
+ export class CustomCurrentRefinements extends TypedBaseWidget<CurrentRefinementsWidgetDescription, CurrentRefinementsConnectorParams> { /* ... */ };
Custom widgets require parentIndex
and instantSearchInstance
Since Algolia now supports a new “parent” widget called index-widget, you need to change the constructor of your custom widget if it extends the BaseWidget
class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Component, Inject, forwardRef, OnInit, Input } from '@angular/core'
import { BaseWidget, NgAisInstantSearch } from 'angular-instantsearch'
import { connectRefinementList } from 'instantsearch.js/es/connectors'
@Component({
selector: 'my-widget',
template: '',
})
export class MyWidget extends BaseWidget implements OnInit {
@Input() public attribute: string
constructor(
+ @Inject(forwardRef(() => NgAisIndex))
+ @Optional()
+ public parentIndex: NgAisIndex,
@Inject(forwardRef(() => NgAisInstantSearch))
- public instantSearchParent: NgAisInstantSearch
+ public instantSearchInstance: NgAisInstantSearch
) {
super('MyWidget')
}
public ngOnInit() {
this.createWidget(connectRefinementList, {
attribute: this.attribute,
})
super.ngOnInit()
}
}
Custom widgets using the helper
API
This release includes version 3 of the algoliasearch-helper
package. If you only use the built-in widgets or connectors, nothing changes for you.
This version of algoliasearch-helper
no longer includes Lodash, which significantly reduces its bundle size (from 27.5 KB to 9.1 KB Gzipped). If you’re using any methods from the helper
, searchParameters
or searchResults
in custom widgets, please refer to the detailed change log of the package.
Custom widgets using a custom connector
Because the algoliasearch-helper
package uses version 3, you have to replace the getConfiguration
lifecycle with getWidgetSearchParameters
and getWidgetState
.
This also means that your custom widget takes part in the routing. You can exclude it from the URL via stateMapping
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const connector = () => ({
getConfiguration(searchParams) {
return {
disjunctiveFacets: ['myAttribute'],
}
},
getWidgetSearchParameters(searchParameters, { uiState }) {
return searchParameters.addDisjunctiveFacetRefinement(
'myAttribute',
uiState.myWidgetName.myAttribute
)
},
getWidgetState(uiState, { searchParameters }) {
return {
...uiState,
myWidgetName: {
myAttribute: searchParameters.getDisjunctiveRefinements('myAttribute'),
},
}
},
})
Becomes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const connector = () => ({
getWidgetSearchParameters(searchParameters, { uiState }) {
return searchParameters
.addDisjunctiveFacet('myAttribute')
.addDisjunctiveFacetRefinement(
'myAttribute',
uiState.myWidgetName.myAttribute
)
},
getWidgetState(uiState, { searchParameters }) {
return {
...uiState,
myWidgetName: {
myAttribute: searchParameters.getDisjunctiveRefinements('myAttribute'),
},
}
},
})
searchParameters
option for ais-instantsearch
The searchParameters
option has been removed from the instantsearch widget. However, you can replace it with the configure widget, like this:
1
2
3
4
5
6
7
<ais-instantsearch
- [config]="{searchParameters: { hitsPerPage: 5 } }"
+ [config]="{}"
>
<!-- children -->
+ <ais-configure [searchParameters]="{ hitsPerPage: 5 }"></ais-configure>
</ais-instantsearch>
You can now add initialUiState
to your instantsearch
widget. This overwrites specific search parameters that would otherwise be set during widget instantiation.
initialUiState
is only taken into account if a widget owning that state is mounted. A warning is displayed in development mode explaining which widget needs to be added for the UI state to have an effect.
A good example of this is the refinement-list widget:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<ais-instantsearch
[config]="{
- searchParameters: {
- disjunctiveFacets: ['brand'],
- disjunctiveFacetsRefinements: {
- brand: ['Apple'],
- },
- },
+ initialUiState: {
+ refinementList: {
+ brand: ['Apple'],
+ },
+ },
}"
>
<!-- children -->
<ais-refinement-list attribute="brands"></ais-refinement-list>
</ais-instantsearch>
If you have a widget that you don’t want to display values for but still want to refine using this method, you can use a “Virtual Widget” like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component, Inject, forwardRef, OnInit, Input } from '@angular/core'
import { BaseWidget, NgAisInstantSearch } from 'angular-instantsearch'
import { connectRefinementList } from 'instantsearch.js/es/connectors'
@Component({
selector: 'virtual-refinement-list',
template: '',
})
export class VirtualRefinementList extends BaseWidget implements OnInit {
@Input() public attribute: string
public ngOnInit() {
this.createWidget(connectRefinementList, {
attribute: this.attribute,
})
super.ngOnInit()
}
}
Routing
Even if you aren’t using multi-index search, the way in which UI state is stored has changed. It used to look like this:
1
2
3
4
{
"query": "value",
"page": 5
}
It now looks like this:
1
2
3
4
5
6
{
"indexName": {
"query": "value",
"page": 5
}
}
If you are using the default state mapping (simpleStateMapping
) with the current version, you can replace it with singleIndexStateMapping('yourIndexName')
. You have to change the code as followed:
1
2
3
4
5
6
7
8
searchConfig = {
indexName: 'myIndex',
routing: {
- stateMapping: simple(),
+ stateMapping: singleIndex('myIndex'),
}
// ...
}
If you are using a custom state mapping, you have to loop over the outer level of the index
widget and add this extra level to the routeToState
. You can check the source for a reference on how to implement this.
For example, a stateMapping
that maps a few properties would change like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Before
const stateMapping = {
stateToRoute(uiState) {
return {
query: uiState.query,
page: uiState.page,
// ...
}
},
routeToState(routeState) {
return {
query: routeState.query,
page: routeState.page,
// ...
}
},
}
// After
const stateMapping = {
stateToRoute(uiState) {
const indexUiState = uiState[indexName]
return {
query: indexUiState.query,
page: indexUiState.page,
// ...
}
},
routeToState(routeState) {
return {
[indexName]: {
query: routeState.query,
page: routeState.page,
// ...
},
}
},
}
Configure
The configure
widget is now included in the UI state. If you want to exclude it from the URL you can use the default stateMapping
s or exclude it in your custom state mapping. A good reason to exclude the configure widget from the UI state is to prevent users from adding any search parameters.
You must exclude this widget in both the stateToRoute
, to keep it from appearing in the URL and routeToState
, so that the URL doesn’t apply to the state.
Check the stateMapping
source code for implementation details.
Other breaking changes
If you were using any InstantSearch.js APIs directly, you have to consult the InstantSearch.js v3 to v4 migration guide for any breaking changes.
Migration from v2 to v3
InstantSearch.js is now a peer dependency
$
npm install --save instantsearch@^3
RefinementList: “Show more” button controlled by showMore
property
The display of the “Show more” button is no longer inferred by the showMoreLimit
and the internal state property canToggleShowMore
.
It’s now controlled by a new input property: showMore: boolean = false
.
If you want to use the “Show more” button on the refinement-list, make sure to set the showMore
property to true
1
2
<ais-refinement-list [showMore]="true">
</ais-refinement-list>
NumericSelector widget has been removed
Use instead numeric-menu.
InfiniteHits: “Show More” button CSS class was renamed
The “Show more” button CSS class has been renamed from showMore
to loadMore
.
InstantSearch: appId
, apiKey
and createAlgoliaClient
have been removed
This enforces the usage of the searchClient
option.
createAlgoliaClient
has been removed as there is no longer a use case for it.
If you have (something like) the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component({
template: `
<ais-instantsearch [config]="config">
<!-- more widgets -->
</ais-instantsearch>
`
})
export class AppComponent {
config = {
appId: 'YourApplicationID',
apiKey: 'YourSearchOnlyAPIKey',
/* ... */
};
}
Replace it with:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { liteClient as algoliasearch } from 'algoliasearch/lite';
@Component({
template: `
<ais-instantsearch [config]="config">
<!-- more widgets -->
</ais-instantsearch>
`
})
export class AppComponent {
config = {
searchClient: algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey'),
/* ... */
};
}
SortBy items
property changed
The name
key in items
has been renamed to value
.
Replace this:
1
2
3
4
5
6
7
<ais-sort-by
[items]="[
{ name: 'products', label: 'Most relevant' },
{ name: 'products_price_desc', label: 'Highest price' }
]"
>
</ais-sort-by>
With this:
1
2
3
4
5
6
7
<ais-sort-by
[items]="[
{ value: 'instant_search', label: 'Featured' },
{ value: 'instant_search_price_asc', label: 'Price asc.' },
{ value: 'instant_search_price_desc', label: 'Price desc.' }
]"
></ais-sort-by>
Migration from v0 to v1
Angular 2 and 4 support drop
To support Angular CLI 6 support for older versions was dropped, the only supported versions are now: 5, 6 and 7.
You can update your Angular 4 application by following this guide: https://update.angular.io/.
If you are using Angular +6 you will need an extra step, polyfill process.env
by adding in your src/polyfill.ts
:
1
(window as any).process = {env: {}};
Widget prefix
The ng-
prefix is considered reserved for core implementations into Angular so it was dropped.
All the widgets are now only starting with ais-
:
1
2
3
4
5
6
7
8
9
10
11
<ais-instantsearch [config]="{...}">
<ais-hits>
<ng-template let-hits="hits">
<div *ngFor="let hit of hits">
Hit {{hit.objectID}}:
<ais-highlight attribute="name" [hit]="hit">
</ais-highlight>
</div>
</ng-template>
</ais-hits>
</ais-instantsearch>
Server-side rendering
- The
createSSRAlgoliaClient
until has been renamed tocreateSSRSearchClient
- You can’t use the new
routing: true
option on<ais-instantsearch>
widget until resolution of preboot#82