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 atemplate
file, in which aLet’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” oneimports
– links to other modules; here we currently have only modules from the@angular
librarydeclarations
– a list of components used in the moduleproviders
– 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)