This time we will learn how to create a small Angular Universal CLI project that is using the WordPress REST service to retrieve and display the title and content of a WordPress blog post.

Angular Universal CLI combines the Universal features like server-side rendering (see Angular 4 Universal: Boosting Performance through Server Side Rendering) with a state-of-the-art handling of Angular projects by Angular CLI.

On a previous blog post, I have given an introduction to server-side rendering via Angular Universal. There, we had cloned a Universal seed file and added a REST client that has retrieved and displayed the content of a WordPress blog post. Later, I have found out that I have used a seed project that does not support Angular CLI. Angular CLI is the more modern way of handling Angular projects. In this blog post, we learn how to port (or create) the end-to-end tests and feature code to a seed project that has been created with Angular Universal CLI. With that, we have access to all ng commands provided by Angular CLI.

Step 0: Get Access to a Docker Host

The instructions will work on any Docker host with 2 GB available RAM. If you do not have access to a Docker host yet, I recommend following the step 0 instructions on my JHipster post.

Step 1: Create Aliases for often used Commands

In this tutorial, we will use following pre-defined aliases and functions for often used commands:

# functions
cli() {
 docker run -it --rm -w /app -v $(pwd):/app --net=host oveits/angular-cli:1.4.3 $@
}
npm() {
 cli npm $@
  if [[ "$@" == "i" ]] || [[ "$@" == "install" ]] ; then
    sudo chown -R $(whoami) .
  fi
}

# aliases
alias ng='cli ng $@'
alias protractor='docker run -it --privileged --rm --net=host -v /dev/shm:/dev/shm -v $(pwd):/protractor webnicer/protractor-headless $@'
alias own='sudo chown -R $(whoami) .'

Step 2: Fork & Clone Universal Starter from GIT

The Github project /universal-starter is a seed project that has been created with Angular CLI. Fork the project and clone it to your local machine (use your own Github name instead of mine, oveits):

$ git clone https://github.com/oveits/universal-starter
$ cd universal-starter

Step 3: Create e2e Test

The universal starter comes with no e2e tests. Let us change that now.

Step 3.1: Add e2e Folder from your existing project

If you have already created specs in an existing project, then copy them into a new e2e folder our project

cd project folder
mkdir e2e
cp <whereever you have your existing specs> e2e/

In my case, I intend to add the functionality I have developed on my blog post Behavior-Driven Angular – Part 2: Inserting REST Data as “innerHTML” into a Web Application. This is, where I have copied the content of the e2e folder from and changed it a little. Namely:

//e2e/app.e2e-spec.ts
import { AppPage } from './app.po';
import { browser, by, element } from 'protractor';

describe('Blog', () => {

  beforeEach(() => {
    browser.get('/2017/06/24/consuming-a-restful-web-service-with-angular-4/');
  });

  const blog_title = element(by.id('blog_title'));
  const blog_content = element(by.id('blog_content'));

  it('should display the blog title as header 1 and id="blog_title"', () => {
    expect(blog_title.getText()).toEqual('Angular 4 Hello World Quickstart');
  });

  it('should display the blog content', () => {
    expect(blog_content.getInnerHtml()).toMatch(/^<p>In this hello world style tutorial, we will follow a step by step guide to a working Angular 4 application./);
  });

});

With this end-to-end test, we expect that the page on /2017/06/13/angular-4-hello-world-with-quickstart/ is displaying the title and content of my WordPress blog Angular 4 Hello World Quickstart.

Step 3.2 Run the end-to-end Tests

Let us try to run e2e tests by issuing following command on the root of the project:

$ protractor

However, we are missing some ingredients. Therefore we will get following response:

**you must either specify a configuration file or at least 3 options. See below for the options:

Usage: protractor [configFile] [options]
configFile defaults to protractor.conf.js
The [options] object will override values from the config file.
See the reference config for a full list of options.

Options:
  --help                                 Print Protractor help menu
  --version                              Print Protractor version
  --browser, --capabilities.browserName  Browsername, e.g. chrome or firefox
  --seleniumAddress                      A running selenium address to use
  --seleniumSessionId                    Attaching an existing session id
  --seleniumServerJar                    Location of the standalone selenium jar file
  --seleniumPort                         Optional port for the selenium standalone server
  --baseUrl                              URL to prepend to all relative paths
  --rootElement                          Element housing ng-app, if not html or body
  --specs                                Comma-separated list of files to test
  --exclude                              Comma-separated list of files to exclude
  --verbose                              Print full spec names
  --stackTrace                           Print stack trace on error
  --params                               Param object to be passed to the tests
  --framework                            Test framework to use: jasmine, mocha, or custom
  --resultJsonOutputFile                 Path to save JSON test result
  --troubleshoot                         Turn on troubleshooting output
  --elementExplorer                      Interactively test Protractor commands
  --debuggerServerPort                   Start a debugger server at specified port instead of repl

Step 3.2.1 Add protractor.conf.js File

We need to add a protractor.conf.js file:

// protractor.conf.js
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts

const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './e2e/**/*.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:8080/',
  useAllAngular2AppRoots: true,
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  onPrepare() {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};

This is the default protractor configuration file that comes with Angular CLI (you will find the file in the base folder of a new Angular CLI project created with ‘ng new-project’). However, we have adapted the parts in blue:

  1. Our application is running on port 8080, so we have changed the default port 4200 to 8080
  2. We have added the useAllAngular2AppRoots: true directive, which should fix an issue I had found on a previous blog Behavior-Driven Angular – part 1: Consuming a RESTful Web Service with Angular 4

