Angular 7.x with webpack 4.20.2 Tutorial

2018-11-07

Overview

Updated 2018-11-07 for Angular 7.x Previous versions: 2018-10-12 for Angular 6.1.0 and webpack 4.20.2 (original post on 2016-06-01, then updated on 2017-03-14 for anglar 4.0.0.rc.3 and webpack 2.x and on 2017-12-13 for Angular 5.1.0 with Webpack 3.x)

We will create a simple angular application. It will be a simple "hello world"-style application so you can focus on the infrastructure and the absolute basics of the angular framework.

We will be using the result from the blog article on webpack. That result is also available from github

Starting point

If you have not completed the webpack then go ahead and clone the solution of that tutorial:

$ git clone https://github.com/edc4it/webpack-intro-tutorial hellow
$ cd hellow

Next let's install the necessary libraries using yarn.

$ npm install

This puts you right at the end of the aforementioned blog article.

Then remove all the contents** of the src folder as we will be building a new application from scratch.

Configure the project

Install dependencies

Let's first install the required angular libraries:

$ npm install @angular/core@7.0.0 \
         @angular/common@7.0.0 \
         @angular/compiler@7.0.0 \
         @angular/platform-browser@7.0.0 \
         @angular/platform-browser-dynamic@7.0.0 \
         @angular/http@7.0.0 \
         @angular/router@7.0.0 \
         @angular/forms@7.0.0 \
         rxjs@6.3.3

Angular also requires a couple of shims in order to support older browsers, install those and we make sure and we create a separate chunk for them afterwards.

$ npm install core-js@2.5.4 \
           zone.js@0.8.26

Polyfills

Create a new file named src/polyfills.ts and add dependencies to our shims:

import "core-js/es7/reflect";
import "zone.js/dist/zone";

Let's then add an webpack entry (polyfills) for our webpack configuration (above the main entry that is already there.):

entry: {
    polyfills: "./src/polyfills.ts", // add this
    main: "./src/app.ts" //change this
},

Notice we also changed the name of the main entry to src/app.ts and we have *removed the vendor.

Our HtmlWebpackPlugin configuration from the previous post will include this on our page (as a first entry)

Optionally you can add the following: At the time of this writing we will also need to use the ContextReplacementPlugin in order to suppress a warnings concerning the code of @angular/core/fesm5/core.js

plugins: [
    …,
    new webpack.ContextReplacementPlugin(
        /\@angular(\\|\/)core(\\|\/)fesm5/,
        path.resolve(__dirname, 'src'),{}
    )
]    

There will still be a few warnings about the use of the deprecated System.import(). You could hide those using webpack-filter-warnings-plugin, however we will just ignore these.

TypeScript configuration

With angular we use decorators (they provide component metadata). By default this is not enabled. We therefore need to adjust the tsconfig.json together with some other configuration:

{
  "compilerOptions": {
    "module": "es2015",
    "target": "es5",
    "moduleResolution": "node",

    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "lib": [ "es2015", "dom" ],
    "typeRoots": ["node_modules/@types"],
    "noImplicitAny": true,
    "sourceMap": true,
    "suppressImplicitAnyIndexErrors": true
  }
}

The code we'll write requires a more lenient tslint, so we will disable some rules (explicit accessor modifiers and the required extra line at the end of a file plus the alphabetic ordering of members in object literals):

"rules": {
  "member-access": false,
  "eofline": false,
  "object-literal-sort-keys":  false
},

Webpack dev configuration

While we are here let's add two more pieces of configuration (you can add this to the bottom of the webpack configuration)

devtool: "source-map",
devServer: {
    historyApiFallback: true
}

The first option instructs webpack to generate source-maps, the second option is required when using the webpack-dev-server together with angular routing.

We will need to add some more configuration later, but we'll do that in context.

Prepare the html

We need to place a component element to our html file so that angular can replace our component on the page (something like a placeholder). We will use a custom element ( we could use any CSS3 selector to target the placement in the DOM).

Create a new file named src/index.html with the following contents:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Angular Hellow</title>
</head>
<body>
<hellow-app>loading...</hellow-app>
</body>
</html>

The angular component we'll write later on is going to replace the contents of the hellow-app element. Notice we have placed some text inside it ("loading..."), this will be shown during application initialisation.

We don't need to include any javascript files for angular, polyfills or our application code, this is handled by webpack and in particular the HtmlWebpackPlugin. This means we are all done with this step!

The Component

Create a new file named src/app.ts. In it declare an HelloComponent class within:

class HelloComponent {
}

Next lets add the metadata using the @Component decorator from "@angular/core" (don't forget the import if your IDE does not do it for you). Add a selector (pointing to the hellow-app element in our index.html, and the template text for this component:

import {Component} from "@angular/core";

@Component({
  selector: "hellow-app",
  template: "<h1>Hello Friend</h1>",
})
class HelloComponent {
}

Angular applications are structured using modules (not to be confused with webpack modules). A module consists of various related angular resources (components, services etc). Angular modules can dependent on each other. In our case we need to "import" the built-in BrowserModule. We will also need to register our component and indicate it is used to bootstrap our application. We then finally need to register the module to be used as the bootstrap module (using platformBrowserDynamic) You'll need at least one such module. Lets go ahead and declare an "application"-module inside the same app.ts file"

import {Component, NgModule} from "@angular/core";
import {BrowserModule} from "@angular/platform-browser";
import {platformBrowserDynamic} from "@angular/platform-browser-dynamic";
…
// Your HelloComponent@NgModule({
    imports:      [BrowserModule ], // import Angular's BrowserModule
    bootstrap:    [HelloComponent],  // indicate the bootstrap component
    declarations: [HelloComponent] // register our component with the module
})
export class AppModule {}

