Building an App from Scratch with Angular and Webpack

March 13, 2018
Written by
Maciej Treder
Contributor
Opinions expressed by Twilio contributors are their own

record_player_arm

Using Angular CLI is a popular way of creating Single Page Apps (SPA) with Angular. However, sometimes CLI is not an option for your project or you just want to have better control on what’s going on behind the scenes. Let’s take a look at how we can create an Angular project entirely from scratch.

In this post we will:

  • Set up a Node project
  • Create an Angular application and make calls to an external API
  • Bundle the application using webpack

To accomplish tasks make sure you have the following installed:

  • Node.js and npm (at the moment of writing this post I was using Node.js v8.5.0 and npm 5.3.0)

Let’s get started!

Set up the project, webpack & ‘Hello World!’

As a first step which we have to initialize our Node project and install the packages we will use:

mkdir myAngularApp
cd myAngularApp
npm init -y
npm install html-webpack-plugin copy-webpack-plugin typescript webpack —save-dev
npm install ejs express —save

Let’s create our application entry point, a simple ‘Hello World’ which we’ll change in the near future:

touch webpack.config.js
touch server.js
mkdir -p src/assets/img
mkdir -p src/assets/styles
touch src/index.html
touch src/main.ts
touch src/assets/styles/main.css

We are going to host our app using Node.js, so create a server.js file:

const express = require('express');

const port = 3000 || process.env.PORT;
const app = express();

app.engine('html', require('ejs').renderFile);

app.set('view engine', 'html');
app.set('views', 'dist');

app.use('/', express.static('dist', { index: false }));

app.get('/*', (req, res) => {
   res.render('./index', {req, res});
});

app.listen(port, () => {
   console.log(`Listening on: http://localhost:${port}`);
});

Create a file src/index.html which will host our SPA:

<html lang="en">
<head>
   <title>Angular & Webpack demystified</title>
   <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,300">
   <link rel="stylesheet" href="assets/styles/main.css">
</head>
<body>
<div class="content">
   <h1>Hello World!</h1>
</div>
<footer><p>Check also <a href="https://github.com/maciejtreder/angular-universal-pwa">Angular Universal PWA</a> by <a href="https://www.maciejtreder.com">Maciej Treder</a></p></footer>
</body>
</html>

To add some styling to the app, add the following code in src/assets/styles/main.css:

body {
   margin: 0 auto;
   max-width: 1000px;
   background: url('../../assets/img/sandbox.png') no-repeat center;
   display: flex;
   flex-direction: column;
   height: 100%;
   font-family: 'Source Sans Pro', calibri, Arial, sans-serif !important;
   min-height: 550px;
}
.content {
   flex: 1 0 auto;
}
footer {
   padding: 10px 0;
   text-align: center;
}

Here we’ll use my favorite fancy background image:


Download the picture and place it in the src/assets/img folder.

Create The Main TypeScript File and Prepare to Compile

We’ll write our app in TypeScript and will then transpile it to JavaScript. The output will be injected into the output rendered by Node.js.

Let’s create our main file – src/main.ts:

console.log("I am entry point of SPA");

Next we are going to prepare for the compilation of our app. Add the following build and start scripts to your package.json file:

{
  "author": "Maciej Treder <contact@maciejtreder.com>",
  "description": "",
  "name": "myAngularApp",
  "version": "1.0.0",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/maciejtreder/angular-universal-pwa.git"
  },
  "scripts": {
    "build": "webpack",
    "start": "node server.js"
  },
  "engines": {
    "node": ">=6.0.0"
  },
  "license": "MIT",
  "devDependencies": {
    "copy-webpack-plugin": "^4.3.1",
    "html-webpack-plugin": "^2.30.1",
    "typescript": "^2.7.1",
    "webpack": "^3.10.0"
  },
  "dependencies": {
    "ejs": "^2.5.7",
    "express": "^4.16.2"
  }
}

We use webpack to bundle our code. Webpack is also the bundler used by Angular CLI behind the scenes. We are going to use webpack instead of the CLI to have more control of what we are doing during the build. Next we need to create a webpack.config.js file for our compilation:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = function () {
   return {
       entry: './src/main.ts',
       output: {
           path: __dirname + '/dist',
           filename: 'app.js'
       },

       plugins: [
           new CopyWebpackPlugin([
               { from: 'src/assets', to: 'assets'}
           ]), 
           new HtmlWebpackPlugin({
               template: __dirname + '/src/index.html',
               output: __dirname + '/dist',
               inject: 'head'
           })
       ]
   };
}

Let’s stop here for a moment and explain what is going on inside the Webpack config file. This file returns an object consumed by Webpack to let it know what to build and how to build it:

  • First we need to define the entry file, in our case it is ./src/main.ts.
  • After that we tell Webpack what output file we expect and where (we use the __dirname variable because Webpack expects an absolute path).
  • We also introduce CopyWebpackPlugin to move our assets together with the compiled application.
  • Finally we want to instruct Webpack to attach our script in the index.html file. To achieve that we use HtmlWebpackPlugin. Inside the plugin we are specifying a template file, in which a

     

    Let’s run our application and take a look if it is working as we expect. Run npm start in your command-line:

npm start

> myAngularApp@1.0.0 start /myAngularApp
> node server.js

Listening on: http://localhost:3000

Open the URL (in this case http://localhost:3000) in your browser and you should see:

You can find all the code up to this point in this GitHub repository

git clone -b angular_and_webpack_demystified_step1 https://github.com/maciejtreder/angular-universal-pwa.git myAngularApp

 

Go on with Angular

Now that our setup is done, we can move forward with Angular. Install Angular’s dependencies by running:

npm install @angular/common @angular/compiler @angular/core @angular/platform-browser @angular/platform-browser-dynamic rxjs zone.js  —save
npm install @ngtools/webpack @angular/compiler-cli script-ext-html-webpack-plugin —save-dev

We can prepare the files which we are going to edit in this step by running:

mkdir src/app
touch src/app/app.module.ts
touch src/app/app.component.ts
touch tsconfig.json

Add the following two things in the src/index.html file. The first is the “base path” (it is necessary to make Angular routing work correctly). Second is our App “wrapper” (). After these changes your src/index.html should look like the following:

<html lang="en">
 <head>
   <base href="/">
   <title>Angular & Webpack demistyfied</title>
   <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,300">
   <link rel="stylesheet" href="assets/styles/main.css">
</head>
<body>
<div class="content">
   <my-app></my-app>
</div>
<footer><p>Check also <a href="https://github.com/maciejtreder/angular-universal-pwa">Angular Universal PWA</a> by <a href="https://www.maciejtreder.com">Maciej Treder</a></p></footer>
</body>
</html>

Now we can start working on the Angular aspect of the app. Create your first component inside the src/app/app.component.ts file:

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

@Component({
 selector: 'my-app',
 template: '<h1>Hello world!</h1>',
})
export class AppComponent {

 constructor(){
   console.log("I am Angular!")
 }
}

Every component must be declared in some module to be used. Copy this code into src/app/app.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { AppComponent } from './app.component';
import { BrowserModule } from '@angular/platform-browser';

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

This is going to be main Module of our app so we add the bootstrap attribute inside the @NgModule annotation. It specifies which component should be used upon the start of our application.

Now that our basic module is ready, let’s invoke it from src/main.ts:

import 'zone.js/dist/zone';

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

platformBrowserDynamic().bootstrapModule(AppModule);

Next we need to update our compilation configuration. First, inside webpack.config.js, we need to inform Webpack what kind of files it should resolve and let it know how to compile our TypeScript files using @ngtools/webpack. Lastly we use ScriptExtPlugin to give Webpack information on how we want to load our application in the index.html file.

(If we don’t do that, Angular will complain that it doesn’t know where to load our application. You’ll see the following error in the browser console:
Error: The selector "app" did not match any elements). We want our script to be loaded in defer mode (executed after DOM initialization), so we need to add this information in ScriptExtPlugin. Modify your webpack.config.js file accordingly:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ScriptExtPlugin = require('script-ext-html-webpack-plugin');
const { AngularCompilerPlugin } = require('@ngtools/webpack');


module.exports = function () {
    return {
        entry: './src/main.ts',
        output: {
            path: __dirname + '/dist',
            filename: 'app.js'
        },
        resolve: {
            extensions: ['.ts', '.js']
        },

        module: {
            rules: [
                {test: /\.ts$/, loader: '@ngtools/webpack'}
            ]
        },
        plugins: [
            new CopyWebpackPlugin([
                { from: 'src/assets', to: 'assets'}
            ]),
            new HtmlWebpackPlugin({
                template: __dirname + '/src/index.html',
                output: __dirname + '/dist',
                inject: 'head'
            }),

            new ScriptExtPlugin({
                defaultAttribute: 'defer'
            }),
     new AngularCompilerPlugin({
               tsConfigPath: './tsconfig.json',
               entryModule: './src/app/app.module#AppModule',
               sourceMap: true
            })

        ]
    };
}

We also need to configure the TypeScript compiler used by @ngtools/webpack by creating a tsconfig.json file with the following content:

{
 "compilerOptions": {
   "experimentalDecorators": true,
   "lib": [ "dom", "esnext" ]
 }
}

The experimentalDecorators option is responsible for properly processing decorator annotations (@Component and @NgModule). lib specifies the libraries used in our application and dependencies.

Now we are ready to compile and launch our application:

npm run build
npm start

Open http://localhost:3000 in your browser and your application should look like this:

You can find all the code up to this point in this GitHub repository:

git clone -b angular_and_webpack_demystified_step2 https://github.com/maciejtreder/angular-universal-pwa.git myAngularApp

 

More on components and services

Our app isn’t really complicated so far. Let’s add a service to it by installing the following dependencies and initializing the files:

npm install —save-dev raw-loader
touch src/app/app.component.html
touch src/app/app.component.css
mkdir src/app/services
touch src/app/services/echo.service.ts
mkdir -p src/assets/img
mkdir -p src/assets/styles
touch src/assets/styles/main.css

Start by adding more code to src/app/app.component.ts:

import { Component, OnInit } from '@angular/core';
import { EchoService } from './services/echo.service';
import { Observable } from 'rxjs/Observable';

@Component({
 selector: 'my-app',
 templateUrl: `app.component.html`,
 styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
   public response: Observable<any>;

   constructor(private echoService: EchoService) {}

   public ngOnInit(): void {
       this.response = this.echoService.makeCall();
   }
}

We introduced a few concepts here. First, we showed external templates and styles using templateUrl and styleUrls. Angular gives us the possibility to create HTML templates and  stylesheets outside of the component class and this makes our code cleaner.

Create the HTML template by placing the following code into src/app/app.component.html:

<h1>Hello World!</h1>
<h2>This component injects EchoService</h2>
Response is: <span>{{response | async | json}}</span>

And the stylesheet in src/app/app.component.css:

span {   
   color: purple;
   display: block;
   background: #ccc;
   padding: 5px;
}

Additionally, we introduced the concept of Dependency Injection. If you take close look at the constructor you will see a parameter of type EchoService. Angular will look for a class of this type and instantiate a singleton for us. After that it will be automatically (almost magically) injected into our component and we will be able to reuse it later (this mechanism works similar to auto-injection in the Spring Framework).

We also used the Observable type and async pipe inside template. Observables work similarly to Promises asynchronous types that emit values pushed to them by another function. If you are unfamiliar with Promises, make sure to check out “A quick guide to JavaScript Promises”.

Observables allow multiple listeners and can emit multiple values that can be manipulated using different operations such as map or filter (you can read more about it at the RxJS github page ). The sync pipe is a special Angular mechanism to display our variable in the view template only when it is evaluated (in other words: a value is emitted by the Observable).

Last but not least we showed the OnInit interface and ngOnInit lifecycle methods. Interfaces in TypeScript work the same way you might expect expected – if you implement it, you must implement methods declared in it. In this particular case, we need to implement the ngOnInit() method. This method is one of the “Angular lifecycle hooks” – methods called automatically by the Angular engine for different life stages of components such as initialization, destruction and other events. You can learn more about them in the Angular documentation.

More Angular ‘Magic’

We injected into component a service that doesn’t exist yet. Create it by adding the following code into src/app/services/echo.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class EchoService {
   constructor(private httpClient: HttpClient) {}

   public makeCall(): Observable<any> {
       return this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1');
   }
}

This service contains only one method to  make a GET request to the https://jsonplaceholder.typicode.com/posts/1.

We’ve been using a lot of Angular “magic” but before our app is fully working we need to connect everything. This is done in the @NgModule decorator of our main module. Make the following changes to provide the necessary information about our new service and dependencies in our application. Modify the src/app/app.module.ts accordingly:

Let’s break down the information specified in this decorator a bit:

  • bootstrap  – the component we want to load as a “main” one
  • imports – links to other modules; here we currently have only modules from the @angular librarydeclarations – a list of components used in the module
  • providers – list of services used across module

The code for our app is now ready. As a last step before the final compilation, we need to modify our webpack configuration once more. Because we introduced external HTML templates and stylesheets, we need to add new loaders for these file types. Change the module section in the webpack.config.js file to:

module: {
   rules: [
       {test: /\.ts$/, loaders: ['@ngtools/webpack']},
       { test: /\.css$/, loader: 'raw-loader' },
       { test: /\.html$/, loader: 'raw-loader' }
   ]
},

 

Compile and run your application:

npm run build
npm start

When you open http://localhost:3000, the result should now look like this:

If you want to reproduce the steps up to here, you can also clone the code from this GitHub repository:

git clone -b angular_and_webpack_demystified_step3 https://github.com/maciejtreder/angular-universal-pwa.git myAngularApp
cd myAngularApp/
npm install
npm run build
npm start

 

Summarizing Scratch Building an Angular and Webpack App

Today we successfully implemented and ran an Angular 5 application entirely from scratch. You also learned about modules and services in Angular. I have hope that you enjoyed it and have already a great idea on where to take your application!

GitHub repository: https://github.com/maciejtreder/angular-universal-pwa/tree/angular_and_webpack_demystified_step3

Also, check out https://github.com/maciejtreder/angular-universal-pwa to learn more Angular features.

Maciej Treder,
contact@maciejtreder.com
https://www.maciejtreder.com
@maciejtreder (GitHub, Twitter, StackOverflow, LinkedIn)