Now again:

$ protractor
[20:21:07] E/configParser - Error code: 105
[20:21:07] E/configParser - Error message: failed loading configuration file ./protractor.conf.js
[20:21:07] E/configParser - Error: Cannot find module 'jasmine-spec-reporter'
 at Function.Module._resolveFilename (module.js:469:15)
 at Function.Module._load (module.js:417:25)
 at Module.require (module.js:497:17)
 at require (internal/module.js:20:19)
 at Object.<anonymous> (/protractor/protractor.conf.js:4:26)
 at Module._compile (module.js:570:32)
 at Object.Module._extensions..js (module.js:579:10)
 at Module.load (module.js:487:32)
 at tryModuleLoad (module.js:446:12)
 at Function.Module._load (module.js:438:3)

We need to install jasmine reporter package.

Step 3.2.2 Adapt the package.json File

Instead of a lot of trial&error, which package might be missing, I have decided to copy all missing packages found in in the devDependencies section of a new Angular CLI project into my package.json. This has lead to following additions in blue:

{
  "name": "ng-universal-demo",
  "version": "0.0.0",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/angular/universal-starter.git"
  },
  "contributors": [
    "AngularClass <hello@angularclass.com>",
    "PatrickJS <patrick@angularclass.com>",
    "Jeff Whelpley <jeff@gethuman.com>",
    "Jeff Cross <crossj@google.com>",
    "Mark Pieszak <mpieszak84@gmail.com>",
    "Jason Jean <jasonjean1993@gmail.com>",
    "Fabian Wiles <fabian.wiles@gmail.com>"
  ],
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "start:dynamic": "npm run build:dynamic && npm run serve:dynamic",
    "start:static": "npm run build:static && npm run serve:static",
    "build": "ng build",
    "build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false",
    "build:static": "npm run build:client-and-server-bundles && npm run webpack:server && npm run generate:static",
    "build:dynamic": "npm run build:client-and-server-bundles && npm run webpack:server",
    "generate:static": "cd dist && node prerender",
    "webpack:server": "webpack --config webpack.server.config.js --progress --colors",
    "serve:static": "cd dist/browser && http-server",
    "serve:dynamic": "node dist/server"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^4.2.4",
    "@angular/common": "^4.2.4",
    "@angular/compiler": "^4.2.4",
    "@angular/core": "^4.2.4",
    "@angular/forms": "^4.2.4",
    "@angular/http": "^4.2.4",
    "@angular/platform-browser": "^4.2.4",
    "@angular/platform-browser-dynamic": "^4.2.4",
    "@angular/platform-server": "^4.3.6",
    "@angular/router": "^4.2.4",
    "@nguniversal/express-engine": "^1.0.0-beta.3",
    "@nguniversal/module-map-ngfactory-loader": "^1.0.0-beta.3",
    "core-js": "^2.4.1",
    "rxjs": "^5.4.2",
    "zone.js": "^0.8.14"
  },
  "devDependencies": {
    "@angular/cli": "^1.3.0",
    "@angular/compiler-cli": "^4.2.4",
    "@angular/language-service": "^4.2.4",
    "@types/jasmine": "~2.5.53",
    "@types/jasminewd2": "~2.0.2",
    "@types/node": "^8.0.30",
    "codelyzer": "~3.1.1",
    "jasmine-core": "~2.6.2",
    "jasmine-spec-reporter": "~4.1.0",
    "karma": "~1.7.0",
    "karma-chrome-launcher": "~2.1.1",
    "karma-cli": "~1.0.1",
    "karma-coverage-istanbul-reporter": "^1.2.1",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.1.2",
    "ts-node": "~3.2.0",
    "tslint": "~5.3.2",
    "cpy-cli": "^1.0.1",
    "http-server": "^0.10.0",
    "reflect-metadata": "^0.1.10",
    "ts-loader": "^2.3.7",
    "typescript": "~2.3.3"
  }
}

Step 3.2.3 Install the new Modules

We need to re-run the installation:

npm i

Step 3.2.4 Re-run the end-to-end Tests

We re-run the protractor test. We still get a bunch of error messages:

