Guides / Building Search UI / Going further

You are reading the documentation for Angular InstantSearch v3, which is in beta. You can find the v2 documentation here.

This is an advanced guide. If you’ve never used Angular InstantSearch, you should follow the getting-started first.

You can find the result of this guide on the Angular InstantSearch repository.

Angular InstantSearch is compatible with server-side rendering, starting from Angular 5. We provide an API that’s easy to use with @angular/universal modules.

For simplicity, this guide uses the @angular/universal-starter boilerplate, a minimal Angular starter for Universal JavaScript using TypeScript and webpack.

How server-side rendering works

The server-side rendering uses two concepts from @angular/universal modules:

  • TransferState: Will cache the first request made to Algolia from your server in order to avoid replicating it when the Angular application starts on the client side.
  • preboot: Will avoid the first rendering of your Angular application on the client side and will start it from the HTML markup sent by the server.

In order to assemble all the pieces you will need to write some code in your own application and instantiate Angular InstantSearch. Let’s dive into the code!

Setup

First, clone the @angular/universal-starter boilerplate:

1
2
3
git clone git@github.com:angular/universal-starter.git [your-app-name]
cd [your-app-name]
npm install

Then, add all the necessary dependencies: preboot, Angular InstantSearch, and InstantSearch.js, which Angular InstantSearch depends on:

1
npm install preboot angular-instantsearch@beta instantsearch.js@3

Angular InstantSearch v3 is not yet compatible with algoliasearch v4.

Now you have all the requirements to start developing your universal Angular InstantSearch application!

Switch to webpack for compiling (required for Angular 6 and later)

Up until Angular 5, the Angular Universal Starter used webpack to compile the server code. Angular 6+ versions use TypeScript. In this example, webpack is still necessary. If you’re running this example with Angular 6+, you need some extra setup.

Install the packages first:

1
npm install -D webpack-cli webpack

Create a file called webpack.server.config.js.

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
// webpack.server.config.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'none',
  entry: {
    // This is our Express server for Dynamic universal
    server: './server.ts',
    // This is an example of static pre-rendering (generative)
    prerender: './prerender.ts'
  },
  target: 'node',
  resolve: { extensions: ['.ts', '.js'] },
  // Make sure we include all `node_modules`, etc.
  externals: [/node_modules/],
  output: {
    // Sets the output at the root of the `dist` folder
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
  },
  plugins: [
    new webpack.ContextReplacementPlugin(
      // fixes error: "WARNING Critical dependency: the request of a dependency is an expression"
      /(.+)?angular(\\|\/)core(.+)?/,
      path.join(__dirname, 'src'), // location of your src
      {} // a map of your routes
    ),
    new webpack.ContextReplacementPlugin(
      // fixes error: 'WARNING Critical dependency: the request of a dependency is an expression'
      /(.+)?express(\\|\/)(.+)?/,
      path.join(__dirname, 'src'),
      {}
    )
  ]
};

In your server.ts and prerender.ts files, update the path of your server app. Replace this line:

1
2
- const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./server/main');
+ const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');

In your package.json file, change the compile:server command into:

1
2
3
4
5
6
{
  "scripts": {
    "compile:server":
      "webpack --config webpack.server.config.js --progress --colors"
  }
}

Run yarn build:ssr && yarn serve:ssr to ensure that everything still works

Angular Universal modules

Once you’ve installed the dependencies you will need to add the TransferState, preboot and HttpClient modules into src/app/app.module.ts:

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
// src/app.module.ts

import {
  BrowserModule,
  BrowserTransferStateModule, // Add this line
} from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http'; // Add this line
import { PrebootModule } from 'preboot'; // Add this line
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { TransferHttpCacheModule } from '@nguniversal/common';
import { NgAisModule } from 'angular-instantsearch';