platformBrowserDynamic().bootstrapModule(AppModule); // bootstrap with our module

Start the webpack-dev-server:

$ npx webpack-dev-server

Open http://localhost:8080 and Bang! it shows "Hello World"!

Restructure

Great that we got it all working so fast in a single file, but we really should reorganise the files and resources better:

  • Place components inside their own files (and directories)
  • Place template html (and later styles) inside their own files
  • Place the module in its own file

Eventually the structure needs to resemble:

src ├── app │   ├── app.component.html │   ├── app.component.scss │   ├── app.component.ts │   └── app.module.ts ├── index.html ├── app.ts ├── polyfills.ts

Separate Module from Component

Create the src/app/app.component.ts and cut/paste the component from app.ts. Make sure you export as this needs to now be accessed by another module:

import {Component} from "@angular/core";

@Component({
    selector: "hellow-app",
    template: "<h1>Hello Friend</h1>",
})
export class HelloComponent {
}

And let's move the AppModule declaration to a file named src/app/app.module.ts (again. don't forget the imports and export):

import {NgModule} from "@angular/core";
import {BrowserModule} from "@angular/platform-browser";
import {HelloComponent} from "./app.component";

@NgModule({
    imports:      [BrowserModule ], // import Angular's BrowserModule
    bootstrap:    [HelloComponent],  // indicate the bootstrap component
    declarations: [HelloComponent], // register our component with the module
})
export class AppModule {}

Fix the reference inside the app.ts by using an import:

import {platformBrowserDynamic} from "@angular/platform-browser-dynamic";
import {AppModule} from "./app/app.module";

platformBrowserDynamic().bootstrapModule(AppModule); // bootstrap with our module

Your browser should again be showing "Hello Friend"

Move the html to a file

Let's now remove the html template from the component decorator and place it in its own file src/app/app.component.html:

<h1>Hello Friend</h1>

Update the metadata and use node's require to obtain the html text:

@Component({
  selector: "hellow-app",
  template: require("./app.component.html"),
})

note: Without webpack you would have used the templateUrl metadata property and point to the html file using a string url: app/app.component.html. But as we are using webpack we are really just loading the html as a string using node's require function.

But hold on: we are declaring a dependency to a "html-module". Recall from the previous article, that webpack only knows how to handle javascript modules. We therefore already added some module rules. We now need to add one for html modules

First install the loader:

$ npm install add html-loader@0.5.5 --save-dev

Then configure the rule:

{
    test: /\.html$/,
    loader: "html-loader"
},

Add styling

Add a scss file named src/app/app.component.scss with the following contents

$color: #0086b3;
h1 {
  font-size: 1.2em;
  color: $color;
}

note: The $color variable should ideally come from a global .scss file which you would then import using @import "../main";. We are using it here just to use a specific non-css scss feature.

Update the metadata to include a styles property and use again node's require:

@Component({
  selector: "hellow-app",
  styles: [ require("./app.component.scss") ],
  template: require("./app.component.html"),
})

But wait a minute, hold on again! How will webpack process this? We have an existing rule for scss. But in this case we don't want to have the style on the page. We just want it to be made available to the styles metadata property above. Change the existing rule and only use the sass-loader followed by a raw-loader (which we need to install):

{
    test: /\.scss$/,
    loader: ["raw-loader", "sass-loader?sourceMap"]
}

Don't forget to install the loader:

$ npm install add raw-loader@0.5.1 --save-dev

Restart your dev server and your browser should now be showing a styled "Hello World" ;)

One-way binding

In this step you will use interpolation using an expression which will be initialised to a static value. (Later we will use two-way binding).

Update our component's template (app/app.component.html)

<h1>{{message}}</h1>

Notice your browser, you will notice the page no longer display any text. You are already seeing angular in action. The reason you don't see any text is because the property message value has no value (if you wanted you could use '{{!message}}and the paye would displaytrue`). Let's add a property and initialise it with a value inside our component:

export class HelloComponent {
  message = "Hoo Ah!";
}

You should now see your message in the browser (and shout it out like Lt. Col. Frank Slade when you see it in your browser)

Two-way binding

You will now bind the message model using two-way data binding.

Add an input element and use the ngModel applying the two-way binding style: [(...)].

<h1>{{message}}</h1> <input type="text" [(ngModel)]="message"/>

You will need to import the FormsModule into your module:

import {FormsModule} from "@angular/forms";

@NgModule({
    imports: [BrowserModule, FormsModule ],
    …
})
export class AppModule {}

Try it out in the browser! You should notice the text input contains "Hoo Ah!", but now when you change the value, it instantly updates the header value.

This completes this introduction on Angular. The solution can be be found on github: github