$ protractor
[17:39:28] I/direct - Using ChromeDriver directly...
[17:39:28] I/launcher - Running 1 instances of WebDriver
Jasmine started
[17:39:42] E/protractor - Could not find Angular on page http://localhost:8080/blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/ : retries looking for angular exceeded

  Blog
    ✗ should display the blog title as header 1 and id="blog_title"
      - Failed: Angular could not be found on the page http://localhost:8080/blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/. If this is not an Angular application, you may need to turn off waiting for Angular. Please see https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular-on-page-load
          at /usr/local/lib/node_modules/protractor/built/browser.js:506:23
          at ManagedPromise.invokeCallback_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1379:14)
          at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14)
          at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21)
          at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2775:27)
          at /usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:639:7
          at process._tickCallback (internal/process/next_tick.js:103:7)
      From: Task: Run beforeEach in control flow
          at Object. (/usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:79:14)
      From asynchronous test:
      Error
          at Suite. (/protractor/e2e/app.e2e-spec.ts:7:3)
          at Object. (/protractor/e2e/app.e2e-spec.ts:5:1)
          at Module._compile (module.js:570:32)
          at Module.m._compile (/protractor/node_modules/ts-node/src/index.ts:392:23)
          at Module._extensions..js (module.js:579:10)
          at Object.require.extensions.(anonymous function) [as .ts] (/protractor/node_modules/ts-node/src/index.ts:395:12)
      - Failed: Error while waiting for Protractor to sync with the page: "window.getAllAngularTestabilities is not a function"
          at /usr/local/lib/node_modules/protractor/built/browser.js:272:23
          at ManagedPromise.invokeCallback_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1379:14)
          at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14)
          at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21)
          at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2775:27)
          at /usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:639:7
          at process._tickCallback (internal/process/next_tick.js:103:7)Error
          at ElementArrayFinder.applyAction_ (/usr/local/lib/node_modules/protractor/built/element.js:461:27)
          at ElementArrayFinder._this.(anonymous function) [as getText] (/usr/local/lib/node_modules/protractor/built/element.js:103:30)
          at ElementFinder.(anonymous function) [as getText] (/usr/local/lib/node_modules/protractor/built/element.js:829:22)
          at Object. (/protractor/e2e/app.e2e-spec.ts:15:34)
          at /usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:94:23
          at new ManagedPromise (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1082:7)
          at controlFlowExecute (/usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:80:18)
          at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14)
          at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21)
          at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2820:25)
      From: Task: Run it("should display the blog title as header 1 and id="blog_title"") in control flow
          at Object. (/usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:79:14)
          at /usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:103:16
          at ManagedPromise.invokeCallback_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1379:14)
          at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14)
          at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21)
      From asynchronous test:
      Error
          at Suite. (/protractor/e2e/app.e2e-spec.ts:14:3)
          at Object. (/protractor/e2e/app.e2e-spec.ts:5:1)
          at Module._compile (module.js:570:32)
          at Module.m._compile (/protractor/node_modules/ts-node/src/index.ts:392:23)
          at Module._extensions..js (module.js:579:10)
          at Object.require.extensions.(anonymous function) [as .ts] (/protractor/node_modules/ts-node/src/index.ts:395:12)
[17:39:52] E/protractor - Could not find Angular on page http://localhost:8080/blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/ : retries looking for angular exceeded
    ✗ should display the blog content
      - Failed: Angular could not be found on the page http://localhost:8080/blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/. If this is not an Angular application, you may need to turn off waiting for Angular. Please see https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular-on-page-load
          at /usr/local/lib/node_modules/protractor/built/browser.js:506:23
          at ManagedPromise.invokeCallback_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1379:14)
          at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14)
          at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21)
          at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2775:27)
          at /usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:639:7
          at process._tickCallback (internal/process/next_tick.js:103:7)
      From: Task: Run beforeEach in control flow
          at Object. (/usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:79:14)
      From asynchronous test:
      Error
          at Suite. (/protractor/e2e/app.e2e-spec.ts:7:3)
          at Object. (/protractor/e2e/app.e2e-spec.ts:5:1)
          at Module._compile (module.js:570:32)
          at Module.m._compile (/protractor/node_modules/ts-node/src/index.ts:392:23)
          at Module._extensions..js (module.js:579:10)
          at Object.require.extensions.(anonymous function) [as .ts] (/protractor/node_modules/ts-node/src/index.ts:395:12)
      - Failed: Error while waiting for Protractor to sync with the page: "window.getAllAngularTestabilities is not a function"
          at /usr/local/lib/node_modules/protractor/built/browser.js:272:23
          at ManagedPromise.invokeCallback_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1379:14)
          at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14)
          at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21)
          at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2775:27)
          at /usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:639:7
          at process._tickCallback (internal/process/next_tick.js:103:7)Error
          at ElementArrayFinder.applyAction_ (/usr/local/lib/node_modules/protractor/built/element.js:461:27)
          at ElementArrayFinder._this.(anonymous function) [as getText] (/usr/local/lib/node_modules/protractor/built/element.js:103:30)
          at ElementFinder.(anonymous function) [as getText] (/usr/local/lib/node_modules/protractor/built/element.js:829:22)
          at Object. (/protractor/e2e/app.e2e-spec.ts:20:25)
          at /usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:94:23
          at new ManagedPromise (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1082:7)
          at controlFlowExecute (/usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:80:18)
          at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14)
          at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21)
          at asyncRun (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2820:25)
      From: Task: Run it("should display the blog content") in control flow
          at Object. (/usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:79:14)
          at /usr/local/lib/node_modules/protractor/node_modules/jasminewd2/index.js:103:16
          at ManagedPromise.invokeCallback_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:1379:14)
          at TaskQueue.execute_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2913:14)
          at TaskQueue.executeNext_ (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/promise.js:2896:21)
      From asynchronous test:
      Error
          at Suite. (/protractor/e2e/app.e2e-spec.ts:19:3)
          at Object. (/protractor/e2e/app.e2e-spec.ts:5:1)
          at Module._compile (module.js:570:32)
          at Module.m._compile (/protractor/node_modules/ts-node/src/index.ts:392:23)
          at Module._extensions..js (module.js:579:10)
          at Object.require.extensions.(anonymous function) [as .ts] (/protractor/node_modules/ts-node/src/index.ts:395:12)

**************************************************
*                    Failures                    *
**************************************************

