0

Getting Started with DC/OS on AWS


In the step-by-step tutorial Getting Started with DC/OS on Vagrant, we have learned how to install a MesosPhere DC/OS data center operating system locally. This time, we will install a DC/OS system on AWS Cloud: existing AWS CloudFormation templates will help us create a fully functional DC/OS data center with a Mesos master and five Mesos slaves within less than two hours. At the end, we will test the environment by starting a “Hello World” service based on Docker from DC/OS’ administration panel and accessing the application from the Internet.

MesoSphere DC/OS is a Data Center Operating System, which is built upon Apache Mesos and Mesosphere Marathon, an open source container orchestration platform. It has the target to hide the complexity of data centers when deploying applications.

AWS, Amazon Web Services is the leading provider offering Infrastructure as a Service and more.

 

Beware that running DC/OS on AWS does not come for free. I am still in the free tier period, so I had to pay only $0.48 for a test duration of less than 45 minutes (measured from the time I have created to the point in time I have terminated the stack). However, the induced cost might be higher in your case. Also, I had to pay a lot more, as the time of usage increased and some of the free usage limits were exceeded.

I recommend to check your current bill before and after the the test on the AWS Billing Home for the region US-West-2.

The guide has been tested for the region us-west-2 and us-east-2. However, it has worked only for us-west-2; probably because the correct image IDs are missing for us-east-2.

We are loosely following https://aws.amazon.com/blogs/apn/announcing-mesosphere-dcos-on-aws/, but we had to add correct some commands and add some instructions on user permissions.

See also

Prerequisites

Step 1: Configure your Credentials

You need to have entered your AWS Access Key and Secret on the ~/.aws/credentials file:

[default]
aws_access_key_id = XXXXXXX
aws_secret_access_key = KKKKKKKK

Step 2: Create an SSH Key for DC/OS

aws --region us-west-2 ec2 create-key-pair --key-name dcos-demo-key --output text --query KeyMaterial > dcos-demo-key_us-west-2.pem
cp dcos-demo-key_us-west-2.pem dcos-demo-key.pem
chmod 600 dcos-demo-key.pem

This will create an additional key pair on region us-west-2 (before, I had no key pair on this region; now it is one key):

Step 3: Find Cloud Formation Template URL

The official DCOS documentation v1.10 on AWS installation offers two options:

For our tests, we will choose the basic variant with one Mesos master and five Mesos slaves.

The corresponding CloudFormation Templates can be found on this page.

We copy the “Launch Stack” link for us-west-2 with Single Master and paste it here:

https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3-us-west-2.amazonaws.com/downloads.dcos.io/dcos/EarlyAccess/commit/a5ecc9af5d9ca903f53fa16f6f0ebd597095652e/cloudformation/single-master.cloudformation.json

From the link, we can see that the template URL is as follows. On a Linux shell (e.g. GIT Bash on Windows), we define:

TEMPLATE_URL=https://s3-us-west-2.amazonaws.com/downloads.dcos.io/dcos/EarlyAccess/commit/a5ecc9af5d9ca903f53fa16f6f0ebd597095652e/cloudformation/single-master.cloudformation.json

Step 4: Launch the CloudFormation Stack from AWS CLI

Step 4.1: First Attempt to launch the Stack

From our main instructions page, we find something like:

aws --region us-west-2 cloudformation create-stack --stack-name dcos-demo \
    --template-url ${TEMPLATE_URL} \
    --parameters ParameterKey=AcceptEULA,ParameterValue="Yes",ParameterKey=KeyName,ParameterValue="dcos-demo-key" \
    --capabilities CAPABILITY_IAM

Note that there were some errors in the instructions page: the line feed formatting was wrong and a comma was missing. This has been corrected above.

If your AWS CLI is using a user without CloudFormation permissions, you will receive the following error message:

A client error (AccessDenied) occurred when calling the CreateStack operation: User: arn:aws:iam::924855196031:user/secadmin is not authorized to perform: cloudformation:CreateStack on resource: arn:aws:cloudformation:us-east-2:924855196031:stack/dcos-demo/*

If you have not encountered this error, you can skip the next three substeps.

Step 4.2: Create Policy for CloudFormation Permissions

On the EC2 Dashboard of the AWS Console for us-west-2 (choose right region in the URL), choose

–> Services
–> IAM
–> Policies
–> Create Policy
–> Select Policy Generator
–> Choose Parameters:
Effect: Allow
AWS Service: AWS CloudFormation
Actions: All
Actions ARN: *

–> Add Statement
–> edit Name, e.g. “CloudFormation”

–> Create Policy

Step 4.3: Attach Policy to User

–> Users
–> Choose your user
–> Add Permission
–> Attach existing policies directly
–> check “CloudFormation”

–> Next Review

–> Add permissions

Step 4.4: Try again: Create Policy for CloudFormation Permissions

TEMPLATE_URL=https://s3-us-west-2.amazonaws.com/downloads.dcos.io/dcos/EarlyAccess/commit/a5ecc9af5d9ca903f53fa16f6f0ebd597095652e/cloudformation/single-master.cloudformation.json
aws --region us-west-2 cloudformation create-stack --stack-name dcos-demo \
    --template-url ${TEMPLATE_URL} \
    --parameters ParameterKey=AcceptEULA,ParameterValue="Yes",ParameterKey=KeyName,ParameterValue="dcos-demo-key" \
    --capabilities CAPABILITY_IAM

This time we get following Response:

{
“StackId”: “arn:aws:cloudformation:us-west-2:924855196031:stack/dcos-demo/0c90e5c0-c716-11e7-9e0d-50d5ca2e7cd2”
}

After some minutes, we will see CREATE_COMPLETE in the AWS Console of US West 2:

On the EC2 Dashboard, we see:

After clicking the “8 Running Instances” link, we see:

The DC/OS is up and running!

Excellent! Thump up!

If you see other errors like

  • API: s3:CreateBucket Access Denied
  • API: iam:CreateRole User: arn:aws:iam::924855196031:user/secadmin is not authorized to perform: iam:CreateRole on resource: arn:aws:iam::924855196031:role/dcos-demo-SlaveRole-LP582D7P32GZ
  • The following resource(s) failed to create: [Vpc, ExhibitorS3Bucket, SlaveRole, DHCPOptions]. . Rollback requested by user.

then follow the instructions in Appendix A. Those are permissions issues.

Step 5 (recommended): Restrict Admin Access

The default is that the machines are open to the Internet world. I recommend to change the settings, so only you can access your systems.

On the EC2 Dashboard -> Security Groups, check out the security group with the description “Enable admin access to servers” and edit the source IP addresses:

Replace 0.0.0.0/0 (any) to “My IP” for all sources.

–> Save

Note, this step needs to be repeated any time your source IP address changes. See Step B6 of AWS Automation based on Vagrant — Part 2: Installation and Usage of the Vagrant AWS Plugin, if you are interested in an example that shows how to update the security rules to point to “My IP” per shell script based on AWS CLI.

TODO: find a better way to secure the admin interfaces, e.g. by adapting the CloudFoundation templates before starting the stack. This way, the admin interfaces are not open to the world from the beginning on.

Step 6: Access the DC/OS Admin Console

Now let us access our DC/OS Admin Console. For that, let us find the public DNS name of the master:

$ aws cloudformation describe-stacks --region us-west-2 | grep dcos-demo-ElasticL | awk -F '"' '{print $4}'
dcos-demo-ElasticL-XRZ8I3ZZ2BB2-549374334.us-west-2.elb.amazonaws.com

This is the DNS name we can connect to:

In my case, I have signed in with Google.

We reach a nice dashboard:

DCOS Dashboard on AWS

Step 7: Install DCOS CLI

The easiest way to automate application orchestration is to make use of the DCOS CLI. For that, click on your name and then “Install CLI” and follow the instructions. You will find some dcos command examples in my previous blog post on DC/OS.

I have followed the Windows instructions, i.e.

dcos cluster setup http://dcos-demo-elasticl-pu3fgu8047kg-271238338.us-west-2.elb.amazonaws.com

A browser window was started and I have logged into the browser session via Google. Then the token was offered:

I had to Copy and paste the token into the command line:

Enter OpenID Connect ID Token: eyJ0eXAiOiJKV1QiLCJ…

After that you should be able to see the dcos services:

dcos service
NAME              HOST     ACTIVE  TASKS  CPU    MEM    DISK  ID
marathon       10.0.5.242   True     4    2.75  1836.0  0.0   d456c8ce-f0e6-4c61-9974-94e3426f5fe8-0001
metronome      10.0.5.242   True     0    0.0    0.0    0.0   d456c8ce-f0e6-4c61-9974-94e3426f5fe8-0000

Marathon and Metronome are already running.

Step 8: Install Marathon LB

(dcos package describe --config marathon-lb)
dcos package install marathon-lb
By Deploying, you agree to the Terms and Conditions https://mesosphere.com/catalog-terms-conditions/#community-services
We recommend at least 2 CPUs and 1GiB of RAM for each Marathon-LB instance.

*NOTE*: For additional ```Enterprise Edition``` DC/OS instructions, see https://docs.mesosphere.com/administration/id-and-access-mgt/service-auth/mlb-auth/
Continue installing? [yes/no] yes
Installing Marathon app for package [marathon-lb] version [1.11.1]
Marathon-lb DC/OS Service has been successfully installed!
See https://github.com/mesosphere/marathon-lb for documentation.

After clicking on marathon-lb, we the details of the configuration of the marathon load balancer:

 

Step 9: Create a Hello World Application

Similar to the blog post, where we have installed DC/OS locally via Vagrant, let us create a hello world application. We choose a NginX application that is displaying some information on the source and destination IP addresses and ports seen from within the container. For that, let us click

–> Services

–> RUN A SERVICE

–> JSON Configuration

Cut and paste following text into the field:

{
   "id": "nginx-hello-world-service",
   "container": {
     "type": "DOCKER",
     "docker": {
       "image": "nginxdemos/hello",
       "network": "BRIDGE",
       "portMappings": [
         { "hostPort": 0, "containerPort": 80, "servicePort": 10007 }
       ]
     }
   },
   "instances": 3,
   "cpus": 0.1,
   "mem": 100,
   "healthChecks": [{
       "protocol": "HTTP",
       "path": "/",
       "portIndex": 0,
       "timeoutSeconds": 2,
       "gracePeriodSeconds": 15,
       "intervalSeconds": 3,
       "maxConsecutiveFailures": 2
   }],
   "labels":{
     "HAPROXY_DEPLOYMENT_GROUP":"nginx-hostname",
     "HAPROXY_DEPLOYMENT_ALT_PORT":"10007",
     "HAPROXY_GROUP":"external",
     "HAPROXY_0_REDIRECT_TO_HTTPS":"true",
     "HAPROXY_0_VHOST": "dcos-demo-PublicSl-1NSRAFIDG6VZS-267420313.us-west-2.elb.amazonaws.com"
   }
}

As HAPROXY_0_VHOST you need to use the public slave’s load balancer address you can retrieve via AWS CLI via:

$ aws cloudformation describe-stacks --region us-west-2 | grep dcos-demo-PublicSl | awk -F '"' '{print $4}' 
dcos-demo-PublicSl-1NSRAFIDG6VZS-267420313.us-west-2.elb.amazonaws.com

 

Now:

–> REVIEW & RUN

–> RUN SERVICE

You will see that the nginx-hello-world-service is being deployed:

After some seconds, the 3 containers are up&running:

 

After clicking on the name of the service, you will see the three containers:

Note that the column “UPDATED” will disappear, if the browser width is too low. If you have a small screen, you can scale the browser content with CTRL and Minus.

Step 10 (optional): Reach the service from inside

On an internal host, I can reach the NginX server via two ways:

Step 10.1: Access Application Container on a Private Slave

The following command will return the HTML code of the single container running on a private slave:

curl 10.0.2.9:14679 # SlaveServerGroup

Here, we have chosen the Endpoint address we can retrieve from the services details page:

Step 10.2: Access the Load Balancer Address

We can also contact the internal load balancer endpoint for the service. This has the advantage that the access is load balanced among the different containers we have started for the service.

curl 10.0.6.204:10007 # PublicSlaveServerGroup

Here we have combined the Public slave IP address with the HAPROXY port we have configured as a label:

Excellent! Thump up!

In the next step, we will access the load balancer endpoint via the Internet.

Step 11: Connect to the Service via Internet

Step 11.1: Direct Connection to the Public Slave

The CloudFormation stack is configured in a way that allows reaching the public slave via the Internet on port 10007. This allows us to access the hello world application directly:

Step 11.2: Connection via AWS Load Balancer

Consider a case where we have more than one public slave. In those situations, it is better to access the service via AWS load balancer, which will distribute the load among the different public slave marathon load balancers (i.e. HAPROXY load balancers). In our case, we access the service on port 80: http://dcos-demo-PublicSl-1NSRAFIDG6VZS-267420313.us-west-2.elb.amazonaws.com

The load balancer address can be retrieved via

$ aws cloudformation describe-stacks --region us-west-2 | grep dcos-demo-PublicSl | awk -F '"' '{print $4}'
dcos-demo-PublicSl-1NSRAFIDG6VZS-267420313.us-west-2.elb.amazonaws.com

By pasting the return value into the browser, we are redirected to the corresponding https page:

After refreshing the page, we will see that we will get answers from the other two containers as well:

With that, we have learned how to create a service and access it from the Internet.

Excellent! Thump up!

 

Step 12: Explore the Marathon Load Balancer

You can access the marathon load balancer by retrieving the public IP address of the public slave from the AWS console (EC2):

We then access the HA Proxy statistics page and configuration page by entering the public IP address or DNS name into the URL field, and adding one of the following strings:

  • :9090/haproxy?stats
  • :9090/_haproxy_getconfig

Step13: Delete the Stack

Do not forget to delete the stack, since it will induce quite a bit of cost if you fail to do so. The stack can be deleted via AWS CLI as follows:

aws --region us-west-2 cloudformation delete-stack --stack-name dcos-demo

Better you check on the  AWS Console that all resources have been deleted successfully:

Excellent! Thump up!

Summary

In this blog post, we have learned to install a DC/OS Cluster on AWS using an existing CloudFormation template. For that, we have used AWS CLI to spin up a DC/OS environment with a single master, a single public slave, and five private slaves (see Appendix ?? below how to tweak the template to run only two private slaves in order to save some money).

Similar to the tests we had performed on a local machine using Vagrant described in the post Getting Started with DC/OS on Vagrant, we have installed a marathon load balancer, before we have deployed a three-container hello-world application. We have shown how to access this application from the public Internet using the AWS elastic load balancer that has been installed automatically via the CloudFormation stack. Moreover, we have shown how to access the marathon load balancer’s statistics and configuration page.

In the course of this step by step tutorial, we have mastered

  • user permission challenges (see step 4 and Appendix A)
  • networking challenges

We had to figure out that the services are only reachable via the AWS load balancers.

Appendix A: Add required User Permissions

Appendix A1: Remedy S3 Permission Error

Symptoms


If your user lacks the correct S3 permissions, we will get following errors in the  AWS Console, when trying to start the CloudFormation stack:

  • API: s3:CreateBucket Access Denied
  • API: iam:CreateRole User: arn:aws:iam::924855196031:user/secadmin is not authorized to perform: iam:CreateRole on resource: arn:aws:iam::924855196031:role/dcos-demo-SlaveRole-LP582D7P32GZ
  • The following resource(s) failed to create: [Vpc, ExhibitorS3Bucket, SlaveRole, DHCPOptions]. . Rollback requested by user.

Resolution

  1. Add S3 Permissions

2) Add IAM Policy:

Add Permissions -> Create policy

-> Policy Generator -> Select ->

-> Add Statement -> Next Step -> Edit Name “IAM” -> Create Policy

-> Filter: Policy Type: Custom managed

-> Choose “IAM”

Let us delete it via console and try again:

TEMPLATE_URL=https://s3-us-west-2.amazonaws.com/downloads.dcos.io/dcos/EarlyAccess/commit/14509fe1e7899f439527fb39867194c7a425c771/cloudformation/single-master.cloudformation.json
aws --region us-west-2 cloudformation create-stack --stack-name dcos-demo \
    --template-url ${TEMPLATE_URL} \
    --parameters ParameterKey=AcceptEULA,ParameterValue="Yes",ParameterKey=KeyName,ParameterValue="dcos-demo-key" \
    --capabilities CAPABILITY_IAM

Now we get following success messages on the AWS console:

After some minutes in the EC2 console:

 

 

Appendix B: [AcceptEULA] do not exist in the template

TEMPLATE_URL=https://s3-us-west-2.amazonaws.com/downloads.dcos.io/dcos/EarlyAccess/commit/14509fe1e7899f439527fb39867194c7a425c771/cloudformation/single-master.cloudformation.json
aws --region us-east-2 cloudformation create-stack --stack-name dcos-demo \
    --template-url ${TEMPLATE_URL} \
    --parameters ParameterKey=AcceptEULA,ParameterValue="Yes" ParameterKey=KeyName,ParameterValue="dcos-demo-key" \
    --capabilities CAPABILITY_IAM

This time we get:

A client error (ValidationError) occurred when calling the CreateStack operation: Parameters: [AcceptEULA] do not exist in the template

This StackOverflow Q&A has pointed to the right direction: I tried to wrap all parameters in ”, but then I got a syntax error, that a comma is expected. The correct syntax turned out to be:

TEMPLATE_URL=https://s3-us-west-2.amazonaws.com/downloads.dcos.io/dcos/EarlyAccess/commit/14509fe1e7899f439527fb39867194c7a425c771/cloudformation/single-master.cloudformation.json
aws --region us-east-2 cloudformation create-stack --stack-name dcos-demo \
    --template-url ${TEMPLATE_URL} \
    --parameters ParameterKey=AcceptEULA,ParameterValue="Yes",ParameterKey=KeyName,ParameterValue="dcos-demo-key" \
    --capabilities CAPABILITY_IAM

with commata between all parameters.

Appendix C: “Template error: Unable to get mapping for NATAmi::us-east-2::default”

How to Reproduce:

Get Key for region=us-east-2 from here: copy the link address of the corresponding Launch Stack Link and paste it somewhere:

https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?templateURL=https://s3-us-west-2.amazonaws.com/downloads.dcos.io/dcos/EarlyAccess/commit/a5ecc9af5d9ca903f53fa16f6f0ebd597095652e/cloudformation/single-master.cloudformation.json

TEMPLATE_URL=https://s3-us-west-2.amazonaws.com/downloads.dcos.io/dcos/EarlyAccess/commit/a5ecc9af5d9ca903f53fa16f6f0ebd597095652e/cloudformation/single-master.cloudformation.json;

Create a key for US East:

aws --region us-east-2 ec2 create-key-pair --key-name dcos-demo-key --output text --query KeyMaterial > dcos-demo-key_us-east-2.pem;
cp -i dcos-demo-key_us-east-2.pem dcos-demo-key.pem;
chmod 600 dcos-demo-key.pem;

Try starting the Stack:

aws --region us-east-2 cloudformation create-stack --stack-name dcos-demo \
    --template-url ${TEMPLATE_URL} \
    --parameters ParameterKey=AcceptEULA,ParameterValue="Yes",ParameterKey=KeyName,ParameterValue="dcos-demo-key" \
    --capabilities CAPABILITY_IAM;

If the user has all needed permissions (see steps 4.x above), then we get the following error:

A client error (ValidationError) occurred when calling the CreateStack operation: Template error: Unable to get mapping for NATAmi::us-east-2::default

Workaround

I have not investigated this issue. However, I guess that the error has to do with missing mappings for the images (AMI). A workaround is to use region=us-west-2 instead of us-east-2.

Appendix D: ERROR: “parameter value decos-demo-key for parameter name KeyName does not exist”

Reproduce

If you closely follow the instructions on https://aws.amazon.com/blogs/apn/announcing-mesosphere-dcos-on-aws/, correct the syntax errors in the aws commands, but keep the wrong key name “decos-demo-key” instead of “dcos-demo-key”, you will encounter the following problem:

After creation of the stack, we ask for the status:

aws --region us-west-2 cloudformation describe-stacks --stack-name dcos-demo --query Stacks[0].StackStatus

You will get the response:

"ROLLBACK_COMPLETE"

On the AWS Console of US West 2 we get:

The following error message is displayed:

Parameter validation failed: parameter value decos-demo-key for parameter name KeyName does not exist. Rollback requested by user.

Solution:

Correct the demo key name: “dcos-demo-key” instead of “decos-demo-key”

Appendix E: Adapt the CloudFormation Template to your Needs

The CloudFormation template is spinning up one master, one public slave, a NAT machine and five (!) private slaves. For the purpose of hello world testing we are performing, two instead of five private slaves are plenty. For that, I have adapted the CloudFormation template as follows:

Step E.1: Download CloudFormation Template

curl https://s3-us-west-2.amazonaws.com/downloads.dcos.io/dcos/EarlyAccess/commit/a5ecc9af5d9ca903f53fa16f6f0ebd597095652e/cloudformation/single-master.cloudformation.json

Step E.2 Adapt CloudFormation Template

I have added following parameter to the template (in blue):

        "SlaveInstanceCount": {
            "Description": "Required: Specify the number of private agent nodes or accept the default.",
            "Default": "5",
            "Type": "Number"
        },
        "SlaveInstanceCountDesired": {
            "Description": "Required: Specify the number of private agent nodes or accept the default.",
            "Default": "2",
            "Type": "Number"
        },
        "PublicSlaveInstanceCount": {
            "Description": "Required: Specify the number of public agent nodes or accept the default.",
            "Default": "1",
            "Type": "Number"
        },

The default of this parameter is two instead of five.

In the same template, I have changed following parts (in blue)

        "SlaveServerGroup": {
            "CreationPolicy": {
                "ResourceSignal": {
                    "Timeout": {
                        "Fn::FindInMap": [
                            "Parameters",
                            "StackCreationTimeout",
                            "default"
                        ]
                    },
                    "Count": {
                        "Ref": "SlaveInstanceCountDesired"
                    }
                }
            },
            "Properties": {
                "MaxSize": {
                    "Ref": "SlaveInstanceCount"
                },
                "DesiredCapacity": {
                    "Ref": "SlaveInstanceCountDesired"
                },
                "MinSize": {
                    "Ref": "SlaveInstanceCountDesired"
                },

Note that the stack will be stuck in CREATE_IN_PROGRESS if the first Count is not changed from SlaveInstanceCount to SlaveInstanceCountDesired.

Step E.3: Create S3 Bucket

The template is too large to use it directly per file: you will get following error if you try to use the template as file TEMPLATE_FILE=template-file-name:

aws --region us-west-2 cloudformation create-stack --stack-name dcos-demo \
 --template-body ${TEMPLATE_FILE} \
 --parameters ParameterKey=AcceptEULA,ParameterValue="Yes",ParameterKey=KeyName,ParameterValue="AWS_SSH_Key" \
 --capabilities CAPABILITY_IAM
An error occurred (ValidationError) when calling the CreateStack operation: 1 validation error detected: Value '<the json cloudformation template is printed>' at 'templateBody' failed to satisfy constraint: Member must have length less than or equal to 51200

The solution is to move the template to an S3 bucket in the same region. Now let us create the bucket:

aws s3api create-bucket --bucket my-us-west-2-bucket --region us-west-2

Step E.4: Copy Template to S3 Bucket

The template file can be copied to the S3 bucket via a command like:

aws s3 cp template_filename s3://my-us-west-2-bucket/

Step E.5: Use Template

Now we are ready to use the S3 bucket URL to create the stack:

TEMPLATE_URL='https://s3.amazonaws.com/my-us-west-2-bucket/template_filename'
SSH_KEY=dcos-demo-key
   aws --region us-west-2 cloudformation create-stack --stack-name dcos-demo \
       --template-url ${TEMPLATE_URL} \
       --parameters ParameterKey=AcceptEULA,ParameterValue="Yes",ParameterKey=KeyName,ParameterValue="AWS_SSH_Key" \
       --capabilities CAPABILITY_IAM

After 15 minutes or so, you should see that the stack is up and running with two private slave instances:

Excellent! Thump up!

Appendix F: Configuration

F.1 Master cloud-config.yml

Is found on /usr/share/oem/cloud-config.yml:

#cloud-config

coreos:
  units:
    - name: etcd.service
      runtime: true
      drop-ins:
        - name: 10-oem.conf
          content: |
            [Service]
            Environment=ETCD_PEER_ELECTION_TIMEOUT=1200

    - name: etcd2.service
      runtime: true
      drop-ins:
        - name: 10-oem.conf
          content: |
            [Service]
            Environment=ETCD_ELECTION_TIMEOUT=1200

    - name: user-configdrive.service
      mask: yes

    - name: user-configvirtfs.service
      mask: yes

    - name: oem-cloudinit.service
      command: restart
      runtime: yes
      content: |
        [Unit]
        Description=Cloudinit from EC2-style metadata

        [Service]
        Type=oneshot
        ExecStart=/usr/bin/coreos-cloudinit --oem=ec2-compat

  oem:
    id: ami
    name: Amazon EC2
    version-id: 0.0.7
    home-url: http://aws.amazon.com/ec2/
    bug-report-url: https://github.com/coreos/bugs/issues

F.2 Public Slave cloud-config.yml

#cloud-config

coreos:
  units:
    - name: etcd.service
      runtime: true
      drop-ins:
        - name: 10-oem.conf
          content: |
            [Service]
            Environment=ETCD_PEER_ELECTION_TIMEOUT=1200

    - name: etcd2.service
      runtime: true
      drop-ins:
        - name: 10-oem.conf
          content: |
            [Service]
            Environment=ETCD_ELECTION_TIMEOUT=1200

    - name: user-configdrive.service
      mask: yes

    - name: user-configvirtfs.service
      mask: yes

    - name: oem-cloudinit.service
      command: restart
      runtime: yes
      content: |
        [Unit]
        Description=Cloudinit from EC2-style metadata

        [Service]
        Type=oneshot
        ExecStart=/usr/bin/coreos-cloudinit --oem=ec2-compat

  oem:
    id: ami
    name: Amazon EC2
    version-id: 0.0.7
    home-url: http://aws.amazon.com/ec2/
    bug-report-url: https://github.com/coreos/bugs/issues

References

 

2

Angular Universal CLI – Step-by-Step Example with REST Client


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.
0

Behavior-Driven Angular – Part 2: Inserting REST Data as “innerHTML” into a Web Application


Today, we will extend the behavior-driven development example of the previous blog post and add the blog content to the document. Like last time, we will retrieve the HTML content from the WordPress API. Sounds easy, right? We will see that the challenge is to display the HTML content correctly, so we do not see escaped HTML like “<p>…” on the page.

As in part 1, we will follow a “test first” strategy: we will create the e2e test specification before we implement the actual code.

Within the Protractor/Jasmine framework, we will learn how to match the text and the inner HTML of browser DOM elements with functions like expect(...).toEqual("..."), .toContain("...") and .toMatch(/regex/) functions. The latter gives us the full flexibility of regular expressions.

Check out this book on Amazon: Angular Test-Driven Development

Plan for Today

Today, we plan to complement the blog title we have shown last time with the blog content, similar to the blog post Angular 4 Hello World Quickstart, which we will uses as our data mine. We will only show the title and the content as follows:

Before we start coding, we will add an e2e test that defines our expectation.

Step 0: Clone the GIT Repository and install the Application

This step can be skipped if you have followed part 1 of this series.

I am assuming that you have a Docker host available with 1.5GB or more RAM, GIT is installed on that host.

alias cli='docker run -it --rm -w /app -v $(pwd):/app -p 4200:4200 oveits/angular-cli:1.4.3 $@'
alias protractor='docker run -it --privileged --rm --net=host -v /dev/shm:/dev/shm -v $(pwd):/protractor webnicer/protractor-headless $@'
git clone https://github.com/oveits/consuming-a-restful-web-service-with-angular.git
cd consuming-a-restful-web-service-with-angular
git checkout -b 320ae88
cli npm i
chown -R $(whoami) .
cli ng serve --host 0.0.0.0

Phase 1: Create an e2e Test

Step 1.1: Create a GIT Feature Branch

As always with a new feature, let us create a feature branch (on a second terminal):

$ cd /vagrant/consuming-a-restful-web-service-with-angular/

$ protractor
[20:24:22] I/direct - Using ChromeDriver directly...
[20:24:22] I/launcher - Running 1 instances of WebDriver
Jasmine started

  consuming-a-restful-web-service-with-angular App
    ✓ should display the title

Executed 1 of 1 spec SUCCESS in 0.756 sec.
[20:24:27] I/launcher - 0 instance(s) of WebDriver still running
[20:24:27] I/launcher - chrome #01 passed

$ git checkout -b feature/0004-add-blog-content

You might need to adapt the path to your project. The protractor command is optional, but it will ensure that the e2e tests have worked on your machine before you start changing the code. I have seen some permissions topic described in the Appendices, which have made me cautious.

Step 1.2 (optional): Apply new Test Functions to the Blog Title

We would like to add a test that checks, whether the blog content is showing on the page. There are many Jasmine specification examples out there. Somehow, I have stumbled over this example. In order to verify that the functions I found there work fine, I thought it would be a good idea to write a new test similar to the ones in the example, but apply the test to the blog title before we write a new test for the blog content. This way, we can verify that we apply the correct syntax.

I have kept the original specification code, but I have added following code to the spec:

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

describe('consuming-a-restful-web-service-with-angular App', () => {
  let page: AppPage;

  beforeEach(() => {
    page = new AppPage();
  });

  it('should display the title', () => {
    page.navigateTo();
    expect(page.getParagraphText()).toContain('Angular 4 Hello World Quickstart');
  });
});

describe('Blog', () => {

  beforeEach(() => {
    browser.get('/');
  });

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

Both protractor e2e tests are successful without changing the code:

$ protractor
[20:59:51] I/direct - Using ChromeDriver directly...
[20:59:51] I/launcher - Running 1 instances of WebDriver
Jasmine started

  consuming-a-restful-web-service-with-angular App
    ✓ should display the title

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

Executed 2 of 2 specs SUCCESS in 2 secs.
[20:59:57] I/launcher - 0 instance(s) of WebDriver still running
[20:59:57] I/launcher - chrome #01 passed

Save it. Note: for pushing the changes to Github, you will need to fork my project and work with your fork. Otherwise, you can keep the git backups locally only.

git commit -am'1.2 added an addional test for the title looking for the first H1 header (successful test)'
git push

Step 1.3 (optional): Refine the Test

Step 1.3.1 Create a Test looking for a specific Element per ID

Since the blog content will not be a header, we will need to look for something, which is unique on the page. We use an ID for fetching the correct element from the page:

import { browser, by, element } from 'protractor';

...

describe('Blog', () => {

  beforeEach(() => {
    browser.get('/');
  });

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

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

Now the protractor test will fail. This is because we have not set the ID on the HTML template yet:

$ protractor
[21:07:51] I/direct - Using ChromeDriver directly...
[21:07:51] I/launcher - Running 1 instances of WebDriver
Jasmine started

  consuming-a-restful-web-service-with-angular App
    ✓ should display the title

  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"])
          at WebDriverError (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/error.js:27:5)
          at NoSuchElementError (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/error.js:242:5)
          at /usr/local/lib/node_modules/protractor/built/element.js:808:27
          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:28:23)
          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:16:5
          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)
      From asynchronous test:
      Error
          at Suite. (/protractor/e2e/app.e2e-spec.ts:26:3)
          at Object. (/protractor/e2e/app.e2e-spec.ts:18: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: No element found using locator: By(css selector, *[id="blog_title"])

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

To save the change:

git commit -am'1.3.1 search title by element id (failed e2e test)'

Step 1.3.2 Fix the Test

Let us fix the failed test like follows: In the HTML template src/app/app.component.html, we specify the element ID:

<h1 id="blog_title">{{title}}</h1>

Now the protractor test is successful again:

$ protractor
[21:14:27] I/direct - Using ChromeDriver directly...
[21:14:27] I/launcher - Running 1 instances of WebDriver
Jasmine started

  consuming-a-restful-web-service-with-angular App
    ✓ should display the title

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

Executed 2 of 2 specs SUCCESS in 2 secs.
[21:14:34] I/launcher - 0 instance(s) of WebDriver still running
[21:14:34] I/launcher - chrome #01 passed

That was simple. Now let us apply our learnings to the blog content.

To save the change:

git commit -am'1.3.2 add ID to HTML template (success)'; git push

Phase 2: Create the Test for the Blog Content

The content of the blog can be seen on WordPress:

The content starts with: In this hello world style tutorial, we will follow a step by step guide to a working Angular 4 application.

Let us search for that on our application.

Step 2.1 Add Blog Content e2e Tests

Similar to what we have done for the Blog Title, let us create an e2e test for the blog content. We add the parts in blue to e2e/app.e2e-spec.ts:

// e2e/app.e2e-spec.ts
...
describe('Blog', () => {

  beforeEach(() => {
    browser.get('/');
  });

  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(element(by.css('h1')).getText()).toEqual('Angular 4 Hello World Quickstart');
    expect(blog_title.getText()).toEqual('Angular 4 Hello World Quickstart');
  });

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

Since the content is quite large, we did not compare it with the equality operator, but we have used the ‘toContain’ function instead.

The new protractor test fails as expected:

$ protractor
[21:23:04] I/direct - Using ChromeDriver directly...
[21:23:04] I/launcher - Running 1 instances of WebDriver
Jasmine started

  consuming-a-restful-web-service-with-angular App
    ✓ should display the title

  Blog
    ✓ should display the blog title as header 1 and id="blog_title"
    ✗ should display the blog content
      - Failed: No element found using locator: By(css selector, *[id="blog_content"])
          at WebDriverError (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/error.js:27:5)
          at NoSuchElementError (/usr/local/lib/node_modules/protractor/node_modules/selenium-webdriver/lib/error.js:242:5)
          at /usr/local/lib/node_modules/protractor/built/element.js:808:27
          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:33: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:16:5
          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)
      From asynchronous test:
      Error
          at Suite. (/protractor/e2e/app.e2e-spec.ts:32:3)
          at Object. (/protractor/e2e/app.e2e-spec.ts:18: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 content
  - Failed: No element found using locator: By(css selector, *[id="blog_content"])

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

To save the change:

git commit -am'2.1 add test for blog content (failed)'; git push

Step 2.2 Fix the Blog Content Test

Let us fix the test now.

Step 2.2.1 Add the Blog Content to the HTML Template

In order to display the blog content, we need to add the following to the HTML template src/app/app.component.html:

Step 2.2.2 Define the Variable ‘content’ in the Component

However, as long as the variable ‘content’ is not defined, we will have added an empty div. To define the variable, we must change the component src/app/app.component.ts

import { Component, OnInit } from '@angular/core';
import { Http } from '@angular/http';
import { Response } from '@angular/http';
import 'rxjs/add/operator/map'

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  title : any = null
  content : any = null

  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);
                });
  }
}

That’s it: the e2e tests are successful:

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

  consuming-a-restful-web-service-with-angular App
    ✓ should display the title

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

Executed 3 of 3 specs SUCCESS in 3 secs.
[21:30:19] I/launcher - 0 instance(s) of WebDriver still running
[21:30:19] I/launcher - chrome #01 passed

To save the change:

git commit -am'2.2.2 Added content to HTML template and component (success)'; git push

Step 2.3 Explore the Result

Now let us have a look at what we have accomplished and let us open the browser on http://localhost:4200:

The good news is: the content is there.

😉

The bad news it: it is not readable because the HTML code in the blog content variable has been HTML escaped.

😦

This is the standard behavior in Angular. So what can we do now? The solution to the problem can be found in Step 2.3 of my original post: we need to set the innerHTML of the div instead of adding the content as text. But, as we are performing a “behavior-driven” approach, let us try to write the tests first.

Step 2.4 Improve the e2e Test Spec

Let us add an additional line to the test specification in order to make sure, we will see the HTML in the correct format:

import { browser, by, element } from 'protractor';

describe('Blog', () => {

  beforeEach(() => {
    browser.get('/');
  });

  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(element(by.css('h1')).getText()).toEqual('Angular 4 Hello World Quickstart');
    expect(blog_title.getText()).toEqual('Angular 4 Hello World Quickstart');
  });

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

With that, we test, whether the innerHTML of the div element starts with the correct HTML code. For that, we have made use of two functionalities of Jasmine:

  1. reading the innerHTML of an element with the getInnerHtml() function
  2. matching against a regular expression with toMatch(/regexp/)

As expected, the protractor test fails with the message

To save the change:

git commit -am'2.4 added innerHTML test for content with regular expression (fail)'; git push

Step 2.5 Fulfill the improved e2e Test

We can see that the content is escaped (e.g.  instead of ). Let us fix that by specifying the innerHTML like follows:

As soon as the content is loaded, the innerHTML ‘Loading…’ will be replaced by the content retrieved from WordPress.

Let us run the test:

$ protractor
[20:55:50] I/direct - Using ChromeDriver directly...
[20:55:50] I/launcher - Running 1 instances of WebDriver
Jasmine started
[20:55:56] W/element - more than one element found for locator By(css selector, app-root h1) - the first result will be used

  consuming-a-restful-web-service-with-angular App
    ✓ should display blog title

[20:55:57] W/element - more than one element found for locator By(css selector, h1) - the first result will be used
  Blog
    ✓ should display the blog title as header 1 and id="blog_title"
    ✓ should display the blog content

Executed 3 of 3 specs SUCCESS in 3 secs.
[20:55:57] I/launcher - 0 instance(s) of WebDriver still running
[20:55:57] I/launcher - chrome #01 passed

That was easy, again.

To save the change:

git commit -am'2.5 Fix the content innerHTML test (success)'; git push

Step 3: Explore the Final Result

Now let us head over to the browser on URL http://localhost:4200 again:

Even though there is no styling implemented yet, that looks much better now. This is, what we had in mind to implement today.

Excellent! Thump up!

 

As a wrap-up, the changes can be merged into the develop branch: the tests are successful and also the explorative “tests” have shown a correct result.


git checkout develop
git merge feature/0004-add-blog-content
git push

Summary

In this blog post, we have shown how to retrieve HTML-formated data from the WordPress API and display it in a correct format. In a “test-driven” approach, we have created Protractor e2e test specifications, before we have implemented the function.

Appendix: Error message: failed loading configuration file ./protractor.conf.js

After successfully cloning and installing the repo, I had seen following error message, when trying to perform the e2e tests:

$ protractor
[19:23:16] E/configParser - Error code: 105
[19:23:16] E/configParser - Error message: failed loading configuration file ./protractor.conf.js
[19:23:16] 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. (/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)

Resolution:

I have seen that the cli command was creating all files as user root. This was because I had defined

alias cli='docker run -it --rm -w /app -v $(pwd):/app oveits/angular-cli:1.4.3 $@'

After changing this to

alias cli='docker run -it --rm -w /app -v $(pwd):/app -p 4200:4200 -u $(id -u $(whoami)) oveits/angular-cli:1.4.3 $@'

and re-performing the cli npm i after the clone, the problem was resolved. However, this has caused the next ‘npm i’ issue described below, and it is better to perform following workaround:

Better:

  1. Keep the first version of the alias
  2. After applying ‘cli npm i’, perform the command sudo chown -R $(whoami) PROJECT_ROOT_DIR .

Appendix npm i: Error: EACCES: permission denied, mkdir ‘/.npm’

npm ERR! Linux 4.2.0-42-generic
npm ERR! argv "/usr/local/bin/node" "/usr/local/bin/npm" "i"
npm ERR! node v6.11.2
npm ERR! npm  v3.10.10
npm ERR! path /.npm
npm ERR! code EACCES
npm ERR! errno -13
npm ERR! syscall mkdir

npm ERR! Error: EACCES: permission denied, mkdir '/.npm'
npm ERR!     at Error (native)
npm ERR!  { Error: EACCES: permission denied, mkdir '/.npm'
npm ERR!     at Error (native)
npm ERR!   errno: -13,
npm ERR!   code: 'EACCES',
npm ERR!   syscall: 'mkdir',
npm ERR!   path: '/.npm',
npm ERR!   parent: 'consuming-a-restful-web-service-with-angular' }
npm ERR!
npm ERR! Please try running this command again as root/Administrator.

npm ERR! Please include the following file with any support request:
npm ERR!     /app/npm-debug.log

The reason is, that I had defined

alias cli='docker run -it --rm -w /app -v $(pwd):/app -p 4200:4200 -u $(id -u $(whoami)) oveits/angular-cli:1.4.3 $@'

With that, npm i is run as the vagrant user with ID=900. However, inside the container, neither the user “vagrant” nor the user ID 900 is defined. This seems to cause the problem that the cli npm i command wants to create a directory /$HOME/.npm, but $HOME is not set. Therefore, the user with id=900 wants to create the file /.npm, but only root is allowed to do so.

The better workaround is to define

alias cli='docker run -it --rm -w /app -v $(pwd):/app -p 4200:4200 oveits/angular-cli:1.4.3 $@'

without the -u option and perform a command

chown -R $(whoami) .

where needed (e.g. after each npm i command).

0

Behavior-Driven Angular – part 1: Consuming a RESTful Web Service with Angular 4


In this step-by-step tutorial, we will follow a behavior-driven development approach to create an Angular 4 application from Angular CLI. The hello-world-like application will consume the WordPress REST API and it will display a blog post title. We will create and run end-to-end test scripts that simulate the customer behavior on a Chrome browser within a Protractor headless Docker container.

As a side feature of this tutorial, we will demonstrate basic Git handling: we will learn how to create a GIT Repository, create a feature branch, commit code changes, and merge the tested and fully functional feature branch into the main development branch.

Check out this book on Amazon: Angular Test-Driven Development

Introduction

My post Consuming a RESTful Web Service with Angular 4 has grown much more popular than expected. Thanks a lot for your views! The September ist still not finished, and the article has achieved more than 3000 views in its fourth month. I hope the trend will keep on:

😉

So, why would I want to rework a blog post that seemingly plays a chord in the developer’s community? The reasons I have started to rework the example are:

  • I have come to a point, where I had the need to refactor the code. However, I do not like refactoring before I do not have a good coverage of end-to-end tests. This was fixed easily in my previous blog post Angular end-to-end Testing.
  • The next topic was not so easy to be resolved: I had created a working example, but when I have created a GIT repository from it, Angular CLI had a problem with new clones of that code. An Angular problem I could not resolve easily and it looked like I had to start from scratch. This is, what I am doing now, committing many snapshots to GIT. If I so, why not explaining to my audience, what I am doing and why? This way, the current post has become an example that demonstrates basic GIT handling.

This blog post will fix those two issues, I deem.

Even if I am tempted to automate many of the development process steps, we will keep it simple without the usage of DevOps tools like Jenkins with BitBucket, Sonar, BrowserStack, JMeter Integration and Docker data center integration you would find in real-world agile projects. Some of such topics can be explored in more detail on my other blog posts on Jenkins (explore the “Jenkins Tutorial” drop-down menu of my blog).

Why behavior driven development?

I have made a very good experience with behavior driven development (BDD), or “test first” development. Some years ago, I have applied this principle on a ProvisioningEngine I had developed based on Ruby on Rails and java (Apache Camel). The advantages of BDD I see are:

  • better customer view: if you follow the behavior driven principle, your first thought is, how the web pages look like and how the web pages behave with respect to customer actions — in detail. This helps me to always start with the customer view in mind.
  • higher motivation: as a developer, I find it rewarding to start with test development with “red” test cases that become green over time
  • higher quality: I often challenge myself to optimize my code (e.g. make it DRYer of more versatile). In this process, I do not want to sacrifice and previous achievements. A large set of unit tests and e2e test help me to keep the set of features intact in phases of code restructuring

Okay, as an Angular beginner, I deem I am far from being an ideal behavior driven Angular developer. However, at some point in future, I believe that I can increase my hobby development productivity by applying principles like BDD together with build&deployment automation based on TravisCI, CircleCI or a local Jenkins system to my development approach.

Overview

Along the way, we will get acquainted with a set of typical error messages and we will learn how to cope with them.

So, if you are ready for a quick ride into a simple “test first” strategy example with GIT repo handling, buckle up and start coding with me in four phases:

😉

  • Phase 1: Create a Hello World App based on Angular CLI
  • Phase 2: Adapt the end-to-end Tests
  • Phase 3: Adapt the Code
  • Phase 4: Verify the successful e2e Tests

If you do not care about BDD and GIT, then you might also want head over to the post Consuming a RESTful Web Service with Angular 4. Or better, follow the instructions you find here, but omit the steps related to e2e testing (protractor) and/or GIT.

Phase 1: Create a Hello World App based on Angular CLI

In this phase, we will

  • use an Angular CLI Docker image to create a new application
  • fix some problems with the end to end testing inherent in the standard hello world app
  • save and upload the changes to GIT

Step 1.0: Get access to a Docker Host with enough Resources

If you do not have access to a Docker host yet, I recommend following the step 0 instructions on my JHipster post. I recommend to use a Docker host with at least 1.5 GB RAM. To be honest, this is a guess. I always test on a 4 GB Docker Host Virtualbox VM, but I know that 750 MB RAM is not sufficient.

Step 1.1: Prepare an alias for later use

Let us first define an alias that helps us to shorten the commands thereafter.

(dockerhost)$ alias cli='docker run -it --rm -w /app -v $(pwd):/app -p 4200:4200 -u $(id -u $(whoami)) oveits/angular-cli:1.4.3 $@'

Why this complicated user option -u $(id -u $(whoami))? The reason is that

  • if we omit it, then all new files will be created as root, so we will get permissions problems later on
  • If we use ‘centos’, then the container will complain that he does not find the user ‘centos’ in its passwd file
  • If we use the ID of centos, then it works. However, it might not work in all cases. This time, the ID of centos user is 1000, and by chance, a user (named ‘node’) exists on the container as well. But let us live with this uncertainty for now.

With each cli something command, we will start a something command on an Angular CLI @ Alpine container originally created by Alex Such and enriched with git and bash by me.

Consider appending the alias command to your Docker host’s ~/.bashrc file, so the alias is persistent.

Step 1.2: Create a Project and install required Modules

Now let us create a new project and install the node modules via npm:

(dockerhost)$ cli ng new consuming-a-restful-web-service-with-angular
(dockerhost)$ cd consuming-a-restful-web-service-with-angular
(dockerhost)$ cli npm install
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 attempt registry request try #1 at 7:54:24 PM
npm http request GET https://registry.npmjs.org/fsevents
npm http 200 https://registry.npmjs.org/fsevents
npm info lifecycle consuming-a-restful-web-service-with-angular@0.0.0~preinstall: consuming-a-restful-web-service-with-angular@0.0.0
npm info linkStuff consuming-a-restful-web-service-with-angular@0.0.0
npm info lifecycle consuming-a-restful-web-service-with-angular@0.0.0~install: consuming-a-restful-web-service-with-angular@0.0.0
npm info lifecycle consuming-a-restful-web-service-with-angular@0.0.0~postinstall: consuming-a-restful-web-service-with-angular@0.0.0
npm info lifecycle consuming-a-restful-web-service-with-angular@0.0.0~prepublish: consuming-a-restful-web-service-with-angular@0.0.0
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@^1.0.0 (node_modules/chokidar/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.1.2: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm info ok

Step 1.3 (optional): Create a local GIT Repository

Now is a good time to create a git repository and to commit the initial code.

If you have not installed GIT on your Docker host, depending on the operating system of your Docker host, you might need to install it first (e.g. apt-get update; apt-get install -y git in case of Ubuntu, or yum install -y git in case of CentOS). Alternatively, you may want to use the git I have installed in the container. In that case, prepend a “do” before the git command, e.g. try cli git --version. However, a git diff does not look nice in a container, so I recommend to install GIT on your Docker host instead.

Now let us initialize the git repo, add all files and commit the changes:

(dockerhost)$ git init
(dockerhost)$ git add .
(dockerhost)$ git commit -m'initial commit'

Now let us start the service in a container:

(dockerhost)$ cli ng serve --host 0.0.0.0
** NG Live Development Server is listening on 0.0.0.0:4200, open your browser on http://localhost:4200/ **
Date: 2017-09-26T20:04:45.036Z
Hash: 24fe32460222f3b3faf2
Time: 15376ms
chunk {inline} inline.bundle.js, inline.bundle.js.map (inline) 5.83 kB [entry] [rendered]
chunk {main} main.bundle.js, main.bundle.js.map (main) 8.88 kB {vendor} [initial] [rendered]
chunk {polyfills} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 209 kB {inline} [initial] [rendered]
chunk {styles} styles.bundle.js, styles.bundle.js.map (styles) 11.3 kB {inline} [initial] [rendered]
chunk {vendor} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.29 MB [initial] [rendered]

webpack: Compiled successfully.

Step 1.4: Perform end-to-end Tests

Step 1.4.1: Use a Protractor Docker Image to perform the Tests

In the spirit of “test first” strategies of “behavior-driven development”, let us check the end-to-end tests that come with Angular CLI 1.4.3. We will see that they are broken and need to be adapted.

Like above, we will use a Docker container for the task. This time we will use the Docker image protractor-headless from webnicer. In a second terminal, we first define an alias, enter the project root folder and run protractor.

(dockerhost)$ alias protractor='docker run -it --privileged --rm --net=host -v /dev/shm:/dev/shm -v $(pwd):/protractor webnicer/protractor-headless $@'
(dockerhost)$ cd consuming-a-restful-web-service-with-angular
(dockerhost)$ protractor

[20:20:34] I/direct - Using ChromeDriver directly...
[20:20:34] I/launcher - Running 1 instances of WebDriver
Jasmine started

  consuming-a-restful-web-service-with-angular App
    ✗ should display welcome message
      - Failed: Error while waiting for Protractor to sync with the page: "Could not find testability for element."
          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 AppPage.getParagraphText (/protractor/e2e/app.po.ts:9:43)
          at Object. (/protractor/e2e/app.e2e-spec.ts:12:17)
          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)
      From: Task: Run it("should display welcome message") 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:16:5
          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)
      From asynchronous test:
      Error
          at Suite. (/protractor/e2e/app.e2e-spec.ts:10:3)
          at Object. (/protractor/e2e/app.e2e-spec.ts:3: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) consuming-a-restful-web-service-with-angular App should display welcome message
  - Failed: Error while waiting for Protractor to sync with the page: "Could not find testability for element."

Executed 1 of 1 spec (1 FAILED) in 0.878 sec.
[20:20:41] I/launcher - 0 instance(s) of WebDriver still running
[20:20:41] I/launcher - chrome #01 failed 1 test(s)
[20:20:41] I/launcher - overall: 1 failed spec(s)
[20:20:41] E/launcher - Process exited with error code 1

Even though my application is listening on port 4200,  we can see that the e2e tests have a problem.

Step 1.4.2: Correct the Protractor sync Issue

As already pointed out in this blog post, we need to add the option

useAllAngular2AppRoots: true

to our protractor.conf.js file. At the end, the file has following content (correction in blue):

// 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:4200/',
  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 } }));
  }
};

After that, the e2e test is still not successful:

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

  consuming-a-restful-web-service-with-angular App
    ✗ should display welcome message
      - Expected 'Welcome to !' to equal 'Welcome to app!'.
          at Object. (/protractor/e2e/app.e2e-spec.ts:12:37)
          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)
          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)

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

1) consuming-a-restful-web-service-with-angular App should display welcome message
  - Expected 'Welcome to !' to equal 'Welcome to app!'.

Executed 1 of 1 spec (1 FAILED) in 0.848 sec.
[20:30:40] I/launcher - 0 instance(s) of WebDriver still running
[20:30:40] I/launcher - chrome #01 failed 1 test(s)
[20:30:40] I/launcher - overall: 1 failed spec(s)
[20:30:40] E/launcher - Process exited with error code 1

Step 1.4.3: Correct the e2e Test Script

The reason is that the app.component.ts file is not correct. In the HTML template, we find a line

Welcome to {{title}}!

but in the component file, the title is missing:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

This is leading to following corrupt web page:

Let us correct this now (in blue):

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  title : any = null

  constructor() { }

  ngOnInit() {
     this.title = "app";
  }

}

Now the Web page looks better:

Now the e2e tests are successful:

$ protractor
[20:53:42] I/direct - Using ChromeDriver directly...
[20:53:42] I/launcher - Running 1 instances of WebDriver
Jasmine started

  consuming-a-restful-web-service-with-angular App
    ✓ should display welcome message

Executed 1 of 1 spec SUCCESS in 0.956 sec.
[20:53:50] I/launcher - 0 instance(s) of WebDriver still running
[20:53:50] I/launcher - chrome #01 passed

The angular CLI installation works as expected now.

Excellent! Thump up!

Let us save the changes:

(dockerhost)$ git add protractor.conf.js
(dockerhost)$ git commit -m'protractor.conf.js: added useAllAngular2AppRoots: true for avoiding sync problems'
(dockerhost)$ git add src/app/app.component.ts
(dockerhost)$ git commit -m'app component: defined missing title'

Now is the time to sign up with Github and save the project. In my case, I have created following project: a project like follows:

https://github.com/oveits/consuming-a-restful-web-service-with-angular

Once this is done, we can upload the changes as follows:

(dockerhost)$ git remote add origin https://github.com/oveits/consuming-a-restful-web-service-with-angular.git
(dockerhost)$ git push -u origin master

Phase 2: Adapt the end-to-end Tests

In this phase, we will

  • based on the input from the WordPress API, we will plan, how the web page should look like from a customer’s point of view.
  • We will adapt the e2e tests, so they reflect the (assumed) customer’s expectations.
  • We will save the changed code to the remote GIT repository.

Step 2.1: Planning

In an attempt to follow a behavior driven development process, we will write/adapt the end to end tests first, before we perform the changes. For this, let us outline our plan:

  • We would like to create a Web page that displays the title and content of a WordPress Article
  • the WordPress article of our choice is the first angular blog post I have written: the Angular 4 Hello World Quickstart blog post
  • The article will be retrieved dynamically from the WordPress API, a REST API.

Step 2.2: Explore the WordPress REST API

Let us have a look at the WordPress API. The WordPress API can be explored via the WordPress.com REST API console. We can display a list of blog posts like so:

We can see that the blog post we would like to display has the ID 3078 and the title and content star like follows:

  • title: “Angular 4 Hello World Quickstart”
  • content: “<p>In this hello world style tutorial, we will follow a step by step guide to a working Angular 4 application. We will also …

The  single blog post can be retrieved with the URL

https://public-api.wordpress.com/rest/v1.1/sites/oliverveits.wordpress.com/posts/3078

We can verify this by copying the URL into a Browser:

Step 2.3: Adapt the end-to-end Tests

With the knowledge about the title and content of the blog post, we can re-write the end-to-end (e2e) test. The e2e test is found in the e2e folder:

ls e2e/
app.e2e-spec.ts app.po.ts tsconfig.e2e.json

$ cat e2e/app.e2e-spec.ts
import { AppPage } from './app.po';

describe('consuming-a-restful-web-service-with-angular App', () => {
  let page: AppPage;

  beforeEach(() => {
    page = new AppPage();
  });

  it('should display welcome message', () => {
    page.navigateTo();
    expect(page.getParagraphText()).toEqual('Welcome to app!');
  });
});

Instead of searching for the text ‘Welcome to app’, let us search for the title “Angular 4 Hello World Quickstart”:

$ cat e2e/app.e2e-spec.ts
import { AppPage } from './app.po';

describe('consuming-a-restful-web-service-with-angular App', () => {
  let page: AppPage;

  beforeEach(() => {
    page = new AppPage();
  });

  it('should display the title', () => {
    page.navigateTo();
    expect(page.getParagraphText()).toContain('Angular 4 Hello World Quickstart');
  });
});

The e2e test should fail now with the message Expected 'Welcome to app!' to contain 'Angular 4 Hello World Quickstart'

$ protractor
[20:46:02] I/direct - Using ChromeDriver directly...
[20:46:02] I/launcher - Running 1 instances of WebDriver
Jasmine started

  consuming-a-restful-web-service-with-angular App
    ✗ should display welcome message
      - Expected 'Welcome to app!' to contain 'Angular 4 Hello World Quickstart'.
          at Object. (/protractor/e2e/app.e2e-spec.ts:12:37)
          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)
          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)

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

1) consuming-a-restful-web-service-with-angular App should display welcome message
  - Expected 'Welcome to app!' to contain 'Angular 4 Hello World Quickstart'.

Executed 1 of 1 spec (1 FAILED) in 0.907 sec.
[20:46:21] I/launcher - 0 instance(s) of WebDriver still running
[20:46:21] I/launcher - chrome #01 failed 1 test(s)
[20:46:21] I/launcher - overall: 1 failed spec(s)
[20:46:21] E/launcher - Process exited with error code 1

Step 2.4: Save the Changes on a separate GIT Branch

We believe that the e2e tests are correct now, so it is a good time to create a new git feature branch and commit the code:

git checkout -b feature/0001-retrieve-and-display-WordPress-title-from-API
git add .
git commit -m'adapted e2e tests to display WordPress blog title'
git push

Phase 3: Adapt the Code

Now, after having written the e2e tests, let us change the code, so our app fulfills the expectations.

Step 3.1: Define the HTML View

In the spirit of a behavior driven approach, let us define the view first. For that we replace the content of the app’s template file:

$ cat src/app/app.component.html
<h1>{{title}}</h1>

The output of the application now is:

This is because, in the Hello World app, we have set the title to the static value ‘app’. The e2e tests are not successful and the error ‘Expected ‘app’ to contain ‘Angular 4 Hello World Quickstart’.’ is thrown when we run protractor.

Step 3.2: Subscribe an Observable

As can be seen in many tutorials, we now subscribe to an observable like follows:

$ cat src/app/app.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  title : any = null

  constructor() { }

  ngOnInit() {
     //this.title = "app";
     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;
                        console.log(data);
                });
  }

}

We perform an HTTP GET on the WordPress API’s URL, map the response to a JSON object and subscribe the retrieved data. The data should contain a title, which we assign to the local title variable.

However, we will see in the log:

ERROR in /app/src/app/app.component.ts (16,11): Property '_http' does not exist on type 'AppComponent'.

And in the browser, we see:

Let us fix that now.

Step 3.3: Define private local _http Variable

In angular, we can define the private local _http variable in the constructor:

constructor(private _http: Http) {}

Once, this is done, the error message is changed to:

ERROR in /app/src/app/app.component.ts (12,30): Cannot find name 'Http'.

Step 3.4: Import Http Components

The used Http module is not known to our app component. Let us change this now. We add the following line

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

to the file src/app/app.component.ts. The error message changes to:

ERROR in /app/src/app/app.component.ts (18,18): Property 'map' does not exist on type 'Observable<Response>'.

Step 3.5: Import map

The map function needs to be imported as well:

import 'rxjs/add/operator/map'

Now we get an illegible error like follows:

ERROR in /app/src/app/app.component.ts (18,6): The type argument for type parameter 'T' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
  Type argument candidate 'Response' is not a valid type argument because it is not a supertype of candidate 'Response'.
    Types of property 'type' are incompatible.
      Type 'ResponseType' is not assignable to type 'ResponseType'. Two different types with this name exist, but they are unrelated.
        Type '"basic"' is not assignable to type 'ResponseType'.

Step 3.6: Import Response Type

We finally can get rid of the quite illegible error message by adding another import:

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

However, this still does not lead to the desired result. In the browser we see an empty page:

and the e2e tests fail with the following message:

$ protractor
...
Failed: Angular could not be found on the page http://localhost:4200/. 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

Step 3.7: Add HttpModule in the app Module

The solution of the above error lies in the src/app/app.module.ts (added parts in blue). We first need to add the HttpModule to the imports, which alters the error message to

$ cat src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule }    from '@angular/http';

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

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    HttpModule,
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

This seems to have been the last stepping stone towards success:

Phase 4: Verify the successful e2e Tests

Now the e2e tests are successful:

$ protractor
[22:16:06] I/direct - Using ChromeDriver directly...
[22:16:06] I/launcher - Running 1 instances of WebDriver
Jasmine started

  consuming-a-restful-web-service-with-angular App
    ✓ should display welcome message

Executed 1 of 1 spec SUCCESS in 1 sec.
[22:16:14] I/launcher - 0 instance(s) of WebDriver still running
[22:16:14] I/launcher - chrome #01 passed

That is, how the e2e Tests should look like. Success!

Excellent! Thump up!

Step 4.2: Save the changes to the develop branch on GIT

Since our new feature “retrieve and display a blog title from WordPress API” has been verified to work fine, it is time to commit the change and push it to the remote repository:

git add .
git commit -m'added all code needed for successful e2e tests'
git push
git checkout -b develop
git push

In addition to that, we can create a new “develop” branch, if it does not exist yet:

git checkout -b develop
git push

In case the develop branch exist already, you need to merge the code to the instead of creating the develop branch:

git checkout develop
git merge feature/0001-retrieve-and-display-WordPress-title-from-API
git push

It makes sense to allow a merge to the develop branch only if the code is fully tested. This way, we will never break the code in the develop branch.

For large teams, several measures can be taken to make sure that only high quality code enters the develop branch: e.g. on BitBucket GIT, you can allow a merge only, if code has been reviewed and acknowledged by a certain number of team members. Moreover, you can integrate the repository with a Jenkins system: with the correct plugins, you can make sure that a merge is allowed only in case all quality gates (e2e test, unit tests, style, performance, …) in the Jenkins pipeline are met.

However, if you are a hobby developer working on a , it is probably sufficient if you run the tests manually before you merge the changed code into the develop or master branch.

Summary

In this hello world style step-by-step guide, we have learned

  • How to create a new Hello World project using Angular CLI, repair the e2e tests and save the changes on GIT.
  • How to create/adapt the e2e tests in advance a “test first” manner.
  • How to consume a REST service using Angular 4 and verify the result using the e2e test scripts we have created before.

Next Steps

In part 2 of this series, we will learn how to add and display HTML content to the body of our application. We will see that we cannot just use the method we have used for the title. If we do so, we will see escaped HTML code like follows:

<p>In this hello world style tutorial,…

We will show how to make Angular accept the HTML code and display it correctly.

References:

Appendix A: Adding Docker Support

This is, how I have added Docker support for the application, following my tl:dr of the blog post Angular 4 Docker Example.

A.1 Add Dockerfile and NginX config

git clone https://github.com/oveits/consuming-a-restful-web-service-with-angular
git checkout -b feature/0002-docker-support
curl -O https://raw.githubusercontent.com/avatsaev/angular4-docker-example/master/Dockerfile
curl -O https://raw.githubusercontent.com/avatsaev/angular4-docker-example/master/nginx/default.conf
mkdir nginx
mv default.conf nginx/

remove ‘package-log.json’ from Dockerfile

git add .
git commit -m 'added Dockerfile and nginx config file'
git push

A.2 Build the Docker  Image

On a docker host, I have issued following commands:

docker build . --tag oveits/consuming-a-restful-web-service-with-angular:v0.2
docker push oveits/consuming-a-restful-web-service-with-angular:v0.2
docker tag oveits/consuming-a-restful-web-service-with-angular:v0.2 oveits/consuming-a-restful-web-service-with-angular:latest
docker push oveits/consuming-a-restful-web-service-with-angular:v0.2
docker push oveits/consuming-a-restful-web-service-with-angular:latest

A.3 Run the Service

$ alias consuming='docker run --rm --name consuming-a-restful-web-service-with-angular -d -p 80:80 oveits/consuming-a-restful-web-service-with-angular $@'
$ consuming

A.4 Access the Service

In a browser, head to the public DNS of the image:

Works!

Excellent! Thump up!

 

 

0

Hello Java Hipster: Angular 4 and Spring Boot


In this blog post, Java Hipster will help us creating a mini blog application based on Angular 4 and Spring Boot. Angular is a popular framework for creating reactive single page applications, while Spring Boot is a robust java-based backend framework that helps you create database access and RESTful APIs.

We will closely follow the JHipster introduction on YouTube and explore administrative functions like user management, logging and API management (with swagger). After creating an importing a simple model for a blog, we will manipulate the blog and its entries via the GUI.

For your convenience, we will run everything in a Docker container, so you do not need to install any of the required software packages (apart from Docker itself). As a little goody, we will outline how to add some additional security functions: we will restrict delete access to authorized users. That is an extension to the many good examples shown in the JHipster introduction on YouTube video.

Tools and Versions used

  • Vagrant 1.8.6
  • Virtualbox 5.0.20 r106931
  • jhipster Docker image v4.6.2

Why JHipster?

JHipster is a Yeoman-based code generator for a complete Web application based on Angular and Spring Boot, two popular frameworks for front-end and backend systems.

  • Angular is a popular single page frontend framework
  • Spring Boot is a high-performance backend framework

mixed with CSS Bootstrap styles, and an easy-to use web-based modeling tool that creates text-based models that can be used to generate the corresponding database and REST entities/functions, JHipster, JHipster is a quick way to get projects started.

I have not checked out yet all other features mentioned on their home page, like

  • microservice support with JHipster Registry, Netflix OSS, Elastic stack and Docker

We will build our first application within a Docker container.

Step 0: Install a Docker Host

This time (again), our application will need more than 750 MB RAM. Therefore, we cannot use my beloved Katacoda as our Docker playground, which is limited to this amount of DRAM. Instead, you need to get access to a Docker host with, >~ 2 GB RAM. A nice way of installing an Ubuntu Docker host via Vagrant is described here (search for the term “Install a Docker Host”).

Prerequisites of this step:

  • I recommend to have direct access to the Internet: via Firewall, but without HTTP proxy. However, if you cannot get rid of your HTTP proxy, read this blog post.
  • Administration rights on you computer.

Steps to install a Docker Host VirtualBox VM:

Download and install Virtualbox (if the installation fails with error message “Oracle VM Virtualbox x.x.x Setup Wizard ended prematurely” see Appendix A of this blog post: Virtualbox Installation Workaround below)

1. Download and Install Vagrant (requires a reboot)

2. Download Vagrant Box containing an Ubuntu-based Docker Host and create a VirtualBox VM like follows:

(basesystem)# mkdir ubuntu-trusty64-docker ; cd ubuntu-trusty64-docker
(basesystem)# vagrant init williamyeh/ubuntu-trusty64-docker
(basesystem)# vagrant up
(basesystem)# vagrant ssh

Now you are logged into the Docker host and we are ready for the next step: to download the Docker image and to start the application in a container.

Step 1: Start JHipster Docker Container

(dockerhost)$ docker run -it -p 8080:8080 -p 3001:3001 -p 9000:9000 -p 9060:9060 -v $(pwd):/localdir jhipster/jhipster:v4.6.2 bash
(containter)# cd /localdir

Note: You also can omit the version tag (:v4.6.2) and work with the latest image. However, v4.6.2 is the version tested in this blog post.

Note that you might have to view (docker ps) and stop (docker stop) any container that is running on port 8080 first, since I have experienced problems, when mapping the port 8080 to another port than 8080 (I need to try again). If you have followed step 0, you will need to issue the command

(dockerhost) sudo docker stop cadvisor

Step 2: Create, Start and Login to App

Step 2.1: Create App

(container)# mkdir blog; cd blog
(container)# yo jhipster

I have chosen

  • monolithic
  • base name: blog
  • default java package name: org.jhipster <– should use another name, probably
  • authentication: JWT
  • database type: SQL
  • production database: MySQL
  • development database: H2 with disk-based persistence
  • Hibernate 2nd level cache: Yes, with ehcache (local cache, for a single node)
  • Other Technologies (choose none, just press enter)
  • Framework: Angular 2
  • libSass: Yes
  • Internationalization: Yes
  • Chosen English and German
  • Testing Frameworks (bBesides JUnit and Karma:
    • Gatling
    • Protractor

Wait for ~2-4 minutes

Step 2.2 Start App

In order to be able to explore an (empty) JHipster app, we can start the application as follows:

Start Spring Boot Application:

In the project root directory, type:

(container)# ./mvnw

Only as a reference: Start Webpack development server (was not needed in my case!)

container)# yarn start

Step 2.3 (optional): Connect to (default) App

In a local browser, connect to localhost:8080:

Sign in as admin with password “admin”.

Step 2.4 (optional): Explore (default) App

The Entities are empty yet, but unser Administration we can see the user management, metrics etc:

We have not yet created and imported a model, so this is still the default JHipster app. It comes with a User Model, though, with three users created per default:

  • admin / admin
  • user / user
  • system / system?

Under Administration -> Metrics, we can see JVM Metrics as well as HTTP statistics:

Under Administration -> Configuration, we see the Spring properties, even though they seem to be read-only:

However, the log configuration can be edited in the way that a log can be chosen e.g. to show DEBUG instead of WARN messages only

Under Administration -> API we find a swagger page that helps you to test your REST services:

Okay, I am cheating a little here: you will not be able to see the blog-resource yet, since we will create it not before the next two steps. You will only see the account, the profile info, the user-jwt controller and the user resource. It can be used to test the interface. E.g. you can read all users by clicking the button

Try it out!

of users-resource GET /api/users and we will receive something like follows:

[
  {
    "id": 1,
    "login": "system",
    "firstName": "System",
    "lastName": "System",
    "email": "system@localhost",
    "imageUrl": "",
    "activated": true,
    "langKey": "en",
    "createdBy": "system",
    "createdDate": "2017-08-15T11:47:06.180Z",
    "lastModifiedBy": "system",
    "lastModifiedDate": null,
    "authorities": [
      "ROLE_USER",
      "ROLE_ADMIN"
    ]
  },
  {
    "id": 3,
    "login": "admin",
    "firstName": "Administrator",
    "lastName": "Administrator",
    "email": "admin@localhost",
    "imageUrl": "",
    "activated": true,
    "langKey": "en",
    "createdBy": "system",
    "createdDate": "2017-08-15T11:47:06.180Z",
    "lastModifiedBy": "system",
    "lastModifiedDate": null,
    "authorities": [
      "ROLE_USER",
      "ROLE_ADMIN"
    ]
  },
  {
    "id": 4,
    "login": "user",
    "firstName": "User",
    "lastName": "User",
    "email": "user@localhost",
    "imageUrl": "",
    "activated": true,
    "langKey": "en",
    "createdBy": "system",
    "createdDate": "2017-08-15T11:47:06.180Z",
    "lastModifiedBy": "system",
    "lastModifiedDate": null,
    "authorities": [
      "ROLE_USER"
    ]
  }
]

We also can choose the German language, since we have chosen English AND German:

Step 3: Create and Download Your Model

You can create and download the model from JDL Studio and access it from your application. If you have followed the instructions above, you need to make sure the file is visible from the JHipster container. For that, you need to place it to the folder you have mapped in step 1 by using the -v docker run flag.

It might be more convenient to cut&paste the content into a file /localdir/blog/model.jh within the container:

entity Blog {
name String required minlength(3),
handle String required minlength(2)
}

entity Entry {
title String required,
content TextBlob required,
date ZonedDateTime required
}

entity Tag {
name String required minlength(2)
}

relationship ManyToOne {
Blog{user(login)} to User,
Entry{blog(name)} to Blog
}

relationship ManyToMany {
Entry{tag(name)} to Tag{entry}
}

paginate Entry, Tag with infinite-scroll

The graphical representation of the model looks like follows:

The modes is re-using the User model that is present in JHipster per default. Now, each blog is mapped to a user. Each blog can have many blog entries and each entry can have many tags, while each tag can be attached to many entries.

In the next step, we will import this model.

Step 4: Import Model

To import the model, we stop the currently running mvnh process (if so), and import the model as follows:

(container)# yo jhispter:import-jdl ./model.jh
Overwrite liquibase/master.xml?: a

Step 5: Start Spring Boot App

After 5 minutes or so, we can start the application again:

(container)# ./mvnw

Note: yarn start alone will not do the trick. See https://stackoverflow.com/questions/43291477/jhipster-cannot-login-after-starting-with-yarn-start-webpack

Step 6: Connect and Login as admin

In a Browser, we need to navigate to localhost:8080 and login as admin with password admin or user with password user. It does not make a difference for now.

We will notice, that now the model entities are available:

Step 7: Create a Blog

Let us choose “Blog” and Create a new Blog:

Click Save.

Step 8: Create a Blog Entry

Now let us Create a new Entry on Entities -> Entry:

We can see that the Blog field is not mandatory (otherwise the mouse will show that the Save button cannot be pressed). However, let us choose the Admin’s Blog we have just created:

Note, that I have put only part of the content in headline style.

 

The ID is 2 instead of 1, since I have played around already and I have created and deleted a blog entry already.

Step 9: Add Multi User Support

In this step we will make sure that a user can only edit and delete any entity that belongs to the logged in user.

Step 9.1: Log out and log in as User “user”

We now log out and log in as user “user” with password “user”. You will notice that you can see and edit or delete any entity that belongs to the admin. This is a no-go for any multi-user application and needs to be fixed.

Step 9.2: Add a User’s Blog

Let us first create a User’s Blog and a Blog entity like follows:

We will see both Blog entries:

(since you have not yet fixed the HTML Display, the content will look different in your case, you will see the escaped HTML instead like

However the Date is missing. Oups, when checking with my screenshot above, I have entered a wrong date far in the future. And there is a bad side-effect: when I try to edit or delete the entry, the application does not react.

For the records:

  • we need to make sure that the user input is verified
  • we need to find a way to delete this entry

For now, let us create a new entry:

This is an entry that can be edited and deleted, if needed.

Note: It seems to take quite long, until the entry is loaded. Also, it will be “Loading…” forever, if you click on View of the upper entry and use the “back” button on that page. Maybe this is the case because of the bad entry #2?

If you are reloading the page, it does display, though and the new entry can be edited, if needed:

Step 9.3: Test write access to admin’s entries

Now click Edit of the admin’s entry and you will see that the user is allowed to edit admin’s entries.

We need to take two measures:

  • the Edit and Delete buttons should not be visible (or grayed out and not functional) on entries that do not belong to to the logged in user.
  • the user should not be able to circumvent security by directly calling the Edit or Delete function for entities of other users.

Step 9.4: Start Application in develop Mode

In order to view the results of any file changes immediately, you need to issue the command

(container)# yarn start

and connect to port 9000 instead of 8080:

Log in as user “user” again.

Step 9.5: Remove foreign Blogs from Blog Table View

For now, as user named “user” we can see both blogs we had created:

A quick&dirty way to remove the Edit and Delete buttons from the user’s view is to remove the admin’s blogs from the view, as shown in the JHipster introduction on YouTube:

Let us search for the function getAllBlogs in the java File BlogResource (found on the container as src/main/java/org/jhipster/web/rest/BlogResource.java)

and we change

findAll()

by

findByUserIsCurrentUser()

which is available on JHipster on any model per default.

After restarting ./mvnw, the admin’s blog will be removed from the blog table:

Step 9.6: Restrict Access for Single Blog View

In the previous step, we have removed the admin’s blog from the user’s blog table view. However, the admins’s blog can still be accessed by the user named “user”, if he knows (or guesses) the ID:

This is a topic that is not mentioned in the JHipster introduction on YouTube. So, let that fix too:

On java/org/jhipster/repository/BlogRepository.java, we can reset the return value to “null”, if the found blog does not belong to the logged in user. For that we add:

// file: java/org/jhipster/repository/BlogRepository.java
import import org.jhipster.security.SecurityUtils;
...
    @GetMapping("/blogs/{id}")
    @Timed
    public ResponseEntity getBlog(@PathVariable Long id) {
        log.debug("REST request to get Blog : {}", id);
        Blog blog = blogRepository.findOne(id);
        
        // The user is not allowed to access this blog, if it is not owned by this user:
        if (!blog.getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin())) { blog = null; }

        return ResponseUtil.wrapOrNotFound(Optional.ofNullable(blog));
    }

For that we import the SecurityUtils and compare the user login of the found blog with the user login of the user that is logged in. If it is not equal, we just reset the found value to null.

With that, the blog is not accessible anymore, after we have restarted ./mvnw:

Excellent! Thump up!

This is, what we want to achieve: blog/3 is not visible because it is not owned by the logged in user.

However, let us double-check, that general access to the blogs is not broken. For that, let us access the user’s blog, /blog/4 in my case:

Yes, that’s it.

Note: since we have not done anything on the DELETE function, any user, who knows how to access the API, will still be able to DELETE or UPDATE the entry. This will be covered in the next blog post, where we will have a closer look to the API created by Spring Boot. To cut is short, following code change (in red) will be shown there:

    @DeleteMapping("/blogs/{id}")
    @Timed
    public ResponseEntity deleteBlog(@PathVariable Long id) {
        log.debug("REST request to delete Blog : {}", id);
        
        Blog blog = blogRepository.findOne(id);     
        
        if(blog != null) {
        	if (!blog.getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin())) { 
        		// The user is not allowed to delete this blog, if it is not owned by this user:
            	        log.debug("Found blog, but user is not allowed to delete it");
            	        return ResponseEntity.status(403).headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id.toString())).build();
        	} else {
	        	blogRepository.delete(id); 
	        	return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id.toString())).build();
        	}
        } else {
        	return ResponseEntity.notFound().headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id.toString())).build();
        }
        
        
    }

Note: A similar code change is needed for the PUT (Update) function.

Step 10: Fix the HTML Display

We can see on Entities -> Entry -> View that the HTML display is not correct, since it is escaped:

Let us fix that now. For that, navigate to src/main/webapp/app/entities/entry/entry-detail.component.html (you can do that inside the container via vi, or, since the content is mapped to the Docker host, we also can use an IDE like Visual Studio Code from outside of the container:


There, we can change

<span>{{entry.content}}</span>

by

<span [innerHTML]="entry.content">Loading...</span>

Unlike what is shown in the JHipster introduction, the application running on localhost:8080 does not seem to recognize, when a file is changed. Even stopping and restarting mvnw did not change anything:

Instead, I have run

(container)# yarn start

within the container and I have connected to the Webpack in development mode on port 9000:

This did the trick.

Excellent! Thump up!

Now, I can change the files from within the container (by opening an extra session into the Docker container via docker exec -it <containerid> bash, and webpack will re-transpile the code within seconds (but I need to refresh the Browser, it seems).

Note: after running the application some days on my notebook, I have logged in as admin again (without reloading the application) and the change now was visible on port 8080 as well:

I guess, this problem needs some more investigation…

 

Summary

In this blog post, we have created a little blog application using the Java Hipster code generator. For that, we have

  • installed a Docker host, if needed
  • started a Docker JHipster container on the Docker host
  • run and explored the default administration functions of the JHipster application like
    • user management,
    • API exploration (via swagger) and
    • logging
  • edited a model for our application using the JDL Studio Web Page
  • imported the model file of a blog application (blogs, entries, tags) into JHipster
  • explored the new functions of the application
  • tweaked the Spring Boot Read and Delete function, so that only the owner is able to see and delete a blog (this is an addition to what you will find on the JHipster introduction on YouTube.)
  • tweaked the view of the blog within an Angular template to display HTML content correctly

We have seen, how easy it is to import an arbitrary model into the default JHipster application and to create and display the entities defined in the model. In our case, we have created a simple blog application with blogs, entries and tags.

Coming Soon

  • I already have started a Blog Post, where I am exploring the REST API that is automatically generated by JHipster. We will learn, how to use swagger to find the correct curl commands, how to authenticate the service and how to tweak the REST interface, so  only the owner of a blog entity is allowed to delete the entry. Follow this blog, if you are interested in the blog post.
0

Angular 4: Automatic Table of Contents – Part 2: Adding Links


In this short blog post, we will learn how to enrich the table of contents (we had added in the previous blog post) with links to the corresponding headline. The end result will look similar to the following picture and a click on one of the links will cause the browser to jump to the corresponding headline:

The method will be applied to dynamic content retrieved from the WordPress REST API. This poses some extra challenges caused by Angular’s security policies: per default, headline IDs are stripped, preventing us to successfully reference those IDs in the links. We will show below, how to make use of the SafeHtml module to successfully face those challenges.

Step 0: Download the Code and start and connect to the Server

Even though this is no prerequisite, I recommend to perform the steps within a Docker container. This way I can provide you with a Docker image that has all necessary software installed.

Fore more details on how to install a Docker host on Windows using Vagrant and Virtualbox, please see Step 1 of my Jenkins tutorial.

On a Docker host we run

(dockerhost)$ mkdir toc; cd toc
(dockerhost)$ docker run -it -p 8001:8000 -v $(pwd):/localdir oveits/angular_hello_world:centos bash
(container)# git clone https://github.com/oveits/ng-universal-demo
(container)# cd ng-universal-demo
(container)# git checkout 8b3948

Now we are up and running and we can start the server:

(container)# npm run watch &
(container)# npm run server

With that, any change of one of the files will cause the server to reload.

Open a browser and head to localhost:8001/blog

This is the end situation of the previous blog post: there is a table of contents, but the links to the headlines are missing. Those are the ones, will add today.

Note: if you are using Vagrant with a Docker host on a VirtualBox VM (as I do), per default, there is only a NAT-based interface and you need to create port-forwarding for any port you want to reach from outside (also the local machine you are working on is to be considered as outside). In this case, we need to add an entry in the port forwarding list of VirtualBox to map from Windows port 8001 to the VM’s port 8001 (in our case).

Step 1: Add IDs to the Headlines

First we need to make sure that each Headline has a unique ID we later can reference. For that, we assign some string “id394752934579″ concatenated by an increasing id_suffix within the getToc function we had defined in the previous blog post. This way we can be quite sure that the auto-created ID is unique on the page:

  private getToc(content: any) {
     // create div for holding the content
     var contentdiv = document.createElement("div");
     contentdiv.innerHTML = content;

     // create an array of headlines:
     var myArrayOfHeadlineNodes = [].slice.call(contentdiv.querySelectorAll("h1, h2"));

...

     // will be appended to the node id to make sure it is unique on the page:
     var id_suffix = 0;

     // loop through the array of headlines
     myArrayOfHeadlineNodes.forEach(
       function(value, key, listObj) {

...

           // if headline has no id, add a unique id
           if ("" == value.id) {
               value.id = "id394752934579" + ++id_suffix;
           }

...
       }
     );

...

     return(toc.innerHTML);
  }

Step 2: Apply the changed IDs to the dynamically retrieved Content

Since the myArrayOfHeadlineNodes is a list of references to the contentdiv’s headlines, the IDs of the headlines within the contentdiv variable has been manipulated in the previous step. However, the class variable “content” is unaffected by this change. Therefore, we need to apply the new enriched content to the class variable:

import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
...
  private getToc(content: any) {
     // create div for holding the content
     var contentdiv = document.createElement("div");
     contentdiv.innerHTML = content;

     // create an array of headlines:
     var myArrayOfHeadlineNodes = [].slice.call(contentdiv.querySelectorAll("h1, h2"));

...

     // update the content with the changed contentdiv, which contains IDs for every headline
     //   note that we need to use the saniztizer.bypassSecurityTrustHtml function in order to tell angular
     //   not to remove the ids, when used as [innerHtml] attribute in the HTML template
     this.content = this.sanitizer.bypassSecurityTrustHtml(contentdiv.innerHTML);

     return(toc.innerHTML);
  }

Just before returning, we write back the changed contentdiv variable’s content to the class variable this.content. But why did we apply this complicated bypass Security function?

 this.content = this.sanitizer.bypassSecurityTrustHtml(contentdiv.innerHTML);

Note that we apply the content as innerHTML in the HTML template (src/app/+blog/blog.module.html) like follows:

If the content is just a string containing HTML code, Angular will strip IDs and names from the innerHTML as a security measure. However, if we use the green saniziter code above instead, we explicitly tell Angular that it should trust the content, preventing Angular from stripping the IDs we are so keen on.

Step 3: Verify changed IDs of the Content’s Headlines

Unfortunately, the usage of “document” is not compatible with server-side rendering, as we have pointed out in the previous blog post. Therefore, we will not find the IDs in the server-provided HTML source: if we right-click on the page choose to view the source code, the IDs will be missing:

However, the IDs can be verified by pressing F12 in a Chrome browser and by navigating to the “elements” section:

It displays the dynamic HTML code as seen by the browser and we see that the id is correctly applied.

Step 4: Add and test Links to the Table of Contents

Now, since call headlines are equipped with unique IDs, we can make use of it. Within the forEach loop above, we add the following list item to the table of contents list:

(we need to use a screenshot, since this blog is saved on WordPress and WordPress refuses to save the embedded HTML code correctly)

We place it within the forEach loop:

  private getToc(content: any) {
     // create div for holding the content
     var contentdiv = document.createElement("div");
     contentdiv.innerHTML = content;

     // create an array of headlines:
     var myArrayOfHeadlineNodes = [].slice.call(contentdiv.querySelectorAll("h1, h2"));

...

     // will be appended to the node id to make sure it is unique on the page:
     var id_suffix = 0;

     // loop through the array of headlines
     myArrayOfHeadlineNodes.forEach(
       function(value, key, listObj) {

...

           // if headline has no id, add a unique id
           if ("" == value.id) {
               value.id = "id394752934579" + ++id_suffix;
           }
...
           
       }
     );

     // debugging:
     console.log(toc.innerHTML);

     // update the content with the changed contentdiv, which contains IDs for every headline
     //   note that we need to use the saniztizer.bypassSecurityTrustHtml function in order to tell angular
     //   not to remove the IDs, when used as [innerHtml] attribute in the HTML template
     this.content = this.sanitizer.bypassSecurityTrustHtml(contentdiv.innerHTML);

     return(toc.innerHTML);
  }

Step 5: Verify the Links

With the changes of the previous step, the page will contain links in the table of contents after being reloaded:

Moreover, the page will jump to the desired position, if one of the links is clicked. E.g. after clicking on the Step 1 link, we will see:

Excellent! Thump up!

This is, what we have aimed at in this short session.

Caveat A: Error Message ‘document is not defined’ (workaround given)

In the window, where npm run server is running, we see following error message, when we access the /blog URL:

ERROR ReferenceError: document is not defined
    at BlogView.exports.modules.490.BlogView.getToc (/localdir/oveits__ng-universal-demo/dist/0.server.js:59:35)
    at SafeSubscriber._next (/localdir/oveits__ng-universal-demo/dist/0.server.js:52:31)
    at SafeSubscriber.__tryOrUnsub (/localdir/oveits__ng-universal-demo/dist/server.js:601:16)
    at SafeSubscriber.next (/localdir/oveits__ng-universal-demo/dist/server.js:548:22)
    at Subscriber._next (/localdir/oveits__ng-universal-demo/dist/server.js:488:26)
    at Subscriber.next (/localdir/oveits__ng-universal-demo/dist/server.js:452:18)
    at MapSubscriber._next (/localdir/oveits__ng-universal-demo/dist/server.js:29934:26)
    at MapSubscriber.Subscriber.next (/localdir/oveits__ng-universal-demo/dist/server.js:452:18)
    at ZoneTask.onComplete [as callback] (/localdir/oveits__ng-universal-demo/dist/server.js:32771:30)
    at ZoneDelegate.invokeTask (/localdir/oveits__ng-universal-demo/dist/server.js:90301:31)

Workaround:

The reason for the problem is that node.js (i.e. the server side) does not understand some of the javascript code we have used in getToc function. A workaround is found on the Readme of Universal (“Universal Gotchas”):

The difference is shown the output of a git diff:

$ git diff 8ca27d5..3225b9a
--- a/src/app/+blog/blog.module.ts
+++ b/src/app/+blog/blog.module.ts
@@ -7,6 +7,9 @@ import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
 import { Pipe, PipeTransform } from '@angular/core';
 import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
+import { PLATFORM_ID } from '@angular/core';
+import { isPlatformBrowser, isPlatformServer } from '@angular/common';
+import { Inject } from '@angular/core';

 @Component({
   selector: 'blog-view',
@@ -18,7 +21,7 @@ export class BlogView implements OnInit {
   content: any = null;
   toc: any = null;

-  constructor(private http: Http, private sanitizer: DomSanitizer) {
+  constructor(private http: Http, private sanitizer: DomSanitizer, @Inject(PLATFORM_ID) private platformId: Object) {

   }

@@ -31,8 +34,11 @@ export class BlogView implements OnInit {
                 .map((res: Response) => res.json())
                  .subscribe(data => {
                        this.title = data.title;
-                       this.content = data.content;
-                        this.toc = this.getToc(this.content);
+                        this.content = data.content;
+                        if (isPlatformBrowser(this.platformId)) {
+                            // Client only code.
+                            this.toc = this.getToc(this.content);
+                        }
                         //console.log(data);
                         console.log("content = " + this.content.changingThisBreaksApplicationSecurity);
                 });

I call it “workaround” and not “resolution” because I would prefer the table of contents code to play well with the server. With this workaround, we just make sure that the code is not run on the server. This was, the table of contents is not shown in the HTML source.

Caveat B: ERROR TypeError: this.html.charCodeAt is not a function (resolved)

In the window, where npm run server is running, we see following error message, when we access the /blog URL:

ERROR TypeError: this.html.charCodeAt is not a function
    at Preprocessor.advance (/localdir/oveits__ng-universal-demo/dist/server.js:111990:24)
    at Tokenizer._consume (/localdir/oveits__ng-universal-demo/dist/server.js:22767:30)
    at Tokenizer.getNextToken (/localdir/oveits__ng-universal-demo/dist/server.js:22725:23)
    at Parser._runParsingLoop (/localdir/oveits__ng-universal-demo/dist/server.js:81548:36)
    at Parser.parseFragment (/localdir/oveits__ng-universal-demo/dist/server.js:81503:10)
    at Object.parseFragment (/localdir/oveits__ng-universal-demo/dist/server.js:35450:19)
    at Parse5DomAdapter.setInnerHTML (/localdir/oveits__ng-universal-demo/dist/server.js:33459:49)
    at Parse5DomAdapter.setProperty (/localdir/oveits__ng-universal-demo/dist/server.js:33100:18)
    at DefaultServerRenderer2.setProperty (/localdir/oveits__ng-universal-demo/dist/server.js:34618:109)
    at BaseAnimationRenderer.setProperty (/localdir/oveits__ng-universal-demo/dist/server.js:92084:23)

Resolution

I have found this issue on git, with some plunkr code that has helped me to sort this out: it seems like I need to define the content, title and toc with types as follows (instead of e.g. content: any = null):

 private title : SafeHtml|String = '';
 private toc : SafeHtml|String = '';
 private content : SafeHtml|String = '';

As a side effect, I had to exchange

this.content = this.sanitizer.bypassSecurityTrustHtml(data.content);

by the simpler expression

this.content = data.content;

and

console.log("content = " + this.content.changingThisBreaksApplicationSecurity);

by the simpler expression

console.log("content = " + this.content);

After those changes, the error messages disappeared.

The first commit, where this is implemented is 2adaa98  and can be reviewed with:

git clone https://github.com/oveits/ng-universal-demo
git checkout 2adaa98

Summary

In this short session, we have enriched the the automatic table of contents of the the previous blog post with links to the corresponding headline position. For that, we have

    • added IDs to all headlines
    • made sure the IDs are not stripped by Angular’s default security policy
    • added links to the table of contents
    • verified the results in Chrome’s debugger
    • tested the results

We only have used plain vanilla javascript functionality to add the IDs and links. Most probably, there exist more Angular’ish ways of performing the same (or better) result. We will explore this in future, most probably. However, it works well for my use case for now.

Download the Code

The code can be cloned via

# git clone https://github.com/oveits/ng-universal-demo
# cd ng-universal-demo
# git checkout 8ca27d5   # to be sure you are working with exact same that has been created in this blog

In addition to the features described, you will see that I have included a ScrollTo function from the bottom of the page to the top of the page.

View the Component Code

For your reference, see here the full code (changes highlighted in bold green) we have applied to the file

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';
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
//import * as angular from "angular";

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

export class BlogView implements OnInit {
  title: any = null;
  content: any = null;
  toc: any = null;

  constructor(private http: Http, private sanitizer: DomSanitizer) {

  }

  ngOnInit(){
    this.getMyBlog();
  }

  private getMyBlog() {
    return 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;
                        this.toc = this.getToc(this.content);
                        //console.log(data);
                        console.log("content = " + this.content.changingThisBreaksApplicationSecurity);
                });
  }

  private getToc(content: any) {
     // create div for holding the content
     var contentdiv = document.createElement("div");
     contentdiv.innerHTML = content;

     // create an array of headlines:
     var myArrayOfHeadlineNodes = [].slice.call(contentdiv.querySelectorAll("h1, h2"));

     // initialize table of contents (toc):
     var toc = document.createElement("ul");

     // initialize a pointer that points to toc root:
     var pointer = toc;

     // will be appended to the node id to make sure it is unique on the page:
     var id_suffix = 0;

     // loop through the array of headlines
     myArrayOfHeadlineNodes.forEach(
       function(value, key, listObj) {

           // if we have detected a top level headline ...
           if ( "H1" == value.tagName ) {
               // ... reset the pointer to top level:
               pointer = toc;
           }

           // if we are at top level and we have detected a headline level 2 ...
           if ( "H2" == value.tagName && pointer == toc ) {
               // ... create a nested unordered list within the current list item:
               pointer = pointer.appendChild(document.createElement("ul"));
           }

           // if headline has no id, add a unique id
           if ("" == value.id) {
               value.id = "id934752938475" + ++id_suffix;
           }

           // for each headline, create a list item with the corresponding HTML content:
           var li = pointer.appendChild(document.createElement("li"));
           
       }
     );

     // debugging:
     console.log(toc.innerHTML);

     // update the content with the changed contentdiv, which contains IDs for every headline
     //   note that we need to use the sanitizer.bypassSecurityTrustHtml function in order to tell angular
     //   not to remove the IDs, when used as [innerHtml] attribute in the HTML template
     this.content = this.sanitizer.bypassSecurityTrustHtml(contentdiv.innerHTML);

     return(toc.innerHTML);
  }
}

@NgModule({
  declarations: [BlogView],
  imports: [
    ScrollToModule.forRoot(),
    RouterModule.forChild([
      { path: '', component: BlogView, pathMatch: 'full'}
    ])
  ]
})

export class BlogModule {

}

Next Steps + Improvement Potential

Do we have improvement potential? Lots of them:

  • Make the Code SSR compatible:
    We are running our code on basis of universal, a server-side rendering solution for Angular. However, we have made use of functions that are only defined in the browser and are not compatible with the node.js server. This leads to the fact that the table of contents is not visible in the server-provided HTML source code. I would like to improve that. This caveat is not relevant for pure client-side rendered solutions, which, I guess, will be the majority of the Angular projects.
  • Be more flexible on the headline levels displayed
    Today, we assume that H1 headlines are present and only H1 and H2 levels are displayed (fixed). A better solution should be more flexible. There might be situations, where H2 or H3 is the top-level headline and we do not want to rewrite your javascript/typescript code for those situations. For that we might also evaluate existing modules like this one from 2014.
  • Replace normal links by a scrollTo function
    nice to have feature

For achieving the first and the third topic, it might be helpful to place the table of contents in its own component with own html template
and use ng-repeat functionality similar to the answer of this StackOverflow Q&A. Why is this interesting? I have tried to make use of Nicky Lenaers ngx-scroll-to plugin, since I wanted the browser to scroll down instead of jumping to the headlines. However, the scroll-to plugin is ignored, if I just add it within the getToc function as a string:

By applying the sanitizer functions to the toc, I have succeeded that the ng-scroll-to function is not stripped, when the toc is applied as innerHTML. However, the ng-scroll-to just did not do anything. I guess, the situation changes, if the ng-scroll-to code is placed in a html template of a component. Even the SSR compatibility might be achieved more easily, if the toc is encapsulated in its own component, as I hope.

 

 

0

Angular 4: Automatic Table of Contents


In this step by step tutorial, we will go through the process of creating a two-level automatic Table of Contents by adding Angular Typescript/javascript code.

We will perform following steps:

  • We will discuss alternative solutions.
  • We will start an Angular Docker Container.
  • We will download a demo application with server-side rendering and WordPress REST API integration.
  • Finally, we will enrich the Web page with a two-level table of contents using javascript methods.

In this blog post, we will we will concentrate on HTML and javascript, i.e. we will not care about styles of the table of contents. Moreover, the links to the headlines will be added later in part 2 of this little series.

Goal

The target of this exercise is to scan the content of a page for occurrences of headlines of level 1 and 2 (i.e. h1 and h2 elements)…

<h1>What is Angular?</h1>
<h1>Angular Hello World via Quickstart</h1>
<h2>Step 1: Start CentOS Container</h2>

… and to generate an unordered list of the headlines, e.g.

  • What is Angular?
  • Angular Hello World via Quickstart
    • Step 1: Start CentOS Container

In HTML, this is a unordered nested list like follows:

<ul> 
    <li>
        What is Angular?
    </li>  
    <li>
        Angular Hello World via Quickstart
        <ul>  
            <li>
                Step 1: Start CentOS Container
            </li>
            <li>
                ...
            </li>
        </ul>
    </li>
</ul>

We will scan the document using the querySelectorAll("h1, h2") and we will create the unordered list with javascript functions like appendChild(document.createElement("ul")) and.appendChild(document.createElement("li"))

Step 0: Make a Decision: Integrating an existing Solution or Starting from Scratch?

I have been looking for a table of contents generator for angular. I have found following material:

I guess any of those possibilities are fit to do, what we need, but since I am new to Angular, I have decided to create a TOC from scratch. Firstly, I will get more familiar with the Angular code, and secondly, I will have full control on the result. If you are familiar on how to integrate existing modules or directives, the links above might be a good alternative for you.

Step 1: Start the Base Project in a Docker Container

You are still reading? That means you have decided to create the table of contents from scratch without the help of any of the offered table of contents modules. Okay, let us start.

I have chosen to apply the needed changes to my Universal Base Project I have created in my previous blog post Angular 4: Boosting Performance through Server Side Rendering. For that we start an Angular Docker image and download the Universal Code from GIT:

(dockerhost)$ mkdir toc; cd toc
(dockerhost)$ docker run -it -p 8001:8000 -v $(pwd):/localdir oveits/angular_hello_world:centos bash
(container)# git clone https://github.com/oveits/ng-universal-demo
(container)# cd ng-universal-demo

My version of the ng-universal-demo has added a blog page that is automatically created from the contents of a WordPress blog post by downloading the information from WordPress’ REST API.

Let us install the dependencies and start the server. The npm run watch command will make sure that the transpilation from typescript to javascript is automatically re-done, as soon as a file change is detected:

(container)# npm i
(container)# npm run watch &
(container)# npm run server

Note: if you need help with setting up a Docker host on a Windows system, you may want to check out Step 0 of this blog post (search for the term “Install a Docker Host”). There, we describe how to run an Ubuntu based Docker host inside a Virtualbox VM.

Step 2: Generate a private getToc Function

In this step, we create a private function that will return a table of contents from the string content in the argument. For that, we create a private function getToc within the BologView class we had created in my previous blog post:

src/app/+blog/blog.module.ts

export class BlogView implements OnInit {
  ...
  private getToc(content: any) {
     // add code here ...
  }
  ...
}

Step 2.1: Generate a DIV element

I have learned that Angular is using typescript and typescript is a super-set of javascript. So, why not starting with normal typescript code? I have tried following valid (I hope!) javascript code.

var div = document.createElement("div");

Even though it seemed to work, I have seen following error messages in my Universal project:

ERROR TypeError: this.html.charCodeAt is not a function
...
ERROR ReferenceError: document is not defined

Even though the TOC I had created with this command was visible in the browser, it did not show up in the HTML source. Therefore, I guess, the code is valid in the Browser, but it is not valid on the server, so server-side rendering does not take place. Therefore, I have replaced the line by:

 (as a screenshot, because WordPress gets confused by the embedded HTML content).

Note: that with angular.element, we see more serious errors than with document.createElement, as you will see below. Before long, we will revert back to the original code with document.createElement, in order to avoid major problems with server side rendering.

Step 2.2: Read in the Content to the DIV

In my case, the content has been read from a REST API as a string with HTML code inside:

content = "In this hello world style tutorial, we will follow a step by step guide..."

It is easy to read in the HTML code into the div:

div.innerHTML = content;

Step 2.3: Read the Headlines from the Content

Now, we would like to read all headlines level 1 and 2 from it. This can be done with the querySelectorAll function:

var myArrayOfNodes = [].slice.call(div.querySelectorAll("h1, h2"));

I have cast it in an array of DOM nodes for easier manipulation.

Step 2.4: Create the Table of Contents

Now we create the table of contents:

var toc = document.createElement("ul");

Step 2.5: For each Headline, create a List Item in the correct Level

There might more elegant solutions to solve this, but we can make sure that we are at the top level of list items by defining a target pointer that is reset to the top level if a level 1 headline is detected. If a level 2 headline is detected, and we are still at the top level, we will create a nested unordered list (UL) within the current list item (LI). Here, we repeat the commands of Step 2.1 to 2.4 in order to get the full picture:

private getToc(content: any) {
 // create div

 // read content into div:
 div.innerHTML = content;

 // create an array of headlines:
initialize table of contents (toc) and select all level 1 and 2 headers, reading them into an array:
 var myArrayOfNodes = [].slice.call(div.querySelectorAll("h1, h2"));

 // 
 var toc = document.createElement("ul");
 var pointer = toc;
 var myArrayOfNodes = [].slice.call(div.querySelectorAll("h1, h2"));

 // loop through the array of headlines
 myArrayOfNodes.forEach(
     function(value, key, listObj) {
     console.log(value.tagName + ": " + value.innerHTML);

     // if we have detected a top level headline:
     if ( "H1" == value.tagName ) {
         // reset the pointer to top level:
         pointer = toc;
     }
     
     // if we are at top level and we have detected a headline level 2
     if ( "H2" == value.tagName && pointer == toc ) {
         // create a nested unordered list
         pointer = pointer.appendChild(document.createElement("ul"));
     }
 
     // for each headline, create a list item with the corresponding HTML content:
     var li = target.appendChild(document.createElement("li"));
     li.innerHTML = value.innerHTML;
     }
  
     // for debugging:
     console.log(toc.innerHTML);
 }

 return(
     toc.innerHTML
 );
}

Note that we will replace the line  by the line var div = document.createElement("div"); soon, since it behaves better with server side rendering. See below.

Finally, we return the unordered nested list as a string by using the innerHTML function. We also could return the toc as DOM, but I have decided to return the innerHTML since this is the same format we get the title and the content from WordPress’ REST API.

Step 3: Assign the Table of Contents to a Class Variable

Now, since we have defined a function that can create a table of contents from any HTML content, we need to make use of it. Remember from the last blog, that we had read the title and content from a blog into public class variables. We now add a variable named “toc” and assign the result of getToc(content) to it. The changes are marked in blue.

export class BlogView implements OnInit {
 title: any = null;
 content: any = null;
 toc: any = null;

 constructor(private http: Http) {
 }

 ngOnInit(){
    this.getMyBlog();
 }

 private getMyBlog() {
     return 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;
             this.toc = this.getToc(data.content);
         console.log(data);
     });
 }

The only new line (in blue) it the one, where we write the table of contents into a public variable named toc.

Step 4: Place the Table of Contents in the HTML Template

Last but not least, we want to make the table of contents visible by adding it to the HTML template.

src/app/+blog/blog.module.html

Here, we have added the second line. We now can see, why we have returned the table of contents as  String: this way we are able to handle the toc variable as if it was just another element returned from the WordPress REST API, all of which are HTML content in string format.

Step 5: Check the Results

Finally, it is time to open a Browser, point it to localhost:8001 (since we have chosen port 8001 in step 1 above) and check the results:

We can see that the unordered list shows up between title and content the way expected.

Excellent! Thump up!

But does it play well with server-side rendering? Let us check:

No, it does not. The whole HTML content is missing.

😦

I could partially remedy the problem by changing back the line

which is causing an error “window is not defined”

by

var div = document.createElement("div");

which is causing the error “ERROR TypeError: this.html.charCodeAt is not a function”.

However, the latter error is better in the sense that title and content are shown as HTML source code again:

The table of contents still does not show up as HTML source code, but title and content are back. However, the table of contents is visible in the browser, which is more important than showing it in the source code:

And this is as good as we can get for today. We will accept the …

Caveat

Table of contents is not shown in the HTML source code.

Workaround: in getToc, analyze the input HTML content string without converting it to a DOM object and create the output table of contents using string functions only. However, this approach is error-prone and tedious, so I have decided to live with the error messages and the fact that the table of contents does not show up as source HTML code.

Summary

Based on an example with server-side rendering and content retrieved via the WordPress REST API, we have performed following steps:

  • We have shown how to create a private getToc function that will create a table of contents from the web page.
  • We have shown how to analyze the document.
  • We have created a nested two-level table of contents from the list of headlines of the document.

The generic javascript functions we have used do not play well with node.js that is used in case of server-side rendering. However, the table of contents shows up in the browser so the solution will be fit for pure client-side rendering. Moreover, we have suggested a workaround that even will work in a situation with server-side rendering: create the table of contents as an explicit string containing HTML code.

Note: The resulting code can be cloned via

git clone https://github.com/oveits/ng-universal-demo; cd ng-universal-demo
git checkout 8b3948a8 # to be sure to be at the same status of the repo as described in the blog

Next

See Part 2: Adding Links to the Table of Contents items

2

Angular 4 Universal: Boosting Performance through Server Side Rendering


This time we will show, how to use server side rendering with Angular 4 (or Angular 2). Like in my previous blog post, we will consume a RESTful Web Service with Angular 4. However, the web page will be displayed almost immediately because of server side rendering, as opposed to the client side rendered situation described in my previous blog post about Angular consuming a REST API. There, we had observed load times of several seconds in situations with low bandwidth between Angular server and REST service.

Why Server Side Rendering?

In our last blog post, Consuming a RESTful Web Service with Angular 4,  we have created an Angular simple single page application that has displayed data from an external resource, the WordPress API. In case of limited bandwidth between client and WordPress API, the latency for some responses were several seconds (!). Because of this fact, the perceived performance of the application was quite poor.

Angular Universal offers a possibility to mitigate the problem: it offers a combination of server side rendering and client side rendering. When the client contacts its server, the complete HTML page is downloaded to the client and the client can display the content immediately. Then the client will perform the REST call and will replace the server-side rendered page by a client-side rendered page. In case of a low performance link to the RESTful interface, this can be perceived by a flickering of the page. This is not perfect, but it is much better than waiting for the page display for seconds.

Now let us begin with our step by step guide.

Phase 1: Run a Server Side Rendered Hello World Page

Step 1.0: Get a Docker Host

This time again, our application will need more than 750 MB RAM. Therefore, we cannot use my beloved Katacoda as our Docker playground, which is limited to this amount of DRAM. Instead, you need to get access to a Docker host with, >~ 2 GB RAM. A nice way of installing an Ubuntu Docker host via Vagrant is described here (search for the term “Install a Docker Host”).

Step 1.1 Run my CentOS Angular Image

On the Docker host, let us start my angular image like follows (the -v $(pwd):/localdir option is only needed, if you want to keep the project folder for later use)

(dockerhost)$ docker run -it -p 8000:8000 -v $(pwd):/localdir oveits/angular_hello_world:centos bash

Then, on the container, we perform following commands:

(container)# git clone https://github.com/FrozenPandaz/ng-universal-demo
(container)# cd ng-universal-demo
(container)# npm i
(container)# npm run watch &
[1] 37

> ng-universal-demo@1.0.0 watch /ng-universal-demo
> webpack --watch


Webpack is watching the files…

Hash: 068db451109474765aa6ca44939d0968e157c122
Version: webpack 2.6.1
Child
    Hash: 068db451109474765aa6
    Time: 15786ms
              Asset       Size  Chunks                    Chunk Names
        0.client.js    2.13 kB       0  [emitted]
          client.js     2.5 MB       1  [emitted]  [big]  main
    0.client.js.map    1.04 kB       0  [emitted]
      client.js.map    3.03 MB       1  [emitted]         main
         index.html  222 bytes          [emitted]
       [0] ./~/rxjs/Observable.js 11.4 kB {1} [built]
       [3] ./~/rxjs/util/root.js 885 bytes {1} [built]
       [7] ./~/@angular/platform-browser/@angular/platform-browser.es5.js 141 kB {1} [built]
       [9] (webpack)/buildin/global.js 509 bytes {1} [built]
      [19] ./~/rxjs/add/operator/map.js 187 bytes {1} [built]
      [39] ./~/@angular/platform-browser-dynamic/@angular/platform-browser-dynamic.es5.js 5.88 kB {1} [built]
      [40] ./src/app/browser-app.module.ts 1.4 kB {1} [built]
      [41] ./~/reflect-metadata/Reflect.js 48 kB {1} [built]
      [42] ./~/zone.js/dist/zone.js 96 kB {1} [built]
      [44] ./~/@angular/compiler/@angular/compiler.es5.js 1.02 MB {1} [built]
      [45] ./src/app/app.module.ts 1.61 kB {1} [built]
      [47] ./src/main.browser.ts 350 bytes {1} [built]
      [49] ./src/modules/transfer-state/browser-transfer-state.module.ts 1.35 kB {1} [built]
      [50] ./~/process/browser.js 5.42 kB {1} [built]
      [78] ./~/rxjs/util/toSubscriber.js 760 bytes {1} [built]
        + 66 hidden modules

    ERROR in /ng-universal-demo/node_modules/rxjs/Subject.d.ts (16,22): Class 'Subject' incorrectly extends base class 'Observable'.
      Types of property 'lift' are incompatible.
        Type '(operator: Operator<T, R>) => Observable' is not assignable to type '(operator: Operator<T, R>) => Observable'.
          Type 'Observable' is not assignable to type 'Observable'.
            Type 'T' is not assignable to type 'R'.
    Child html-webpack-plugin for "index.html":
           [0] ./~/html-webpack-plugin/lib/loader.js!./src/index.html 193 bytes {0} [built]
Child
    Hash: ca44939d0968e157c122
    Time: 21298ms
              Asset     Size  Chunks                    Chunk Names
        0.server.js  2.14 kB       0  [emitted]
          server.js  4.23 MB       1  [emitted]  [big]  main
    0.server.js.map  1.04 kB       0  [emitted]
      server.js.map  5.18 MB       1  [emitted]         main
       [4] ./~/@angular/core/@angular/core.es5.js 489 kB {1} [built]
     [145] ./src/api/app.ts 222 bytes {1} [built]
     [146] ./src/app/server-app.module.ts 2.2 kB {1} [built]
     [147] ./src/routes.ts 80 bytes {1} [built]
     [148] ./~/@nguniversal/express-engine/index.js 196 bytes {1} [built]
     [149] ./~/express/index.js 224 bytes {1} [built]
     [150] ./~/reflect-metadata/Reflect.js 48 kB {1} [built]
     [151] ./~/rxjs/Rx.js 9.65 kB {1} [built]
     [152] ./~/zone.js/dist/zone-node.js 71.1 kB {1} [built]
     [158] ./src/main.server.ts 1.22 kB {1} [built]
     [245] ./~/rxjs/add/operator/bufferCount.js 235 bytes {1} [built]
     [336] ./~/rxjs/add/operator/windowTime.js 229 bytes {1} [built]
     [337] ./~/rxjs/add/operator/windowToggle.js 241 bytes {1} [built]
     [338] ./~/rxjs/add/operator/windowWhen.js 229 bytes {1} [built]
     [339] ./~/rxjs/add/operator/withLatestFrom.js 253 bytes {1} [built]
        + 475 hidden modules

    WARNING in ./~/express/lib/view.js
    80:29-41 Critical dependency: the request of a dependency is an expression

    ERROR in /ng-universal-demo/node_modules/rxjs/Subject.d.ts (16,22): Class 'Subject' incorrectly extends base class 'Observable'.
      Types of property 'lift' are incompatible.
        Type '(operator: Operator<T, R>) => Observable' is not assignable to type '(operator: Operator<T, R>) => Observable'.
          Type 'Observable' is not assignable to type 'Observable'.
            Type 'T' is not assignable to type 'R'.

    ERROR in /ng-universal-demo/node_modules/rxjs/observable/dom/WebSocketSubject.d.ts (24,22): Class 'WebSocketSubject' incorrectly extends base class 'AnonymousSubject'.
      Types of property 'lift' are incompatible.
        Type '(operator: Operator<T, R>) => WebSocketSubject' is not assignable to type '(operator: Operator<T, R>) => Observable'.
          Type 'WebSocketSubject' is not assignable to type 'Observable'.
            Types of property 'operator' are incompatible.
              Type 'Operator<any, R>' is not assignable to type 'Operator<any, T>'.
                Type 'R' is not assignable to type 'T'.

Let us ignore the error in red for now.

(container)# npm run server
[2] 60

> ng-universal-demo@1.0.0 server /ng-universal-demo
> nodemon dist/server.js

[nodemon] 1.11.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: dist/*server.js src/index.html
[nodemon] starting `node dist/server.js`
Listening at http://localhost:8000
GET: /: 187.390ms
GET: /data: 3.213ms

or if you want to avoid the npm run watch error described as issue on git:angular/angular, you either can fix the typescript version in package.js: "typescript": "2.3.4". Alternatively you can replace the previous two commands by npm run start (the npm run server command, which needs to be restarted often below, is quicker though):

(container)# npm run start
> ng-universal-demo@1.0.0 start /localdir/FrozenPandaz__ng-universal-demo
> npm run build && npm run server


> ng-universal-demo@1.0.0 prebuild /localdir/FrozenPandaz__ng-universal-demo
> npm run clean


> ng-universal-demo@1.0.0 clean /localdir/FrozenPandaz__ng-universal-demo
> rimraf dist


> ng-universal-demo@1.0.0 build /localdir/FrozenPandaz__ng-universal-demo
> webpack

Hash: a746c6e416ab32c2fe97cac872fdf2e493c7e402
Version: webpack 2.6.1
Child
    Hash: a746c6e416ab32c2fe97
    Time: 18765ms
              Asset       Size  Chunks                    Chunk Names
        0.client.js    2.07 kB       0  [emitted]
          client.js     2.5 MB       1  [emitted]  [big]  main
    0.client.js.map    1.07 kB       0  [emitted]
      client.js.map    3.03 MB       1  [emitted]         main
         index.html  222 bytes          [emitted]
       [0] ./~/rxjs/Observable.js 11.4 kB {1} [built]
       [3] ./~/rxjs/util/root.js 885 bytes {1} [built]
       [7] ./~/@angular/platform-browser/@angular/platform-browser.es5.js 141 kB {1} [built]
       [9] (webpack)/buildin/global.js 509 bytes {1} [built]
      [19] ./~/rxjs/add/operator/map.js 187 bytes {1} [built]
      [39] ./~/@angular/platform-browser-dynamic/@angular/platform-browser-dynamic.es5.js 5.88 kB {1} [built]
      [40] ./src/app/browser-app.module.ts 1.35 kB {1} [built]
      [41] ./~/reflect-metadata/Reflect.js 48 kB {1} [built]
      [42] ./~/zone.js/dist/zone.js 96 kB {1} [built]
      [44] ./~/@angular/compiler/@angular/compiler.es5.js 1.02 MB {1} [built]
      [45] ./src/app/app.module.ts 1.55 kB {1} [built]
      [47] ./src/main.browser.ts 350 bytes {1} [built]
      [49] ./src/modules/transfer-state/browser-transfer-state.module.ts 1.31 kB {1} [built]
      [50] ./~/process/browser.js 5.42 kB {1} [built]
      [78] ./~/rxjs/util/toSubscriber.js 760 bytes {1} [built]
        + 66 hidden modules
    Child html-webpack-plugin for "index.html":
           [0] ./~/html-webpack-plugin/lib/loader.js!./src/index.html 193 bytes {0} [built]
Child
    Hash: cac872fdf2e493c7e402
    Time: 24164ms
              Asset     Size  Chunks                    Chunk Names
        0.server.js  2.07 kB       0  [emitted]
          server.js  4.23 MB       1  [emitted]  [big]  main
    0.server.js.map  1.07 kB       0  [emitted]
      server.js.map  5.18 MB       1  [emitted]         main
       [4] ./~/@angular/core/@angular/core.es5.js 489 kB {1} [built]
     [145] ./src/api/app.ts 222 bytes {1} [built]
     [146] ./src/app/server-app.module.ts 2.11 kB {1} [built]
     [147] ./src/routes.ts 80 bytes {1} [built]
     [148] ./~/@nguniversal/express-engine/index.js 196 bytes {1} [built]
     [149] ./~/express/index.js 224 bytes {1} [built]
     [150] ./~/reflect-metadata/Reflect.js 48 kB {1} [built]
     [151] ./~/rxjs/Rx.js 9.65 kB {1} [built]
     [152] ./~/zone.js/dist/zone-node.js 71.1 kB {1} [built]
     [158] ./src/main.server.ts 1.22 kB {1} [built]
     [245] ./~/rxjs/add/operator/bufferCount.js 235 bytes {1} [built]
     [336] ./~/rxjs/add/operator/windowTime.js 229 bytes {1} [built]
     [337] ./~/rxjs/add/operator/windowToggle.js 241 bytes {1} [built]
     [338] ./~/rxjs/add/operator/windowWhen.js 229 bytes {1} [built]
     [339] ./~/rxjs/add/operator/withLatestFrom.js 253 bytes {1} [built]
        + 475 hidden modules

    WARNING in ./~/express/lib/view.js
    80:29-41 Critical dependency: the request of a dependency is an expression

> ng-universal-demo@1.0.0 server /localdir/FrozenPandaz__ng-universal-demo
> nodemon dist/server.js

[nodemon] 1.11.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: dist/*server.js src/index.html
[nodemon] starting `node dist/server.js`
Listening at http://localhost:8000

Now we connect to port 8000 of the Docker host, I have mapped the internal port 8000 to:

When looking at the source code, we can see the server-side rendered HTML Code:

The text “Universal Demo” and “Hello World” are visible in the source.

Excellent! Thump up!

This is exactly, what we wanted to see: a web page with the full HTML content, not just the “Loading…” directive that you usually see in Angular Index files.

Phase 2: Create a functional new Link in the main Page

Step 2.1 Create a new Link on the Home Page

With following little change in blue, we will add a new link to the Hello World page:

src/app/app.component.ts

import { Component, OnInit } from '@angular/core'
import { TransferState } from '../modules/transfer-state/transfer-state';

@Component({
  selector: 'demo-app',
  template: `
    <h1>Universal Demo</h1>
    <a routerLink="/">Home</a>
    <a routerLink="/lazy">Lazy</a>
    <a routerLink="/blog">Blog</a>
    <router-outlet></router-outlet>
  `,
  styles: [
    `h1 {
      color: green;
    }`
  ]
})
export class AppComponent implements OnInit {
  constructor(private cache: TransferState) {}
  ngOnInit() {
    this.cache.set('cached', true);
  }
}

From the steps above, npm run watch is still running in the background.

It seems that npm run server needs to be stopped are restarted manually. Since we have started it in the foreground above, a <Ctrl>-C and re-issuing the command is sufficient:

(container)# <Ctrl>-C
(container)# npm run server

After that, the change should be seen immediately in the browser (try pressing F5 to refresh, if this is not the case):

You will notice, though, that the Blog link is not yet functional. When you press F12, choose the “console” tab in the Browser and reload the page, we will see, what is missing:

ERROR Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'blog'

We still need to define the route.

Step 2.2: Create a Route from /blog to a Module

For creating a route, we need to add the /blog route to following file:

src/routes.ts

export const ROUTES: string[] = [
  '/',
  '/lazy',
  '/blog'
];

As we can see in the browser network debugging (F12), the error message does not change:

ERROR Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'blog'

To turn this around, we need a second change: we need to add a link from ‘blog’ to a module. For now, let us point the /blog link to the same module as the /lazy link:

src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { APP_BASE_HREF, CommonModule } from '@angular/common';
import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { HomeView } from './home/home-view.component';
import { TransferHttpModule } from '../modules/transfer-http/transfer-http.module';


@NgModule({
  imports: [
    CommonModule,
    HttpModule,
    TransferHttpModule,
    RouterModule.forRoot([
      { path: '', component: HomeView, pathMatch: 'full'},
      { path: 'lazy', loadChildren: './+lazy/lazy.module#LazyModule'},
      { path: 'blog', loadChildren: './+lazy/lazy.module#LazyModule'}
    ])
  ],
  declarations: [ AppComponent, HomeView ],
  exports: [ AppComponent ]
})
export class AppModule {}

Now the /blog link is functional and is pointing to the lazy module, showing “i’m lazy”, when clicking on the Blog link.

Now let us create our own module that is pointing to “i’m a blog”

Step 2.3: Create your own Blog Module

Above, we have re-used an existing “LazyModule”. Now, let us create our own module by copying and changing LazyModule:

mkdir src/app/+blog
cp src/app/+lazy/lazy.module.ts src/app/+blog/blog.module.ts

We replace ‘lazy’ by ‘blog’ and ‘Lazy’ by ‘Blog’ in place:

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

After that, the content of blog.module.ts looks like follows:

Now we need to change the route to point to the new BlogModule:

sed -r -i '/blog/s/lazy/blog/g; /blog/s/Lazy/Blog/' src/app/app.module.ts

after that, the file content looks like follows:

src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { APP_BASE_HREF, CommonModule } from '@angular/common';
import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { HomeView } from './home/home-view.component';
import { TransferHttpModule } from '../modules/transfer-http/transfer-http.module';


@NgModule({
  imports: [
    CommonModule,
    HttpModule,
    TransferHttpModule,
    RouterModule.forRoot([
      { path: '', component: HomeView, pathMatch: 'full'},
      { path: 'lazy', loadChildren: './+lazy/lazy.module#LazyModule'},
      { path: 'blog', loadChildren: './+blog/blog.module#BlogModule'}
    ])
  ],
  declarations: [ AppComponent, HomeView ],
  exports: [ AppComponent ]
})
export class AppModule {}

After restarting the server and reloading the Browser, the link to ‘Blog’ leads to following output:

In the source view, we also can confirm that this is a server-rendered page:

This is exactly, what we wanted to see: a web page with the full HTML content, not just the “Loading…” directive that you usually see in Angular Index files. With that, we can see, that this a server rendered page.

Excellent! Thump up!

Step 2.4 Add Includes to Browser, Server and Server AOT

To be honest, I am a newbie to Angular and I do not exactly know the function of following three includes. However, I have found them by searching recursively for occurrences of the term “lazy”. Those three includes seem to be needed, although the server side rendering seems to look fine without as well.

This one might be needed for client side rendering within webpack:

src/tsconfig.browser.json

{
  "extends": "../tsconfig.json",
  "angularCompilerOptions": {
    "entryModule": "./app/browser-app.module#BrowserAppModule"
  },
  "include": [
    "./main.browser.ts",
    "./app/+lazy/lazy.module.ts",
    "./app/+blog/blog.module.ts"
  ]
}

AOT stands for “Ahead of Time” and often refers to the compile time. Since we are not compiling, but we are “transpiling” in case of Angular, I guess, the next file will control the server-side pre-tranpiled pages:

src/tsconfig.server.aot.json

{
  "extends": "./tsconfig.server.json",
  "angularCompilerOptions": {
    "genDir": "ngfactory",
    "entryModule": "./app/server-app.module#ServerAppModule"
  },
  "include": [
    "main.server.aot.ts",
    "./app/+lazy/lazy.module.ts",
    "./app/+blog/blog.module.ts",
    "./app/server-app.module.ts"
  ]
}

The next one is a server configuration file for non AOT pages?

src/tsconfig.server.json

{
  "extends": "../tsconfig.json",
  "angularCompilerOptions": {
    "entryModule": "./app/server-app.module#ServerAppModule"
  },
  "include": [
    "main.server.ts",
    "./app/+lazy/lazy.module.ts",
    "./app/+blog/blog.module.ts"
  ]
}

Phase 3: Inserting a WordPress Blog POST via RESTful API

In this phase, we will insert a single WordPress Blog Post via a RESTful API of WordPress, as we had done in my previous blog post “Consuming a RESTful Web Service with Angular 4“. However, this time, the page will be served server-side rendered, which helps for a much better user experience (quicker load time). Especially, the performance is improved substantially, if the WordPress REST API is reachable via a low-bandwidth connection only.

Step 3.1: Change your Blog Module to perform HTTP Requests

In order to perform HTTP requests, we need to adapt the file src/app/+blog/blog.module.ts file, so it performs the same function as did the file src/app/app.component.ts in my previous blog post about Angular consuming HTTP:

  • Like last time, we had to import Http, Response, Headers as well as map as well as Observable. We add Oninit as well, this time.
  • I have replaced the inline template by a templateUrl file. This also helps me to display the content of my Blog Module correctly. However, this will lead to typescript errors as long as the template is not created. We will do this soon.
  • Different from last time, we explicitly have defined private variables title and content. The reason we are not using data.title and data.content in the HTML template is, that the data is null as long as we are waiting for the HTML response and a title and content of null does not exist. When debugging the browser, errors are visible. With the private variable title and content, we do not create such errors.
  • Like last time, we need to define a private variable (_http in our case)with type Http as argument of the constructor
  • Different from last time, we have introduced an OnInit function, which is calling the getMyBlog() function, instead of calling this function in the constructor. However, both possibilities work fine.
  • The getMyBlog() function looks similar to last time. The only difference is, that we set the title and content explicitly.

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.module.html'
})

export class BlogView implements OnInit {
  data: any = null;
  title: any = null;
  content: any = null;
  public subs: Observable<string>;

  constructor(private _http: Http) {
  }

  ngOnInit(){
    this.getMyBlog();
  }

  private getMyBlog() {
    return 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.data = data;
                        this.title = this.data.title;
                        this.content = this.data.content;
                        console.log(this.data);
                });
  }

}

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

}

The function getMyBlog() is retrieving the data of a blog post from the WP.COM Rest API v1.1, similar to what we also have done on the previous blog post Consuming a RESTful Web Service with Angular 4. This time we are writing the title and content in the corresponding public variables, which can be used in the HTML template.

Step 3.2 Create Blog HTML Template

If your npm run watch command is still active, you will notice following error in the console, where the command is running:

    ERROR in ./src/app/+blog/blog.module.ts
    Module not found: Error: Can't resolve './blog.module.html' in '/ng-universal-demo/src/app/+blog'
     @ ./src/app/+blog/blog.module.ts 38:18-47
     @ ./src lazy
     @ ./~/@angular/core/@angular/core.es5.js
     @ ./src/app/browser-app.module.ts
     @ ./src/main.browser.ts

The error will disappear, if we create the following file:

src/app/+blog/blog.module.html

The blog.module.ts file is pointing to a HTML templateUrl on ./blog.module.html with following content (as picture, since WordPress is confused about the HTML code, even if set in <pre>…</pre> tags):

Now the error is disappeared. If we restart the npm run server and the Browser content (e.g. press F5), we will see following content:

And different from last time, we not only see “Loading…” in the source of the page, but we will see the full HTML content:

To be honest, we see the content more often than we need: we see it as “innerHtml”, but we also see it explicitly as data within the tags. Large pages will have doubled size. Okay, seeing it twice is better than seeing it no time at all. Let us call it not perfect, but still …

Excellent! Thump up!

Summary

In this blog post, we have performed following tasks:

  • Phase 1: Run a Server Side Rendered Hello World Page
    • Here we could show that the server provides the browser with the full HTML content
  • Phase 2: Create a functional new Link in the main Page
    • Those steps are about Link creation and routing to a module
  • Phase 3: Inserting a WordPress Blog POST via RESTful API
    • In those steps we have shown how to create a component that retrieves content from a REST API and how to display the information in a browser

We could see, that the user is presented with the page content much more quickly than was the case in a client rendered solution of my previous blog. Especially, if there is a low bandwidth connection between Angular server and REST service, the user perception is improved a lot by server side rendering: the content is displayed almost immediately as opposed of the several second loading time we had experienced in case of client side rendering.

Appendix: Why using innerHtml?

Let us first demonstrate, what happens, if we use following template: The HTML template is making use of the variables “title” and “content” we have defined in the blog.module.ts before. We need to make use of this innerHtml trick in order to display the HTML-based content correctly. If we would use “{{content}}” instead, we will see escaped HTML.

This is not, what we want. If we change the template by following content:

or better (see this StackOverflow Q&A that states “Sanitized content can’t be bound using prop="{{sanitizedContent}}" because {{}} stringyfies the value before it is assigned which breaks sanitization.”; we do not yet sanitize here, but we might do so in the future):

With this, we will get, what we want:

Appendix: Webpack Problem

Error

Webpack has been initialised using a configuration object that does not match the API schema.

How to reproduce

docker run -it -p 8081:8000 -v $(pwd):/localdir oveits/angular_hello_world:centos bash
cd /localdir
git clone https://github.com/robwormald/ng-universal-demo
cd ng-universal-demo
npm i
$ npm start
> ng-universal-demo@1.0.0 start /localdir/ng-universal-demo
> npm run build && npm run server


> ng-universal-demo@1.0.0 build /localdir/ng-universal-demo
> webpack -p

Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
 - configuration.output.path: The provided value "dist" is not an absolute path!

Workaround

Clone the fork https://github.com/FrozenPandaz/ng-universal-demo instead.

Further Reading

0

Vagrant on CentOS 7 – Setting up Test Environments the easy Way


After stumbling upon several guides still describing a Vagrant installation via a RubyGem – which is no longer supported – the following article was created and will provide a quick setup guide on how to setup Vagrant on CentOS 7. All commands used in this guide are executed having root permissions.

Setting up VirtualBox as your Vagrant Provider

Since Vagrant is a utility to manage the lifecycle of virtual machines but doesn’t provide them it relies on providers. As described here Vagrant supports different providers by default such as  VirtualBoxHyper-V, and Docker. Due to the wide availability of VirtualBox it will be used in this guide.

In order to install VirtualBox we first need to add the VirtualBox repository:

cd /etc/yum.repos.d
yum install -y wget
wget http://download.virtualbox.org/virtualbox/rpm/rhel/virtualbox.repo

Because the installation of VirtualBox will require building kernel modules DKMS – Dynamic Kernel Module Support – from the EPEL repository will be installed first:

yum -y install epel-release
yum -y install dkms

In order to find the latest available VirtualBox version execute:

yum provides VirtualBox

and install via:

yum -y install VirtualBox-5.1-5.1.22_115126_el7-1.x86_64

Installing Vagrant

The latest Vagrant packages are available here. With a simple:

yum install https://releases.hashicorp.com/vagrant/1.9.6/vagrant_1.9.6_x86_64.rpm

the installation is completed in seconds.

Setting up your project environment

Vagrant projects are managed via Vagrantfiles. In order to start a new project create a dedicated folder and execute Vagrant init which will automatically create a new Vagrantfile:

mkdir Test_Project
cd Test_Project/
vagrant init

Now that we have our environment set up it is time to add a new base image. A variety of images – also called boxes – are available at https://app.vagrantup.com. We will add the latest Ubuntu Image to our boxes with:

vagrant box add ubuntu/xenial64

After the box was added locally we need to change the Vagrantfile which got created before. By changing the parameter config.vm.box from base to ubuntu/xenial64 Vagrant is configured to run the box we added previously.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"
end

Starting your  machine

Now that everything is set up the new machine is started with:

vagrant up

and is accessible by ssh executing:

vagrant ssh

The machine can be stopped with:

vagrant halt

Note: If you can not connect via ssh because you are asked for a password it might be related to: https://bugs.launchpad.net/cloud-images/+bug/1569237. You can switch to ubuntu/trusty64 by deleting your current Vagrantfile:

rm Vagrantfile

adding the ubuntu/trusty64 box:

vagrant box add ubuntu/trusty64

and create a new Vagrantfile containing a proper config.vm.box parameter:

vagrant init ubuntu/trusty64
1

Angular 4 Hello World Quickstart


In this hello world style tutorial, we will follow a step by step guide to a working Angular 4 application. We will also be able to demonstrate the two way binding feature, which will make sure that the browser reflects any changes to the model or the HTML code.

Note: while the latest quickstart example is using Angular 4 code, the tutorial works also for Angular 2.

What is Angular?

Angular (a.k.a. AngularJS) is a framework for dynamic web apps that is ideal for creating one page applications. A typical index.html file contains a custom HTML tag that is defined in an Angular component. The Angular component maps to a HTML template, which can contain variables in double curly brackets {{variable}}. The variables are defined within Angular components. Below, we will see a simple example and review the HTML index file and the template + model defined in a Angular component.

Angular Hello World via Quickstart

In this chapter, we will learn how to create an Angular Hello World application within a Docker CentoOS container. We will follow the fourth path shown on this article on courcetro. This is the only way I have found that will work on Katacoda (because of its low RAM consumption). The hands-on playground on Katacoda allows you to perform all steps in a console in your browser without the need to install any Docker host on your machine:

Click here to start your Katacoda course (requires free sign-up).

For the case you prefer to run a local test within a Docker container, follow the steps below.

Step 1: Start CentOS Container

We had problems with NPM on the latest Ubuntu, so we prefer to use a CentOS 7 container:

(dockerhost)$ docker run -it -p 3000:3000 centos:7 bash

Step 2: Install Git and NPM

Within the container, we need to install Git and NPM. One way of doing so is to install Git and NPM from the EPEL release (see here for other ways to install NPM):

(container)# yum install -y epel-release; yum install -y git npm

If you want to skip the lengthy installation process, you also can run a docker container from a Docker image, where npm and git is already installed:

docker run -it -p 3000:3000 oveits/centos_npm_git bash
(container)# npm -v
3.10.10

Step 3: Clone Quickstart Template

Now we clone the Quickstart Template for angular:

(container)# git clone https://github.com/angular/quickstart

Step 4: Install Dependencies

Now we install packages via NPM:

(container)# cd quickstart; npm i

Step 5: Start Service

Now we can start the service in the background like follows:

(container)# npm start &
npm WARN deprecated node-uuid@1.4.8: Use uuid module instead
npm WARN deprecated minimatch@0.3.0: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
angular-quickstart@1.0.0 /quickstart
+-- @angular/common@4.0.3
+-- @angular/compiler@4.0.3
+-- @angular/core@4.0.3
+-- @angular/forms@4.0.3
+-- @angular/http@4.0.3
+-- @angular/platform-browser@4.0.3
+-- @angular/platform-browser-dynamic@4.0.3
+-- @angular/router@4.0.3
+-- @types/jasmine@2.5.36
+-- @types/node@6.0.78
+-- angular-in-memory-web-api@0.3.2
+-- canonical-path@0.0.2
+-- concurrently@3.4.0
| +-- chalk@0.5.1
| | +-- ansi-styles@1.1.0
| | +-- escape-string-regexp@1.0.5
| | +-- has-ansi@0.1.0
| | | `-- ansi-regex@0.2.1
| | +-- strip-ansi@0.3.0
| | `-- supports-color@0.2.0
| +-- commander@2.6.0
| +-- date-fns@1.28.5
| +-- rx@2.3.24
| +-- spawn-command@0.0.2-1
| +-- supports-color@3.2.3
| | `-- has-flag@1.0.0
| `-- tree-kill@1.1.0
+-- core-js@2.4.1
+-- jasmine-core@2.4.1
+-- karma@1.7.0
| +-- bluebird@3.5.0
| +-- body-parser@1.17.2
| | +-- bytes@2.4.0
| | +-- content-type@1.0.2
| | +-- debug@2.6.7
| | | `-- ms@2.0.0
| | +-- depd@1.1.0
| | +-- http-errors@1.6.1
| | | +-- setprototypeof@1.0.3
| | | `-- statuses@1.3.1
| | +-- iconv-lite@0.4.15
| | +-- on-finished@2.3.0
| | | `-- ee-first@1.1.1
| | +-- qs@6.4.0
| | +-- raw-body@2.2.0
| | | `-- unpipe@1.0.0
| | `-- type-is@1.6.15
| |   +-- media-typer@0.3.0
| |   `-- mime-types@2.1.15
| |     `-- mime-db@1.27.0
| +-- chokidar@1.7.0
| | +-- anymatch@1.3.0
| | | `-- arrify@1.0.1
| | +-- async-each@1.0.1
| | +-- glob-parent@2.0.0
| | +-- inherits@2.0.3
| | +-- is-binary-path@1.0.1
| | | `-- binary-extensions@1.8.0
| | +-- is-glob@2.0.1
| | | `-- is-extglob@1.0.0
| | +-- path-is-absolute@1.0.1
| | `-- readdirp@2.1.0
| |   +-- readable-stream@2.2.11
| |   | +-- isarray@1.0.0
| |   | +-- process-nextick-args@1.0.7
| |   | +-- safe-buffer@5.0.1
| |   | `-- string_decoder@1.0.2
| |   |   `-- safe-buffer@5.0.1
| |   `-- set-immediate-shim@1.0.1
| +-- colors@1.1.2
| +-- combine-lists@1.0.1
| +-- connect@3.6.2
| | +-- finalhandler@1.0.3
| | | +-- encodeurl@1.0.1
| | | `-- escape-html@1.0.3
| | +-- parseurl@1.3.1
| | `-- utils-merge@1.0.0
| +-- di@0.0.1
| +-- dom-serialize@2.2.1
| | +-- custom-event@1.0.1
| | +-- ent@2.2.0
| | +-- extend@3.0.1
| | `-- void-elements@2.0.1
| +-- expand-braces@0.1.2
| | +-- array-slice@0.2.3
| | +-- array-unique@0.2.1
| | `-- braces@0.1.5
| |   `-- expand-range@0.1.1
| |     +-- is-number@0.1.1
| |     `-- repeat-string@0.2.2
| +-- glob@7.1.2
| | +-- fs.realpath@1.0.0
| | +-- inflight@1.0.6
| | | `-- wrappy@1.0.2
| | `-- once@1.4.0
| +-- graceful-fs@4.1.11
| +-- http-proxy@1.16.2
| | +-- eventemitter3@1.2.0
| | `-- requires-port@1.0.0
| +-- isbinaryfile@3.0.2
| +-- lodash@3.10.1
| +-- log4js@0.6.38
| | +-- readable-stream@1.0.34
| | | +-- core-util-is@1.0.2
| | | +-- isarray@0.0.1
| | | `-- string_decoder@0.10.31
| | `-- semver@4.3.6
| +-- mime@1.3.6
| +-- minimatch@3.0.4
| | `-- brace-expansion@1.1.8
| |   +-- balanced-match@1.0.0
| |   `-- concat-map@0.0.1
| +-- optimist@0.6.1
| | +-- minimist@0.0.10
| | `-- wordwrap@0.0.3
| +-- qjobs@1.1.5
| +-- range-parser@1.2.0
| +-- safe-buffer@5.1.0
| +-- socket.io@1.7.3
| | +-- debug@2.3.3
| | | `-- ms@0.7.2
| | +-- engine.io@1.8.3
| | | +-- accepts@1.3.3
| | | | `-- negotiator@0.6.1
| | | +-- base64id@1.0.0
| | | +-- cookie@0.3.1
| | | +-- debug@2.3.3
| | | | `-- ms@0.7.2
| | | `-- engine.io-parser@1.3.2
| | |   +-- after@0.8.2
| | |   +-- arraybuffer.slice@0.0.6
| | |   +-- base64-arraybuffer@0.1.5
| | |   +-- blob@0.0.4
| | |   `-- wtf-8@1.0.0
| | +-- has-binary@0.1.7
| | | `-- isarray@0.0.1
| | +-- object-assign@4.1.0
| | +-- socket.io-adapter@0.5.0
| | | `-- debug@2.3.3
| | |   `-- ms@0.7.2
| | +-- socket.io-client@1.7.3
| | | +-- backo2@1.0.2
| | | +-- component-bind@1.0.0
| | | +-- component-emitter@1.2.1
| | | +-- debug@2.3.3
| | | | `-- ms@0.7.2
| | | +-- engine.io-client@1.8.3
| | | | +-- component-emitter@1.2.1
| | | | +-- component-inherit@0.0.3
| | | | +-- debug@2.3.3
| | | | | `-- ms@0.7.2
| | | | +-- has-cors@1.1.0
| | | | +-- parsejson@0.0.3
| | | | +-- parseqs@0.0.5
| | | | +-- xmlhttprequest-ssl@1.5.3
| | | | `-- yeast@0.1.2
| | | +-- indexof@0.0.1
| | | +-- object-component@0.0.3
| | | +-- parseuri@0.0.5
| | | | `-- better-assert@1.0.2
| | | |   `-- callsite@1.0.0
| | | `-- to-array@0.1.4
| | `-- socket.io-parser@2.3.1
| |   +-- component-emitter@1.1.2
| |   +-- debug@2.2.0
| |   | `-- ms@0.7.1
| |   +-- isarray@0.0.1
| |   `-- json3@3.3.2
| +-- source-map@0.5.6
| +-- tmp@0.0.31
| | `-- os-tmpdir@1.0.2
| `-- useragent@2.1.13
|   `-- lru-cache@2.2.4
+-- karma-chrome-launcher@2.1.1
| +-- fs-access@1.0.1
| | `-- null-check@1.0.0
| `-- which@1.2.14
|   `-- isexe@2.0.0
+-- karma-cli@1.0.1
| `-- resolve@1.3.3
|   `-- path-parse@1.0.5
+-- karma-jasmine@1.1.0
+-- karma-jasmine-html-reporter@0.2.2
+-- lite-server@2.3.0
| +-- browser-sync@2.18.12
| | +-- browser-sync-client@2.5.1
| | | +-- etag@1.8.0
| | | `-- fresh@0.3.0
| | +-- browser-sync-ui@0.6.3
| | | +-- async-each-series@0.1.1
| | | +-- stream-throttle@0.1.3
| | | | `-- limiter@1.1.0
| | | `-- weinre@2.0.0-pre-I0Z7U9OV
| | |   +-- express@2.5.11
| | |   | +-- connect@1.9.2
| | |   | | `-- formidable@1.0.17
| | |   | +-- mime@1.2.4
| | |   | +-- mkdirp@0.3.0
| | |   | `-- qs@0.4.2
| | |   +-- nopt@3.0.6
| | |   | `-- abbrev@1.1.0
| | |   `-- underscore@1.7.0
| | +-- bs-recipes@1.3.4
| | +-- connect@3.5.0
| | | +-- debug@2.2.0
| | | | `-- ms@0.7.1
| | | `-- finalhandler@0.5.0
| | +-- dev-ip@1.0.1
| | +-- easy-extender@2.3.2
| | | `-- lodash@3.10.1
| | +-- eazy-logger@3.0.2
| | | `-- tfunk@3.1.0
| | |   +-- chalk@1.1.3
| | |   | +-- ansi-styles@2.2.1
| | |   | +-- has-ansi@2.0.0
| | |   | | `-- ansi-regex@2.1.1
| | |   | +-- strip-ansi@3.0.1
| | |   | `-- supports-color@2.0.0
| | |   `-- object-path@0.9.2
| | +-- emitter-steward@1.0.0
| | +-- fs-extra@3.0.1
| | | +-- jsonfile@3.0.0
| | | `-- universalify@0.1.0
| | +-- http-proxy@1.15.2
| | +-- immutable@3.8.1
| | +-- localtunnel@1.8.2
| | | +-- debug@2.2.0
| | | | `-- ms@0.7.1
| | | +-- openurl@1.1.0
| | | `-- yargs@3.29.0
| | |   +-- camelcase@1.2.1
| | |   `-- window-size@0.1.4
| | +-- micromatch@2.3.11
| | | +-- arr-diff@2.0.0
| | | | `-- arr-flatten@1.0.3
| | | +-- braces@1.8.5
| | | | +-- expand-range@1.8.2
| | | | | `-- fill-range@2.2.3
| | | | |   +-- is-number@2.1.0
| | | | |   +-- isobject@2.1.0
| | | | |   +-- randomatic@1.1.7
| | | | |   | +-- is-number@3.0.0
| | | | |   | | `-- kind-of@3.2.2
| | | | |   | `-- kind-of@4.0.0
| | | | |   `-- repeat-string@1.6.1
| | | | +-- preserve@0.2.0
| | | | `-- repeat-element@1.1.2
| | | +-- expand-brackets@0.1.5
| | | | `-- is-posix-bracket@0.1.1
| | | +-- extglob@0.3.2
| | | +-- filename-regex@2.0.1
| | | +-- kind-of@3.2.2
| | | | `-- is-buffer@1.1.5
| | | +-- normalize-path@2.1.1
| | | | `-- remove-trailing-separator@1.0.2
| | | +-- object.omit@2.0.1
| | | | +-- for-own@0.1.5
| | | | | `-- for-in@1.0.2
| | | | `-- is-extendable@0.1.1
| | | +-- parse-glob@3.0.4
| | | | +-- glob-base@0.3.0
| | | | `-- is-dotfile@1.0.3
| | | `-- regex-cache@0.4.3
| | |   +-- is-equal-shallow@0.1.3
| | |   `-- is-primitive@2.0.0
| | +-- opn@4.0.2
| | | `-- pinkie-promise@2.0.1
| | |   `-- pinkie@2.0.4
| | +-- portscanner@2.1.1
| | | +-- async@1.5.2
| | | `-- is-number-like@1.0.7
| | |   +-- bubleify@0.5.1
| | |   | `-- buble@0.12.5
| | |   |   +-- acorn@3.3.0
| | |   |   +-- acorn-jsx@3.0.1
| | |   |   +-- acorn-object-spread@1.0.0
| | |   |   +-- chalk@1.1.3
| | |   |   | +-- ansi-styles@2.2.1
| | |   |   | +-- has-ansi@2.0.0
| | |   |   | | `-- ansi-regex@2.1.1
| | |   |   | +-- strip-ansi@3.0.1
| | |   |   | `-- supports-color@2.0.0
| | |   |   +-- magic-string@0.14.0
| | |   |   | `-- vlq@0.2.2
| | |   |   +-- minimist@1.2.0
| | |   |   `-- os-homedir@1.0.2
| | |   `-- lodash.isfinite@3.3.2
| | +-- qs@6.2.1
| | +-- resp-modifier@6.0.2
| | +-- rx@4.1.0
| | +-- serve-index@1.8.0
| | | +-- batch@0.5.3
| | | +-- debug@2.2.0
| | | | `-- ms@0.7.1
| | | `-- http-errors@1.5.1
| | |   `-- setprototypeof@1.0.2
| | +-- serve-static@1.12.2
| | | `-- send@0.15.2
| | |   +-- debug@2.6.4
| | |   | `-- ms@0.7.3
| | |   +-- destroy@1.0.4
| | |   +-- fresh@0.5.0
| | |   +-- mime@1.3.4
| | |   `-- ms@1.0.0
| | +-- server-destroy@1.0.1
| | +-- socket.io@1.6.0
| | | +-- debug@2.3.3
| | | | `-- ms@0.7.2
| | | `-- engine.io@1.8.0
| | |   +-- base64id@0.1.0
| | |   +-- debug@2.3.3
| | |   | `-- ms@0.7.2
| | |   +-- engine.io-parser@1.3.1
| | |   | +-- after@0.8.1
| | |   | `-- has-binary@0.1.6
| | |   |   `-- isarray@0.0.1
| | |   `-- ws@1.1.1
| | +-- socket.io-client@1.6.0
| | | +-- component-emitter@1.2.1
| | | +-- debug@2.3.3
| | | | `-- ms@0.7.2
| | | `-- engine.io-client@1.8.0
| | |   `-- debug@2.3.3
| | |     `-- ms@0.7.2
| | +-- ua-parser-js@0.7.12
| | `-- yargs@6.4.0
| |   +-- camelcase@3.0.0
| |   +-- cliui@3.2.0
| |   | +-- strip-ansi@3.0.1
| |   | | `-- ansi-regex@2.1.1
| |   | `-- wrap-ansi@2.1.0
| |   |   `-- strip-ansi@3.0.1
| |   |     `-- ansi-regex@2.1.1
| |   +-- decamelize@1.2.0
| |   +-- get-caller-file@1.0.2
| |   +-- os-locale@1.4.0
| |   | `-- lcid@1.0.0
| |   |   `-- invert-kv@1.0.0
| |   +-- read-pkg-up@1.0.1
| |   | +-- find-up@1.1.2
| |   | | `-- path-exists@2.1.0
| |   | `-- read-pkg@1.1.0
| |   |   +-- load-json-file@1.1.0
| |   |   | +-- parse-json@2.2.0
| |   |   | | `-- error-ex@1.3.1
| |   |   | |   `-- is-arrayish@0.2.1
| |   |   | `-- strip-bom@2.0.0
| |   |   |   `-- is-utf8@0.2.1
| |   |   +-- normalize-package-data@2.3.8
| |   |   | +-- hosted-git-info@2.4.2
| |   |   | +-- is-builtin-module@1.0.0
| |   |   | | `-- builtin-modules@1.1.1
| |   |   | `-- validate-npm-package-license@3.0.1
| |   |   |   +-- spdx-correct@1.0.2
| |   |   |   | `-- spdx-license-ids@1.2.2
| |   |   |   `-- spdx-expression-parse@1.0.4
| |   |   `-- path-type@1.1.0
| |   +-- require-directory@2.1.1
| |   +-- require-main-filename@1.0.1
| |   +-- set-blocking@2.0.0
| |   +-- string-width@1.0.2
| |   | +-- code-point-at@1.1.0
| |   | +-- is-fullwidth-code-point@1.0.0
| |   | | `-- number-is-nan@1.0.1
| |   | `-- strip-ansi@3.0.1
| |   |   `-- ansi-regex@2.1.1
| |   +-- which-module@1.0.0
| |   +-- window-size@0.2.0
| |   +-- y18n@3.2.1
| |   `-- yargs-parser@4.2.1
| |     `-- camelcase@3.0.0
| +-- connect-history-api-fallback@1.3.0
| +-- connect-logger@0.0.1
| | `-- moment@2.18.1
| `-- minimist@1.2.0
+-- lodash@4.17.4
+-- protractor@4.0.14
| +-- @types/q@0.0.32
| +-- @types/selenium-webdriver@2.53.37
| +-- adm-zip@0.4.7
| +-- chalk@1.1.3
| | +-- ansi-styles@2.2.1
| | +-- has-ansi@2.0.0
| | | `-- ansi-regex@2.1.1
| | +-- strip-ansi@3.0.1
| | `-- supports-color@2.0.0
| +-- jasmine@2.4.1
| | +-- exit@0.1.2
| | `-- glob@3.2.11
| |   `-- minimatch@0.3.0
| |     `-- sigmund@1.0.1
| +-- jasminewd2@0.0.10
| +-- q@1.4.1
| +-- saucelabs@1.3.0
| | `-- https-proxy-agent@1.0.0
| |   `-- agent-base@2.1.1
| |     `-- semver@5.0.3
| +-- selenium-webdriver@2.53.3
| | +-- adm-zip@0.4.4
| | +-- tmp@0.0.24
| | +-- ws@1.1.2
| | | +-- options@0.0.6
| | | `-- ultron@1.0.2
| | `-- xml2js@0.4.4
| |   +-- sax@0.6.1
| |   `-- xmlbuilder@9.0.0
| +-- source-map-support@0.4.15
| `-- webdriver-manager@10.3.0
|   +-- del@2.2.2
|   | +-- globby@5.0.0
|   | | `-- array-union@1.0.2
|   | |   `-- array-uniq@1.0.3
|   | +-- is-path-cwd@1.0.0
|   | +-- is-path-in-cwd@1.0.0
|   | | `-- is-path-inside@1.0.0
|   | |   `-- path-is-inside@1.0.2
|   | `-- pify@2.3.0
|   +-- ini@1.3.4
|   +-- minimist@1.2.0
|   +-- request@2.78.0
|   | +-- aws-sign2@0.6.0
|   | +-- aws4@1.6.0
|   | +-- caseless@0.11.0
|   | +-- combined-stream@1.0.5
|   | | `-- delayed-stream@1.0.0
|   | +-- forever-agent@0.6.1
|   | +-- form-data@2.1.4
|   | | `-- asynckit@0.4.0
|   | +-- har-validator@2.0.6
|   | | +-- chalk@1.1.3
|   | | | +-- ansi-styles@2.2.1
|   | | | +-- has-ansi@2.0.0
|   | | | | `-- ansi-regex@2.1.1
|   | | | +-- strip-ansi@3.0.1
|   | | | `-- supports-color@2.0.0
|   | | +-- commander@2.9.0
|   | | | `-- graceful-readlink@1.0.1
|   | | `-- is-my-json-valid@2.16.0
|   | |   +-- generate-function@2.0.0
|   | |   +-- generate-object-property@1.2.0
|   | |   | `-- is-property@1.0.2
|   | |   +-- jsonpointer@4.0.1
|   | |   `-- xtend@4.0.1
|   | +-- hawk@3.1.3
|   | | +-- boom@2.10.1
|   | | +-- cryptiles@2.0.5
|   | | +-- hoek@2.16.3
|   | | `-- sntp@1.0.9
|   | +-- http-signature@1.1.1
|   | | +-- assert-plus@0.2.0
|   | | +-- jsprim@1.4.0
|   | | | +-- assert-plus@1.0.0
|   | | | +-- extsprintf@1.0.2
|   | | | +-- json-schema@0.2.3
|   | | | `-- verror@1.3.6
|   | | `-- sshpk@1.13.1
|   | |   +-- asn1@0.2.3
|   | |   +-- assert-plus@1.0.0
|   | |   +-- bcrypt-pbkdf@1.0.1
|   | |   +-- dashdash@1.14.1
|   | |   | `-- assert-plus@1.0.0
|   | |   +-- ecc-jsbn@0.1.1
|   | |   +-- getpass@0.1.7
|   | |   | `-- assert-plus@1.0.0
|   | |   +-- jsbn@0.1.1
|   | |   `-- tweetnacl@0.14.5
|   | +-- is-typedarray@1.0.0
|   | +-- isstream@0.1.2
|   | +-- json-stringify-safe@5.0.1
|   | +-- node-uuid@1.4.8
|   | +-- oauth-sign@0.8.2
|   | +-- qs@6.3.2
|   | +-- stringstream@0.0.5
|   | +-- tough-cookie@2.3.2
|   | | `-- punycode@1.4.1
|   | `-- tunnel-agent@0.4.3
|   `-- semver@5.3.0
+-- rimraf@2.6.1
+-- rxjs@5.0.1
| `-- symbol-observable@1.0.4
+-- systemjs@0.19.40
| `-- when@3.7.8
+-- tslint@3.15.1
| +-- diff@2.2.3
| +-- findup-sync@0.3.0
| | `-- glob@5.0.15
| `-- underscore.string@3.3.4
|   +-- sprintf-js@1.1.1
|   `-- util-deprecate@1.0.2
+-- typescript@2.1.6
`-- zone.js@0.8.12

npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@^1.0.0 (node_modules/chokidar/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.1.2: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

The warnings can be ignored for the purpose of this tutorial.

Step 6: Load Application

You can load the angular application on the browser on http://localhost:3000

Note: if your Docker host is running on VirtualBox, you might need to map the port 3000 from your local machine to the VirtualBox VM:

Step 7 (optional): Review Angular Files

src/index.html

The index.html is the file that is loaded by the browser.

<!DOCTYPE html>
<html>
  <head>
    <title>Angular QuickStart</title>
    <base href="/">
    ...(lines omitted)
    
      System.import('main.js').catch(function(err){ console.error(err); });
    
  </head>

  <body>
    <my-app>Loading AppComponent content here ...</my-app>
  </body>
</html>

Some javascript files are loaded, and a custom tag named my-app is used in the body. The my-app tag is defined in the following component:

src/app/app.component.ts

Angular’s most important

(container)# cat src/app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `<h1>Hello {{name}}</h1>`,
})
export class AppComponent  { name = 'World'; }

The component defines the HTML tag ‘my-app’, tells Angular to load the text Hello {{name}} as level 1 heading. The variable in double-curly brackets is replaced by its definition in the AppComponent.

Step 8: Change Component

With following example, we can see that a change in the component file is detected immediately and the Appliction is changed accordingly:

(container)# sed -r -i 's/Angular/World/g' src/app/app.component.ts
[root@471dd514dbef quickstart]# [0] 4:07:24 PM - File change detected. Starting incremental compilation...
[1] [BS] Reloading Browsers...
[0] 4:07:24 PM - Compilation complete. Watching for file changes.
[1] 17.06.13 16:07:25 304 GET /index.html
[1] 17.06.13 16:07:25 304 GET /styles.css
[1] 17.06.13 16:07:25 304 GET /core-js/client/shim.min.js
[1] 17.06.13 16:07:25 304 GET /zone.js/dist/zone.js
[1] 17.06.13 16:07:25 304 GET /systemjs/dist/system.src.js
[1] 17.06.13 16:07:25 304 GET /systemjs.config.js
[1] 17.06.13 16:07:25 304 GET /main.js
[1] 17.06.13 16:07:25 304 GET /@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js
[1] 17.06.13 16:07:25 304 GET /systemjs-angular-loader.js
[1] 17.06.13 16:07:25 304 GET /@angular/compiler/bundles/compiler.umd.js
[1] 17.06.13 16:07:25 304 GET /@angular/core/bundles/core.umd.js
[1] 17.06.13 16:07:25 304 GET /@angular/common/bundles/common.umd.js
[1] 17.06.13 16:07:25 304 GET /@angular/platform-browser/bundles/platform-browser.umd.js
[1] 17.06.13 16:07:25 304 GET /app/app.module.js
[1] 17.06.13 16:07:25 304 GET /rxjs/Observable.js
[1] 17.06.13 16:07:25 304 GET /rxjs/observable/merge.js
[1] 17.06.13 16:07:25 304 GET /rxjs/operator/share.js
[1] 17.06.13 16:07:25 304 GET /rxjs/Subject.js
[1] 17.06.13 16:07:25 200 GET /app/app.component.js
[1] 17.06.13 16:07:25 304 GET /rxjs/util/root.js
[1] 17.06.13 16:07:25 304 GET /rxjs/util/toSubscriber.js
[1] 17.06.13 16:07:25 304 GET /rxjs/symbol/observable.js
[1] 17.06.13 16:07:25 304 GET /rxjs/operator/merge.js
[1] 17.06.13 16:07:25 304 GET /rxjs/operator/multicast.js
[1] 17.06.13 16:07:25 304 GET /rxjs/Subscriber.js
[1] 17.06.13 16:07:25 304 GET /rxjs/Subscription.js
[1] 17.06.13 16:07:25 304 GET /rxjs/util/ObjectUnsubscribedError.js
[1] 17.06.13 16:07:25 304 GET /rxjs/SubjectSubscription.js
[1] 17.06.13 16:07:25 304 GET /rxjs/symbol/rxSubscriber.js
[1] 17.06.13 16:07:25 304 GET /rxjs/Observer.js
[1] 17.06.13 16:07:25 304 GET /rxjs/observable/ArrayObservable.js
[1] 17.06.13 16:07:25 304 GET /rxjs/operator/mergeAll.js
[1] 17.06.13 16:07:25 304 GET /rxjs/util/isScheduler.js
[1] 17.06.13 16:07:25 304 GET /rxjs/observable/ConnectableObservable.js
[1] 17.06.13 16:07:25 304 GET /rxjs/util/isFunction.js
[1] 17.06.13 16:07:25 304 GET /rxjs/util/isArray.js
[1] 17.06.13 16:07:25 304 GET /rxjs/util/isObject.js
[1] 17.06.13 16:07:25 304 GET /rxjs/util/tryCatch.js
[1] 17.06.13 16:07:25 304 GET /rxjs/util/errorObject.js
[1] 17.06.13 16:07:25 304 GET /rxjs/util/UnsubscriptionError.js
[1] 17.06.13 16:07:26 304 GET /rxjs/observable/ScalarObservable.js
[1] 17.06.13 16:07:26 304 GET /rxjs/observable/EmptyObservable.js
[1] 17.06.13 16:07:26 304 GET /rxjs/OuterSubscriber.js
[1] 17.06.13 16:07:26 304 GET /rxjs/util/subscribeToResult.js
[1] 17.06.13 16:07:26 304 GET /rxjs/util/isPromise.js
[1] 17.06.13 16:07:26 304 GET /rxjs/symbol/iterator.js
[1] 17.06.13 16:07:26 304 GET /rxjs/InnerSubscriber.js

In the browser, “Hello Angular” is automatically exchanged by “Hello World”. There is no need to reload the browser page manually:

To toggle back and to ‘Hello Angular’, you can issue the command

(container)# sed -r -i 's/World/Angular/g' src/app/app.component.ts

Step 9: Change index file

In this step, we will demonstrate that also a change in the index file is detected automatically. For that, you either can add some HTML code before the <my-app> tag. Alternatively, the following command will do that for you:

(container)# sed -r -i 's/<my-app/<h1>TEST<\/h1><my-app/g' src/index.html

After the change is performed, the browser will detect the change and display:

Excellent! Thump up!

Summary

We have learned how to create and deploy a simple Angular Hello World app in a Docker container by downloading an quickstart example from git. After installing the NPM packet manager, NPM has helped us to resolve all dependencies and start the service.

We could demonstrate the Angular two way binding: a change in the component’s model or a change in the HTML code has triggered an automatic reload of the page.

References