@NgModule({
  declarations: [AppComponent, HomeComponent],
  imports: [
    BrowserModule.withServerTransition({ appId: 'my-app' }),
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full' },
      { path: 'lazy', loadChildren: './lazy/lazy.module#LazyModule' },
      { path: 'lazy/nested', loadChildren: './lazy/lazy.module#LazyModule' },
    ]),
    TransferHttpCacheModule,
    PrebootModule.withConfig({ appRoot: 'app-root' }), // Add this line
    BrowserTransferStateModule, // Add this line
    HttpClientModule // Add this line
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

And voilà, you have the requirements and your are now ready to plug Angular InstantSearch into your universal Angular application!

Transfer the request object to your server-side Angular Application

In order to get the query of the client request into your Angular application you need to provide the original request object you receive into the express server. Open ./server.ts and replace this block:

1
2
3
4
5
6
app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

By this one:

1
2
3
4
5
6
7
8
9
10
app.engine('html', (_, options, callback) => {
  const engine = ngExpressEngine({
    bootstrap: AppServerModuleNgFactory,
    providers: [
      { provide: 'request', useFactory: () => options.req, deps: [] },
      provideModuleMap(LAZY_MODULE_MAP)
    ]
  });
  engine(_, options, callback);
});

Now on server-side rendering we can have access to the request object by using the injector. We will see how to do that in the next chapter.

Introducing Angular InstantSearch

First, you need to import the Angular InstantSearch module into your application like you will do in any Angular application. (If you don’t know how to do this, please read the following part in the getting started guide). The only difference is on how you configure <ais-instantsearch> component.

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
// src/app.module.ts

import {
  BrowserModule,
  BrowserTransferStateModule,
} from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { PrebootModule } from 'preboot';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { TransferHttpCacheModule } from '@nguniversal/common';
import { NgAisModule } from 'angular-instantsearch'; // Add this line

@NgModule({
  declarations: [AppComponent, HomeComponent],
  imports: [
    BrowserModule.withServerTransition({ appId: 'my-app' }),
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full' },
      { path: 'lazy', loadChildren: './lazy/lazy.module#LazyModule' },
      { path: 'lazy/nested', loadChildren: './lazy/lazy.module#LazyModule' },
    ]),
    TransferHttpCacheModule,
    PrebootModule.withConfig({ appRoot: 'app-root' }),
    BrowserTransferStateModule,
    HttpClientModule,
    NgAisModule.forRoot(), // Add this line
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

This will be our starting component. For simplicity you can re-use the Home component from the universal starter boilerplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/home/home.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'home',
  template: `
    <ais-instantsearch [config]="config">
    </ais-instantsearch>
  `
})
export class HomeComponent {
  public config: any;

  constructor() {
    this.config = {
      appId: "latency",
      apiKey: "6be0576ff61c053d5f9a3225e2a90f76",
      indexName: "instant_search"
    }
  }
}

We now need to import the TransferState, HttpClient, Injector and PLATFORM_ID components into our constructor. Let’s update the component’s code.

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
// src/home/home.component.ts

import { Component, Inject, Injector, PLATFORM_ID } from '@angular/core';
import {
  createSSRSearchClient,
  parseServerRequest,
} from 'angular-instantsearch';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { isPlatformServer } from '@angular/common';

@Component({
  selector: 'app-home',
  template: `
    <ais-instantsearch [config]="config">
      <ais-search-box></ais-search-box>
      <ais-hits>
        <ng-template let-hits="hits">
          <ol class="ais-Hits-list">
            <li *ngFor="let hit of hits; index as i" class="ais-Hits-item">
              <div class="hit-name">
                <ais-highlight attribute="name" [hit]="hit"></ais-highlight>
              </div>
            </li>
          </ol>
        </ng-template>
      </ais-hits>
    </ais-instantsearch>
  `,
})
export class HomeComponent {
  public config: any;
  constructor(
    private httpClient: HttpClient,
    private transferState: TransferState,
    private injector: Injector,
    @Inject(PLATFORM_ID) private platformId: Object
  ) {
    const req = isPlatformServer(this.platformId)
      ? this.injector.get('request')
      : undefined;

    const searchParameters = parseServerRequest(req);

    const searchClient = createSSRSearchClient({
      makeStateKey,
      HttpHeaders,
      transferState: this.transferState,
      httpClient: this.httpClient,
      appId: 'latency',
      apiKey: '6be0576ff61c053d5f9a3225e2a90f76',
    });

    this.config = {
      searchParameters,
      indexName: 'instant_search',
      searchClient,
      routing: true
    };
  }
}

Note that we also updated config with the modules we provided to angular-instantsearch, so we can make Algolia API requests on the server:

Congratulations! You can now add more Angular InstantSearch widgets on your search page component and run:

1
2
> npm run build:ssr && npm run serve:ssr
> open http://localhost:4000

You have now fully universal Angular InstantSearch application running on your server and browser! If you want to run the application directly we provide a complete example that you can find on the angular-instantsearch GitHub repository.

Did you find this page helpful?