1) Blog should display the blog title as header 1 and id="blog_title"
  - Failed: Angular could not be found on the page http://localhost:8080/blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/. If this is not an Angular application, you may need to turn off waiting for Angular. Please see https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular-on-page-load
  - Failed: Error while waiting for Protractor to sync with the page: "window.getAllAngularTestabilities is not a function"

2) Blog should display the blog content
  - Failed: Angular could not be found on the page http://localhost:8080/blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/. If this is not an Angular application, you may need to turn off waiting for Angular. Please see https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular-on-page-load
  - Failed: Error while waiting for Protractor to sync with the page: "window.getAllAngularTestabilities is not a function"

Executed 2 of 2 specs (2 FAILED) in 21 secs.
[17:39:52] I/launcher - 0 instance(s) of WebDriver still running
[17:39:52] I/launcher - chrome #01 failed 2 test(s)
[17:39:52] I/launcher - overall: 2 failed spec(s)
[17:39:52] E/launcher - Process exited with error code 1

What is it telling us? Okay, I have forgotten to start the application, before we started the test. Let us correct this now.

Step 3.2.5: Run the Application

Let us run the application as a static universal project as described in this Readme:

$ cd my-project-root
$ npm run start:static
npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~prestart:static: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~start:static: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 start:static /app
> npm run build:static && npm run serve:static

npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~prebuild:static: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~build:static: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 build:static /app
> npm run build:client-and-server-bundles && npm run webpack:server && npm run generate:static

npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~prebuild:client-and-server-bundles: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~build:client-and-server-bundles: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 build:client-and-server-bundles /app
> ng build --prod && ng build --prod --app 1 --output-hashing=false

Date: 2017-10-28T19:27:48.583Z
Hash: c5eeae31051a6225ca92
Time: 15759ms
chunk {0} 0.7ce44253311853d97e73.chunk.js () 1.02 kB {2}  [rendered]
chunk {1} polyfills.80bfeb690703af4fafee.bundle.js (polyfills) 66.1 kB {5} [initial] [rendered]
chunk {2} main.2f46e8d1609d5ba758f8.bundle.js (main) 5.04 kB {4} [initial] [rendered]
chunk {3} styles.d41d8cd98f00b204e980.bundle.css (styles) 0 bytes {5} [initial] [rendered]
chunk {4} vendor.74a477af39cd1230db04.bundle.js (vendor) 305 kB [initial] [rendered]
chunk {5} inline.baceea59185918784cfc.bundle.js (inline) 1.47 kB [entry] [rendered]
Date: 2017-10-28T19:27:58.456Z
Hash: bdee05e0c4e2c172ab79
Time: 5226ms
chunk {0} main.bundle.js (main) 13.6 kB [entry] [rendered]
chunk {1} styles.bundle.css (styles) 0 bytes [entry] [rendered]
npm info lifecycle ng-universal-demo@0.0.0~postbuild:client-and-server-bundles: ng-universal-demo@0.0.0
npm info ok
npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~prewebpack:server: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~webpack:server: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 webpack:server /app
> webpack --config webpack.server.config.js --progress --colors

 10% building modules 0/2 modules 2 active .../ts-loader/index.js!/app/preHash: 7d5e698066b1d16e7805                                                         Version: webpack 3.7.1
Time: 9365ms
       Asset     Size  Chunks                    Chunk Names
   server.js  4.06 MB       0  [emitted]  [big]  server
prerender.js  3.36 MB       1  [emitted]  [big]  prerender
  [55] ./src lazy 160 bytes {0} {1} [built]
 [104] ./dist/server/main.bundle.js 13.6 kB {0} {1} [built]
 [176] ./server.ts 1.94 kB {0} [built]
 [228] ./src 160 bytes {0} [built]
 [234] (webpack)/buildin/module.js 517 bytes {0} [built]
 [251] ./prerender.ts 2.08 kB {1} [built]
 [253] ./static.paths.js 57 bytes {1} [built]
    + 247 hidden modules
npm info lifecycle ng-universal-demo@0.0.0~postwebpack:server: ng-universal-demo@0.0.0
npm info ok
npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~pregenerate:static: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~generate:static: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 generate:static /app
> cd dist && node prerender

npm info lifecycle ng-universal-demo@0.0.0~postgenerate:static: ng-universal-demo@0.0.0
npm info ok
npm info lifecycle ng-universal-demo@0.0.0~postbuild:static: ng-universal-demo@0.0.0
npm info ok
npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~preserve:static: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~serve:static: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 serve:static /app
> cd dist/browser && http-server

Starting up http-server, serving ./
Available on:
  http://127.0.0.1:8080
  http://172.31.21.180:8080
  http://172.17.0.1:8080

Step 3.2.6: Repeat the end-to-end Tests

If we then run protractor in the other terminal, we will see that the error messages have not changed:

$ protractor
...
1) Blog should display the blog title as header 1 and id="blog_title"
  - Failed: Angular could not be found on the page http://localhost:8080/blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/. If this is not an Angular application, you may need to turn off waiting for Angular. Please see https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular-on-page-load
  - Failed: Error while waiting for Protractor to sync with the page: "window.getAllAngularTestabilities is not a function"

2) Blog should display the blog content
  - Failed: Angular could not be found on the page http://localhost:8080/blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/. If this is not an Angular application, you may need to turn off waiting for Angular. Please see https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular-on-page-load
  - Failed: Error while waiting for Protractor to sync with the page: "window.getAllAngularTestabilities is not a function"
...

The error messages have not changed compared to the situation before. However, in the other terminal, we can see that the NodeJS server understands the requests and answers with a “404 Not found”, since we have not yet implemented the feature:

...
Available on:
  http://127.0.0.1:8080
  http://172.31.21.180:8080
  http://172.17.0.1:8080
[Sat Oct 28 2017 19:28:45 GMT+0000 (UTC)] "GET /blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36"
[Sat Oct 28 2017 19:28:45 GMT+0000 (UTC)] "GET /blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/" Error (404): "Not found"
[Sat Oct 28 2017 19:28:45 GMT+0000 (UTC)] "GET /favicon.ico" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36"
[Sat Oct 28 2017 19:28:55 GMT+0000 (UTC)] "GET /blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36"
[Sat Oct 28 2017 19:28:55 GMT+0000 (UTC)] "GET /blog/2017/06/24/consuming-a-restful-web-service-with-angular-4/" Error (404): "Not found"

Let us assume that the protractor errors will vanish, once we have implemented the service correctly.

Step 4: Implement the Feature

Step 4.1 Create Link

In src/app/app.component.ts, we add a link to the single blog post as defined in the spec:

On port 8080, we can see that the new link is visible:

However, if we klick the link, nothing happens. When pressing F12 and repeating the click, we get the error message:

ERROR Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'blog/2017/06/13/angular-4-hello-world-with-quickstart'
Error: Cannot match any routes. URL Segment: 'blog/2017/06/13/angular-4-hello-world-with-quickstart'

A route to the link is missing.

Step 4.2 Add Route

Let us add a route now.

If we restart the server, we get the message:

ERROR in Error: Could not resolve "./blog/blog.module" from "/app/src/app/app.module.ts".

This is expected since the file does not exist yet. Let us change that now:

$ cp -R src/app/lazy src/app/blog
$ mv src/app/blog/lazy.module.ts src/app/blog/blog.module.ts
$ sed -r -i "s/lazy/blog/g" src/app/blog/blog.module.ts
$ sed -r -i "s/i'm blog/i'm a blog/g" src/app/blog/blog.module.ts
$ sed -r -i "s/Lazy/Blog/g" src/app/blog/blog.module.ts

This will copy the lazy component to a blog component.

Now the error message is gone and we get the following output in a browser, if we click the link:

However, we the protractor messages do not change at all. We still get the error messages:

$ protractor
...
1) Blog should display the blog title as header 1 and id="blog_title"
  - Failed: Angular could not be found on the page http://localhost:8080/blog/2017/06/13/angular-4-hello-world-with-quickstart. If this is not an Angular application, you may need to turn off waiting for Angular. Please see https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular-on-page-load
  - Failed: Error while waiting for Protractor to sync with the page: "window.getAllAngularTestabilities is not a function"

2) Blog should display the blog content
  - Failed: Angular could not be found on the page http://localhost:8080/blog/2017/06/13/angular-4-hello-world-with-quickstart. If this is not an Angular application, you may need to turn off waiting for Angular. Please see https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular-on-page-load
  - Failed: Error while waiting for Protractor to sync with the page: "window.getAllAngularTestabilities is not a function"

Why is this the case? I would have expected the error message to tell us that the page is available with the content not being the one expected by the spec. However, if we run the end-to-end tests, we can observe that the link is still returning a “404 Not found” message:

[Sat Oct 28 2017 22:02:19 GMT+0000 (UTC)] "GET /blog/2017/06/13/angular-4-hello-world-with-quickstart" Error (404): "Not found"

This is the case, although the link seems to be reachable within the browser, if we click the link named “Angular 4 Hello World Quickstart:

In debug mode (F12) we see:

However, if we press the reload the page in the browser, we get an empty page:

After finishing the debug mode and reloading the page, we get a “404 Not found” message:

That is interesting: The link /blog/2017/06/13/angular-4-hello-world-with-quickstart is reached by client-side routing by clicking on the link, but a reload of the page fails. Let us fix that now.

Step 4.2.1 Fix the Error: 404 Not found

The error is caused by the fact that we are running the service in static mode, but we have not taken any measures that the path is created in the HTTP server yet.

Fixing the error is as simple as

  • adding the link to static.paths.js link
  • re-starting the server with npm run start:static

The static.paths.js file is located in the project’s root:

// static.paths.js
module.exports = [
  '/',
  '/lazy',
  '/lazy/nested',
  '/blog/2017/06/13/angular-4-hello-world-with-quickstart'
];

Now we create the missing path via:

npm run start:static

This is running the command npm run build:static, which is running the command cd dist && node prerender (among others). This will create the additional path in the directory tree (in blue):

$ yum install -y tree # if not installed already
...
$ tree dist/
dist/
├── browser
│   ├── 0.7ce44253311853d97e73.chunk.js
│   ├── 1.1fd3684dd14207827a93.chunk.js
│   ├── 3rdpartylicenses.txt
│   ├── blog
│   │   └── 2017
│   │       └── 06
│   │           └── 13
│   │               └── angular-4-hello-world-with-quickstart
│   │                   └── index.html
│   ├── favicon.ico
│   ├── index.html
│   ├── inline.a70b6ebe7ee886967e09.bundle.js
│   ├── lazy
│   │   ├── index.html
│   │   └── nested
│   │       └── index.html
│   ├── main.2d468591087d33c2e372.bundle.js
│   ├── polyfills.54dd1bb0dea7bab42697.bundle.js
│   ├── styles.d41d8cd98f00b204e980.bundle.css
│   └── vendor.b2c3f787d02157b98c0e.bundle.js
├── prerender.js
├── server
│   ├── favicon.ico
│   ├── main.bundle.js
│   └── styles.bundle.css
└── server.js

9 directories, 18 files

From now on, we can directly access the path /blog/2017/06/13/angular-4-hello-world-with-quickstart on the server. If we access the path, we see the following line in the server log:

[Sat Oct 28 2017 22:51:25 GMT+0000 (UTC)] "GET /blog/2017/06/13/angular-4-hello-world-with-quickstart" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36"

In the browser, we can directly access the path and we see that the server is serving the path. We not only can click the “Angular 4 Hello World Quickstart link” from the root path, but we also can reload the page. We can see in the browser’s debug window (press F12) that the corresponding path is known to the server:

Finally, as expected, the protractor output changes:

$ protractor
...
1) Blog should display the blog title as header 1 and id="blog_title"
  - Failed: No element found using locator: By(css selector, *[id="blog_title"])

2) Blog should display the blog content
  - Failed: No element found using locator: By(css selector, *[id="blog_content"])

This is expected: we have created a static page with some dummy content, but we have not yet downloaded and displayed the content of the WordPress blog post.

Now let us save the changes on GIT:

git add .
git commit -m'added blog module and added links to blog module on app and static.paths.js'

Step 4.3: Create HTML Template for the Blog

The specs are expecting an HTML template with a blog title in an element with the ID blog_title and a blog content in an element with the ID blog_content. Let us create that now. We create a new file:

To connect the new HTML template with the rest, we need to add a templateURL reference the blog.module.ts file:

// src/app/blog/blog.module.ts
import {NgModule, Component} from '@angular/core'
import {RouterModule} from '@angular/router'

@Component({
  selector: 'blog-view',
  templateUrl: './blog.component.html'
})
export class BlogComponent {}

@NgModule({
  declarations: [BlogComponent],
  imports: [
    RouterModule.forChild([
      { path: '', component: BlogComponent, pathMatch: 'full'}
    ])
  ]
})
export class BlogModule {

}

After a restart of the server, the protractor output changes:

$ protractor
...
1) Blog should display the blog title as header 1 and id="blog_title"
  - Expected 'This is the blog title' to equal 'Angular 4 Hello World Quickstart'.

2) Blog should display the blog content
  - Expected 'This is the blog content' to contain 'In this hello world style tutorial, we will follow a step by step guide to a working Angular 4 application.'.
  - Expected 'This is the blog content' to match /^<p>In this hello world style tutorial, we will follow a step by step guide to a working Angular 4 application.'.

Now let us save the result:

git add .
git commit -m'added blog HTML template'

Step 4.4: Create Variables

We change the HTML template, so it used variables we will create thereafter:

The variables are not yet defined. Therefore we would get following error message, if we reload the server:

ERROR in /app/src/$$_gendir/app/blog/blog.module.ngfactory.ts (34,31): Property 'title' does not exist on type 'BlogComponent'.
ERROR in ng:///app/src/app/blog/blog.component.html (3,1): Property 'content' does not exist on type 'BlogComponent'.

Let us define the variables:

Step 4.5: Read Variables from the WordPress API

Now we read in the HTTP content into the variables using Observables.

// src/app/blog/blog.module.ts
import {NgModule, Component, OnInit} from '@angular/core'
import {RouterModule} from '@angular/router'
import { Http, Response, Headers } from '@angular/http';
import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable';

@Component({
  selector: 'blog-view',
  templateUrl: './blog.component.html'
})

export class BlogComponent implements OnInit {

  title : String = "Loading..."
  content : String = "Loading..."

  constructor(private _http: Http) {}

  ngOnInit() {
     this._http.get('https://public-api.wordpress.com/rest/v1.1/sites/oliverveits.wordpress.com/posts/3078')
                .map((res: Response) => res.json())
                 .subscribe(data => {
                        this.title = data.title;
                        this.content = data.content;
                        console.log(data);
                });
  }

}

import { HttpModule } from '@angular/http';
@NgModule({
  declarations: [BlogComponent],
  imports: [
    HttpModule,
    RouterModule.forChild([
      { path: '', component: BlogComponent, pathMatch: 'full'}
    ])
  ]
})
export class BlogModule {

}

There, we have defined a private variable for the HTTP service. This service is used to create an observable with the GET function, which we subscribe to read the title and content of a single post into the variable. Moreover, we have defined the provider for HttpModule in the BlogModule part. See this blog post for a quick introduction to this concept. More in-depth step-by-step instructions including end-to-end tests can be found in part 1 and part 2 of the Behavior-driven Angular series.

After restarting the server we already can see the title and content of the blog post retrieved via WordPress API:

npm run start:static

Let us test the result with ‘protractor’:

$ protractor
[17:44:30] I/direct - Using ChromeDriver directly...
[17:44:30] I/launcher - Running 1 instances of WebDriver
Jasmine started

  Blog
    ✓ should display the blog title as header 1 and id="blog_title"
    ✓ should display the blog content

Executed 2 of 2 specs SUCCESS in 1 sec.
[17:44:35] I/launcher - 0 instance(s) of WebDriver still running
[17:44:35] I/launcher - chrome #01 passed

This was successful, this time!

Excellent! Thump up!

Step 5: Verify Server-Side Rendering

The reason why we have chosen Angular Universal is its server-side rendering feature. In situations with low bandwidth to the Internet, server-side rendering helps us to provide the user with the content of the page with a much lower latency.

To be sure that server-side rendering works as expected, we review the HTML source manually:

Unfortunately, server-side rendering does not seem to work the way expected. We still see the “Loading…” directive instead of the innerHTML content. Re-starting the server will reveal some errors in the log:

$ npm run start:static
npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~prestart:static: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~start:static: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 start:static /app
> npm run build:static && npm run serve:static

npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~prebuild:static: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~build:static: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 build:static /app
> npm run build:client-and-server-bundles && npm run webpack:server && npm run generate:static

npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~prebuild:client-and-server-bundles: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~build:client-and-server-bundles: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 build:client-and-server-bundles /app
> ng build --prod && ng build --prod --app 1 --output-hashing=false

Date: 2017-10-29T17:50:51.072Z
Hash: 36c4b63a2e9bf0ceb4c9
Time: 14508ms
chunk {0} 0.7ce44253311853d97e73.chunk.js () 1.02 kB {1} {3}  [rendered]
chunk {1} 1.e11ff4adfdb6d0ed7929.chunk.js () 21.1 kB {0} {3}  [rendered]
chunk {2} polyfills.54dd1bb0dea7bab42697.bundle.js (polyfills) 66.1 kB {6} [initial] [rendered]
chunk {3} main.2d468591087d33c2e372.bundle.js (main) 5.76 kB {5} [initial] [rendered]
chunk {4} styles.d41d8cd98f00b204e980.bundle.css (styles) 0 bytes {6} [initial] [rendered]
chunk {5} vendor.9a16430f75eb51537ae8.bundle.js (vendor) 305 kB [initial] [rendered]
chunk {6} inline.1735ca01d11efd0014a9.bundle.js (inline) 1.5 kB [entry] [rendered]
Date: 2017-10-29T17:50:59.897Z
Hash: cd969720fe342e3bf65d
Time: 5221ms
chunk {0} main.bundle.js (main) 17.1 kB [entry] [rendered]
chunk {1} styles.bundle.css (styles) 0 bytes [entry] [rendered]
npm info lifecycle ng-universal-demo@0.0.0~postbuild:client-and-server-bundles: ng-universal-demo@0.0.0
npm info ok
npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~prewebpack:server: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~webpack:server: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 webpack:server /app
> webpack --config webpack.server.config.js --progress --colors

 10% building modules 0/2 modules 2 active .../ts-loader/index.js!/app/preHash: 46749ef5d0e4cdd33844                                                         Version: webpack 3.7.1
Time: 9306ms
       Asset     Size  Chunks                    Chunk Names
   server.js  4.07 MB       0  [emitted]  [big]  server
prerender.js  3.36 MB       1  [emitted]  [big]  prerender
  [56] ./src lazy 160 bytes {0} {1} [built]
 [104] ./dist/server/main.bundle.js 17.1 kB {0} {1} [built]
 [177] ./server.ts 1.94 kB {0} [built]
 [229] ./src 160 bytes {0} [built]
 [235] (webpack)/buildin/module.js 517 bytes {0} [built]
 [252] ./prerender.ts 2.08 kB {1} [built]
 [254] ./static.paths.js 117 bytes {1} [built]
    + 248 hidden modules
npm info lifecycle ng-universal-demo@0.0.0~postwebpack:server: ng-universal-demo@0.0.0
npm info ok
npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~pregenerate:static: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~generate:static: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 generate:static /app
> cd dist && node prerender

ERROR Error: not implemented
    at Parse5DomAdapter.getCookie (/app/dist/prerender.js:37285:68)
    at CookieXSRFStrategy.configureRequest (/app/dist/prerender.js:41698:119)
    at XHRBackend.createConnection (/app/dist/prerender.js:41747:28)
    at httpRequest (/app/dist/prerender.js:42155:20)
    at Http.request (/app/dist/prerender.js:42265:34)
    at Http.get (/app/dist/prerender.js:42279:21)
    at e.X+Mx.e.ngOnInit (/app/dist/prerender.js:78900:8159)
    at checkAndUpdateDirectiveInline (/app/dist/prerender.js:11698:19)
    at checkAndUpdateNodeInline (/app/dist/prerender.js:13196:20)
    at checkAndUpdateNode (/app/dist/prerender.js:13139:16)
npm info lifecycle ng-universal-demo@0.0.0~postgenerate:static: ng-universal-demo@0.0.0
npm info ok
npm info lifecycle ng-universal-demo@0.0.0~postbuild:static: ng-universal-demo@0.0.0
npm info ok
npm info it worked if it ends with ok
npm info using npm@3.10.10
npm info using node@v6.11.2
npm info lifecycle ng-universal-demo@0.0.0~preserve:static: ng-universal-demo@0.0.0
npm info lifecycle ng-universal-demo@0.0.0~serve:static: ng-universal-demo@0.0.0

> ng-universal-demo@0.0.0 serve:static /app
> cd dist/browser && http-server

Starting up http-server, serving ./
Available on:
  http://127.0.0.1:8080
  http://172.31.21.180:8080
  http://172.17.0.1:8080
Hit CTRL-C to stop the server

The dynamic server __npm run start:dynamic__ has the same problems. The only difference is that the error message appears every time the link is clicked.

After a lot of googling (in vain) and testing, I finally came up with a workaround: if we move out the HttpModule import from the BlogModule to the AppModule, the client-side-rendering as well as the server-side-rendering work fine:

// src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';

import { HttpModule } from '@angular/http';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
  ],
  imports: [
    HttpModule,
    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'},
      { path: 'blog/2017/06/13/angular-4-hello-world-with-quickstart', loadChildren: './blog/blog.module#BlogModule'}
    ])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

The corresponding lines need to be removed in the BlogModule. Otherwise, the error messaged do not disappear (I would have preferred to them for portability of the blog component, since I do not want the blog component to depend on imports of an upstream component; however, I am forced to remove it anyway, it seems):

// src/app/blog/blog.module.ts
import {NgModule, Component, OnInit} from '@angular/core'
import {RouterModule} from '@angular/router'
import { Http, Response, Headers } from '@angular/http';
import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable';

@Component({
  selector: 'blog-view',
  templateUrl: './blog.component.html'
})

export class BlogComponent implements OnInit {

  title : String = "Loading..."
  content : String = "Loading..."

  constructor(private _http: Http) {}

  ngOnInit() {
     this._http.get('https://public-api.wordpress.com/rest/v1.1/sites/oliverveits.wordpress.com/posts/3078')
                .map((res: Response) => res.json())
                 .subscribe(data => {
                        this.title = data.title;
                        this.content = data.content;
                        console.log(data);
                });
  }

}

//import { HttpModule }    from '@angular/http'; // moved to src/app/app.module.ts

@NgModule({
  declarations: [BlogComponent],
  imports: [
    //HttpModule, // moved to src/app/app.module.ts
    RouterModule.forChild([
      { path: '', component: BlogComponent, pathMatch: 'full'}
    ])
  ]
})
export class BlogModule {
}

Now, after restarting the server, the blog post page is visible in both ways:

client-side rendering:

For testing client-side rendering, we open the root URL localhost:8080/

server-side rendering:

For testing the server-side rendering, we need to access the URL directly, e.g. by reloading the page we see above (e.g. press F5) or by cutting&pasting the full URL into the browser.

We see the following:

–> the page displays the full content after less than a second

–> the page switches over to client-side rendering and the “Loading…” appears

–> once the page has been retrieved from the WordPress API, the full content is visible again

As the last test before re-running the end-to-end tests, we can see that the blog title and content can be seen in the HTML source:

This is both, SEO-friendly and the content will show up much quicker on mobile devices with a low-bandwidth Internet connection compared to the client-side rendering case.

Excellent! Thump up!

Note that the protractor tests are still successful:

$ protractor
[17:44:30] I/direct - Using ChromeDriver directly...
[17:44:30] I/launcher - Running 1 instances of WebDriver
Jasmine started

  Blog
    ✓ should display the blog title as header 1 and id="blog_title"
    ✓ should display the blog content

Executed 2 of 2 specs SUCCESS in 1 sec.
[17:44:35] I/launcher - 0 instance(s) of WebDriver still running
[17:44:35] I/launcher - chrome #01 passed

This was successful again.

Summary

We have successfully created an Angular Universal application based on Angular CLI with a REST client feeding in a single blog post from the WordPress API. We had experienced some trouble with server-side rendering, which was resolved miraculously by moving the HttpModule import from the BlogModule (the module using HTTP) to the root AppModule. At the end, we have succeeded to create an application that loads the page from the server and hands over the control to the browser thereafter.

ToDo:

  • dynamic path: I would like that all paths /blog/xxx are available and contain the title and content of the corresponding WordPress link https://oliverveits.wordpress.com/xxx/
  • Refactoring:
    • Move the REST client into a separate ‘@Injectable’ service.
    • separate the blog module from the blog component (small effort, but low priority)
  • In order to test server-side rendering, I had to restart the server every time the code has changed. I need to find a better way to handle this in future: e.g. use continuous testing for development with client-side rendering, and let the continuous integration machinery (e.g. TravisCI or CircleCI) perform the full tests in productive mode after each GIT push.
  • In future, I have to find out, how to write tests that will fail if the server-side rendering does not work. This time, I had to manually review, whether the HTML source contains the content.

4 comments

  1. Hi,
    Very nice article, Good insight. I have a doubt,
    If we use Angular universal should the consuming client also be in Angular ?
    I have a requirement where I want to get the HTML rendered using Angular Universal but the consuming client can be a react application or a plain Vanilla application. Is this possible using Angular universal ?
    I see most of the examples showing in Angular for both server and client side.
    Please provide any examples.

    1. Hi R. John, thanks alot for your encouraging words. I am quite new in the angular business, but I will try to answer your question nevertheless: from my understanding angular will run in any modern browser like client. As far as I know, the client needs to understand HTML, CSS and Javascript. However, I might be wrong.

    2. Your comment had some weird link in the beginning, why it had been moved to the spam folder. This is, why my answer comes so late, sorry.

      As I understand it, you are right by assuming that Angular Universal requires the client to be an Angular client. If you need a server side rendered page, why not using NodeJS on the server side and using any other client on the browser? I believe that this comes closest to what you seem to aim at: a JavaScript server and an independent client like react. Does this make sense to you?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.