In this article by, Chandermani Arora and Kevin Hennessy, the authors of the book Angular 2 By Example, we will build a new app in Angular, and in the process, develop a better understanding of the framework. This app will also help us explore some new capabilities of the framework.
(For more resources related to this topic, see here.)
The topics that we will cover in this article include the following:
Let's get started! The first thing we will do is define the scope of our 7 Minute Workout app.
We want everyone reading this article to be physically fit. Our purpose is to simulate your grey matter. What better way to do it than to build an app that targets physical fitness!
7 Minute Workout is an exercise/workout plan that requires us to perform a set of twelve exercises in quick succession within the seven minute time span. 7 Minute Workout has become quite popular due to its benefits and the short duration of the workout. We cannot confirm or refute the claims but doing any form of strenuous physical activity is better than doing nothing at all. If you are interested to know more about the workout, then check out http://well.blogs.nytimes.com/2013/05/09/the-scientific-7-minute-workout/.
The technicalities of the app include performing a set of 12 exercises, dedicating 30 seconds for each of the exercises. This is followed by a brief rest period before starting the next exercise. For the app that we are building, we will be taking rest periods of 10 seconds each. So, the total duration comes out be a little more than 7 minutes.
Once the 7 Minute Workout app ready, it will look something like this:
The code for this app can be downloaded from the GitHub site https://github.com/chandermani/angular2byexample dedicated to this article. Since we are building the app incrementally, we have created multiple checkpoints that map to GitHub branches such as checkpoint2.1, checkpoint2.2, and so on. During the narration, we will highlight the branch for reference. These branches will contain the work done on the app up to that point in time.
The 7 Minute Workout code is available inside the repository folder named trainer.
So let's get started!
Remember that we are building on a modern platform for which browsers still lack support. Therefore, directly referencing script files in HTML is out of question (while common, it's a dated approach that we should avoid anyway). The current browsers do not understand TypeScript; as a matter of fact, even ES 2015 (also known as ES6) is not supported. This implies that there has to be a process that converts code written in TypeScript into standard JavaScript (ES5), which browsers can work with.
Hence, having a build setup for almost any Angular 2 app becomes imperative. Having a build process may seem like overkill for a small application, but it has some other advantages as well.
If you are a frontend developer working on the web stack, you cannot avoid Node.js. This is the most widely used platform for Web/JavaScript development. So, no prizes for guessing that the Angular 2 build setup too is supported over Node.js with tools such as Grunt, Gulp, JSPM, and webpack.
Since we are building on the Node.js platform, install Node.js before starting.
While there are quite elaborate build setup options available online, we go for a minimal setup using Gulp. The reason is that there is no one size fits all solution out there. Also, the primary aim here is to learn about Angular 2 and not to worry too much about the intricacies of setting up and running a build.
Some of the notable starter sites plus build setups created by the community are as follows:
Start site |
Location |
angular2-webpack-starter |
|
angular2-seed |
|
angular-cli— It allows us to generate the initial code setup, including the build configurations, and has good scaffolding capabilities too. |
A natural question arises if you are very new to Node.js or the overall build process: what does a typical Angular build involve? It depends! To get an idea about this process, it would be beneficial if we look at the build setup defined for our app. Let's set up the app's build locally then. Follow these steps to have the boilerplate Angular 2 app up and running:
git checkout base
This code serves as the starting point for our app.
npm i -g gulp
npm install
The first command installs Gulp globally so that you can invoke the Gulp command line tool from anywhere and execute Gulp tasks. A Gulp task is an activity that Gulp performs during the build execution. If we look at the Gulp build script (which we will do shortly), we realize that it is nothing but a sequence of tasks performed whenever a build occurs. The second command installs the app's dependencies (in the form of npm packages). Packages in the Node.js world are third-party libraries that are either used by the app or support the app's building process. For example, Gulp itself is a Node.js package. The npm is a command-line tool for pulling these packages from a central repository.
gulp play
This compiles and runs the app. If the build process goes fine, the default browser window/tab will open with a rudimentary "Hello World" page (http://localhost:9000/index.html). We are all set to begin developing our app in Angular 2!
But before we do that, it would be interesting to know what has happened under the hood.
Even if you are new to Gulp, looking at gulpfile.js gives you a fair idea about what the build process is doing. A Gulp build is a set of tasks performed in a predefined order. The end result of such a process is some form of package code that is ready to be run. And if we are building our apps using TypeScript/ES2015 or some other similar language that browsers do not understand natively, then we need an additional build step, called transpilation.
As it stands in 2016, browsers still cannot run ES2015 code. While we are quick to embrace languages that hide the not-so-good parts of JavaScript (ES5), we are still limited by the browser's capabilities. When it comes to language features, ES5 is still the safest bet as all browsers support it. Clearly, we need a mechanism to convert our TypeScript code into plain JavaScript (ES5). Microsoft has a TypeScript compiler that does this job.
The TypeScript compiler takes the TypeScript code and converts it into ES5-format code that can run in all browsers. This process is commonly referred to as transpiling, and since the TypeScript compiler does it, it's called a transpiler.
Interestingly, transpilation can happen at both build/compile time and runtime:
To compile TypeScript files, we can install the TypeScript compiler manually from the command line using this:
npm install -g typescript
Once installed, we can compile any TypeScript file into ES5 format using the compiler (tsc.exe).
But for our build setup, this process is automated using the ts2js Gulp task (check out gulpfile.js). And if you are wondering when we installed TypeScript… well, we did it as part of the npm install step, when setting up the code for the first time. The gulp-typescript package downloads the TypeScript compiler as a dependency.
With this basic understanding of transpilation, we can summarize what happens with our build setup:
livereload also has been set up for the app. Any changes to the code refresh the browser running the app automatically. In case browser refresh fails, we can always do a manual refresh.
This is a rudimentary build setup required to run an Angular app. For complex build requirements, we can always look at the starter/seed projects that have a more complete and robust build setup, or build something of our own.
Next let's look at the boilerplate app code already there and the overall code organization.
This is how we are going to organize our code and other assets for the app:
The trainer folder is the root folder for the app and it has a folder (static) for the static content (such as images, CSS, audio files, and others) and a folder (src) for the app's source code.
The organization of the app's source code is heavily influenced by the design of Angular and the Angular style guide (http://bit.ly/ng2-style-guide) released by the Angular team. The components folder hosts all the components that we create. We will be creating subfolders in this folder for every major component of the application. Each component folder will contain artifacts related to that component, which includes its template, its implementation and other related item. We will also keep adding more top-level folders (inside the src folder) as we build the application.
If we look at the code now, the components/app folder has defined a root level component TrainerAppComponent and root level module AppModule. bootstrap.ts contains code to bootstrap/load the application module (AppModule).
7 Minute Workout uses Just In Time (JIT) compilation to compile Angular views. This implies that views are compiled just before they are rendered in the browser. Angular has a compiler running in the browser that compiles these views.
Angular also supports the Ahead Of Time (AoT) compilation model. With AoT, the views are compiled on the server side using a server version of the Angular compiler. The views returned to the browser are precompiled and ready to be used.
For 7 Minute Workout, we stick to the JIT compilation model just because it is easy to set up as compared to AoT, which requires server-side tweaks and package installation.
We highly recommend that you use AoT compilation for production apps due the numerous benefits it offers. AoT can improve the application's initial load time and reduce its size too. Look at the AoT platform documentation (cookbook) at http://bit.ly/ng2-aot to understand how AoT compilation can benefit you.
Time to start working on our first focus area, which is the app's model!
Designing the model for this app requires us to first detail the functional aspects of the 7 Minute Workout app, and then derive a model that satisfies those requirements. Based on the problem statement defined earlier, some of the obvious requirements are as follows:
Some other valuable features that we will add to this app are as follows:
As we can see, the central theme for this app is workout and exercise. Here, a workout is a set of exercises performed in a specific order for a particular duration. So, let's go ahead and define the model for our workout and exercise.
Based on the requirements just mentioned, we will need the following details about an exercise:
With TypeScript, we can define the classes for our model.
Create a folder called workout-runner inside the src/components folder and copy the model.ts file from the checkpoint2.1 branch folder workout-runner(http://bit.ly/ng2be-2-1-model-ts) to the corresponding local folder. model.ts contains the model definition for our app.
The Exercise class looks like this:
export class Exercise {
constructor(
public name: string,
public title: string,
public description: string,
public image: string,
public nameSound?: string,
public procedure?: string,
public videos?: Array<string>) { }
}
TypeScript Tips
Passing constructor parameters with public or private is a shorthand for creating and initializing class members at one go.
The ? suffix after nameSound, procedure, and videos implies that these are optional parameters.
For the workout, we need to track the following properties:
So, the model class (WorkoutPlan) looks like this:
export class WorkoutPlan {
constructor(
public name: string,
public title: string,
public restBetweenExercise: number,
public exercises: ExercisePlan[],
public description?: string) { }
totalWorkoutDuration(): number { … }
}
The totalWorkoutDuration function returns the total duration of the workout in seconds.
WorkoutPlan has a reference to another class in the preceding definition—ExercisePlan. It tracks the exercise and the duration of the exercise in a workout, which is quite apparent once we look at the definition of ExercisePlan:
export class ExercisePlan {
constructor(
public exercise: Exercise,
public duration: number) { }
}
These three classes constitute our base model, and we will decide in the future whether or not we need to extend this model as we start implementing the app's functionality.
Since we have started with a preconfigured and basic Angular app, you just need to understand how this app bootstrapping is occurring.
Look at the src folder. There is a bootstrap.ts file with only the execution bit (other than imports):
platformBrowserDynamic().bootstrapModule(AppModule);
The boostrapModule function call actually bootstraps the application by loading the root module, AppModule. The process is triggered by this call in index.html:
System.import('app').catch(console.log.bind(console));
The System.import statement sets off the app bootstrapping process by loading the first module from bootstrap.ts.
Modules defined in the context of Angular 2, (using @NgModule decorator) are different from modules SystemJS loads. SystemJS modules are JavaScript modules, which can be in different formats adhering to CommonJS, AMD, or ES2015 specifications.
Angular modules are constructs used by Angular to segregate and organize its artifacts.
Unless the context of discussion is SystemJS, any reference to module implies Angular module.
Now we have details on how SystemJS loads our Angular app.
SystemJS starts loading the JavaScript module with the call to System.import('app') in index.html.
SystemJS starts by loading bootstrap.ts first. The imports defined inside bootstrap.ts cause SystemJS to then load the imported modules. If these module imports have further import statements, SystemJS loads them too, recursively.
And finally the platformBrowserDynamic().bootstrapModule(AppModule); function gets executed once all the imported modules are loaded.
For the SystemJS import function to work, it needs to know where the module is located. We define this in the file, systemjs.config.js, and reference it in index.html, before the System.import script:
<script src="systemjs.config.js"></script>
This configuration file contains all of the necessary configuration for SystemJS to work correctly.
Open systemjs.config.js, the app parameter to import function points to a folder dist as defined on the map object:
var map = {
'app': 'dist',
...
}
And the next variable, packages, contains settings that hint SystemJS how to load a module from a package when no filename/extension is specified. For app, the default module is bootstrap.js:
var packages = {
'app': { main: 'bootstrap.js', defaultExtension: 'js' },
...
};
Are you wondering what the dist folder has to do with our application? Well, this is where our transpiled scripts end up. As we build our app in TypeScript, the TypeScript compiler converts these .ts script files in the src folder to JavaScript modules and deposits them into the dist folder. SystemJS then loads these compiled JavaScript modules. The transpiled code location has been configured as part of the build definition in gulpfile.js. Look for this excerpt in gulpfile.ts:
return tsResult.js
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist'))
The module specification used by our app can again be verified in gulpfile.js. Take a look at this line:
noImplicitAny: true,
module: 'system',
target: 'ES5',
These are TypeScript compiler options, with one being module, that is, the target module definition format.
The system module type is a new module format designed to support the exact semantics of ES2015 modules within ES5.
Once the scripts are transpiled and the module definitions created (in the target format), SystemJS can load these modules and their dependencies.
It's time to get into the thick of action; let's build our first component.
To implement the WorkoutRunnerComponent, we need to outline the behavior of the application.
What we are going to do in the WorkoutRunnerComponent implementation is as follows:
Let's start with the implementation. The first thing that we will create is the WorkoutRunnerComponent implementation.
Open workout-runner folder in the src/components folder and add a new code file called workout-runner.component.ts to it. Add this chunk of code to the file:
import {WorkoutPlan, ExercisePlan, Exercise} from './model'
export class WorkoutRunnerComponent { }
The import module declaration allows us to reference the classes defined in the model.ts file in WorkoutRunnerComponent.
We first need to set up the workout data. Let's do that by adding a constructor and related class properties to the WorkoutRunnerComponent class:
workoutPlan: WorkoutPlan;
restExercise: ExercisePlan;
constructor() {
this.workoutPlan = this.buildWorkout();
this.restExercise = new ExercisePlan(
new Exercise("rest", "Relax!", "Relax a bit", "rest.png"),
this.workoutPlan.restBetweenExercise);
}
The buildWorkout on WorkoutRunnerComponent sets up the complete workout, as we will see shortly. We also initialize a restExercise variable to track even the rest periods as exercise (note that restExercise is an object of type ExercisePlan).
The buildWorkout function is a lengthy function, so it's better if we copy the implementation from the workout runner's implementation available in Git branch checkpoint2.1 (http://bit.ly/ng2be-2-1-workout-runner-component-ts). The buildWorkout code looks like this:
buildWorkout(): WorkoutPlan {
let workout = new WorkoutPlan("7MinWorkout",
"7 Minute Workout", 10, []);
workout.exercises.push(
new ExercisePlan(
new Exercise(
"jumpingJacks",
"Jumping Jacks",
"A jumping jack or star jump, also called side-straddle hop is a physical jumping exercise.",
"JumpingJacks.png",
"jumpingjacks.wav",
`Assume an erect position, with feet together and
arms at your side. …`,
["dmYwZH_BNd0", "BABOdJ-2Z6o", "c4DAnQ6DtF8"]),
30));
// (TRUNCATED) Other 11 workout exercise data.
return workout;
}
This code builds the WorkoutPlan object and pushes the exercise data into the exercises array (an array of ExercisePlan objects), returning the newly built workout.
The initialization is complete; now, it's time to actually implement the start workout. Add a start function to the WorkoutRunnerComponent implementation, as follows:
start() {
this.workoutTimeRemaining =
this.workoutPlan.totalWorkoutDuration();
this.currentExerciseIndex = 0; this.startExercise(this.workoutPlan.exercises[this.currentExerciseIndex]);
}
Then declare the new variables used in the function at the top, with other variable declarations:
workoutTimeRemaining: number;
currentExerciseIndex: number;
The workoutTimeRemaining variable tracks the total time remaining for the workout, and currentExerciseIndex tracks the currently executing exercise index. The call to startExercise actually starts an exercise. This how the code for startExercise looks:
startExercise(exercisePlan: ExercisePlan) {
this.currentExercise = exercisePlan;
this.exerciseRunningDuration = 0;
let intervalId = setInterval(() => {
if (this.exerciseRunningDuration >=
this.currentExercise.duration) {
clearInterval(intervalId);
}
else { this.exerciseRunningDuration++; }
}, 1000);
}
We start by initializing currentExercise and exerciseRunningDuration. The currentExercise variable tracks the exercise in progress and exerciseRunningDuration tracks its duration. These two variables also need to be declared at the top:
currentExercise: ExercisePlan;
exerciseRunningDuration: number;
We use the setInterval JavaScript function with a delay of 1 second (1,000 milliseconds) to track the exercise progress by incrementing exerciseRunningDuration. The setInterval invokes the callback every second. The clearInterval call stops the timer once the exercise duration lapses.
TypeScript Arrow functions
The callback parameter passed to setInterval (()=>{…}) is a lambda function (or an arrow function in ES 2015). Lambda functions are short-form representations of anonymous functions, with added benefits. You can learn more about them at https://basarat.gitbooks.io/typescript/content/docs/arrow-functions.html.
As of now, we have a WorkoutRunnerComponent class. We need to convert it into an Angular component and define the component view.
Add the import for Component and a component decorator (highlighted code):
import {WorkoutPlan, ExercisePlan, Exercise} from './model'
import {Component} from '@angular/core';
@Component({
selector: 'workout-runner',
template: `
<pre>Current Exercise: {{currentExercise | json}}</pre>
<pre>Time Left: {{currentExercise.duration-
exerciseRunningDuration}}</pre>`
})
export class WorkoutRunnerComponent {
As you already know how to create an Angular component. You understand the role of the @Component decorator, what selector does, and how the template is used.
The JavaScript generated for the @Component decorator contains enough metadata about the component. This allows Angular framework to instantiate the correct component at runtime.
String enclosed in backticks (` `) are a new addition to ES2015. Also called template literals, such string literals can be multi line and allow expressions to be embedded inside (not to be confused with Angular expressions). Look at the MDN article here at http://bit.ly/template-literals for more details.
The preceding template HTML will render the raw ExercisePlan object and the exercise time remaining. It has an interesting expression inside the first interpolation: currentExercise | json. The currentExercise property is defined in WorkoutRunnerComponent, but what about the | symbol and what follows it (json)? In the Angular 2 world, it is called a pipe. The sole purpose of a pipe is to transform/format template data. The json pipe here does JSON data formatting. A general sense of what the json pipe does, we can remove the json pipe plus the | symbol and render the template; we are going to do this next.
As our app currently has only one module (AppModule), we add the WorkoutRunnerComponent declaration to it. Update app.module.ts by adding the highlighted code:
import {WorkoutRunnerComponent} from '../workout-runner/workout-runner.component';
@NgModule({
imports: [BrowserModule],
declarations: [TrainerAppComponent, WorkoutRunnerComponent],
Now WorkoutRunnerComponent can be referenced in the root component so that it can be rendered. Modify src/components/app/app.component.ts as highlighted in the following code:
@Component({
...
template: `
<div class="navbar ...> ...
</div>
<div class="container ...>
<workout-runner></workout-runner>
</div>`
})
We have changed the root component template and added the workout-runner element to it. This will render the WorkoutRunnerComponent inside our root component.
While the implementation may look complete there is a crucial piece missing. Nowhere in the code do we actually start the workout. The workout should start as soon as we load the page.
Component life cycle hooks are going to rescue us!
Life of an Angular component is eventful. Components get created, change state during their lifetime and finally they are destroyed. Angular provides some life cycle hooks/functions that the framework invokes (on the component) when such event occurs. Consider these examples:
As developers, we can tap into these key moments and perform some custom logic inside the respective component.
Angular has TypeScript interfaces for each of these hooks that can be applied to the component class to clearly communicate the intent. For example:
class WorkoutRunnerComponent implements OnInit { ngOnInit (){ ... } ...
The interface name can be derived by removing the prefix ng from the function names.
The hook we are going to utilize here is ngOnInit. The ngOnInit function gets fired when the component's data-bound properties are initialized but before the view initialization starts.
Add the ngOnInit function to the WorkoutRunnerComponent class with a call to start the workout:
ngOnInit() {
this.start();
}
And implement the OnInit interface on WorkoutRunnerComponent; it defines the ngOnInit method:
import {Component,OnInit} from '@angular/core';
…
export class WorkoutRunnerComponent implements OnInit {
There are a number of other life cycle hooks, including ngOnDestroy, ngOnChanges, and ngAfterViewInit, that components support; but we are not going to dwell into any of them here. Look at the developer guide (http://bit.ly/ng2-lifecycle) on Life cycle Hooks to learn more about other such hooks.
Time to run our app! Open the command line, navigate to the trainer folder, and type this line:
gulp play
If there are no compilation errors and the browser automatically loads the app (http://localhost:9000/index.html), we should see the following output:
The model data updates with every passing second! Now you'll understand why interpolations ({{ }}) are a great debugging tool.
This will also be a good time to try rendering currentExercise without the json pipe (use {{currentExercise}}), and see what gets rendered.
We are not done yet! Wait long enough on the index.html page and you will realize that the timer stops after 30 seconds. The app does not load the next exercise data. Time to fix it!
Update the code inside the setInterval if condition:
if (this.exerciseRunningDuration >=
this.currentExercise.duration) {
clearInterval(intervalId);
let next: ExercisePlan = this.getNextExercise();
if (next) {
if (next !== this.restExercise) {
this.currentExerciseIndex++;
}
this.startExercise(next);
}
else { console.log("Workout complete!"); }
}
The if condition if (this.exerciseRunningDuration >= this.currentExercise.duration) is used to transition to the next exercise once the time duration of the current exercise lapses. We use getNextExercise to get the next exercise and call startExercise again to repeat the process. If no exercise is returned by the getNextExercise call, the workout is considered complete.
During exercise transitioning, we increment currentExerciseIndex only if the next exercise is not a rest exercise. Remember that the original workout plan does not have a rest exercise. For the sake of consistency, we have created a rest exercise and are now swapping between rest and the standard exercises that are part of the workout plan. Therefore, currentExerciseIndex does not change when the next exercise is rest.
Let's quickly add the getNextExercise function too. Add the function to the WorkoutRunnerComponent class:
getNextExercise(): ExercisePlan {
let nextExercise: ExercisePlan = null;
if (this.currentExercise === this.restExercise) {
nextExercise =
this.workoutPlan.exercises[this.currentExerciseIndex + 1];
}
else if (this.currentExerciseIndex <
this.workoutPlan.exercises.length - 1) {
nextExercise = this.restExercise;
}
return nextExercise;
}
The WorkoutRunnerComponent.getNextExercise returns the next exercise that needs to be performed.
Note that the returned object for getNextExercise is an ExercisePlan object that internally contains the exercise details and the duration for which the exercise runs.
The implementation is quite self-explanatory. If the current exercise is rest, take the next exercise from the workoutPlan.exercises array (based on currentExerciseIndex); otherwise, the next exercise is rest, given that we are not on the last exercise (the else if condition check).
With this, we are ready to test our implementation. So go ahead and refresh index.html. Exercises should flip after every 10 or 30 seconds. Great!
The current build setup automatically compiles any changes made to the script files when the files are saved; it also refreshes the browser post these changes. But just in case the UI does not update or things do not work as expected, refresh the browser window.
If you are having a problem with running the code, look at the Git branch checkpoint2.1 for a working version of what we have done thus far.
Or if you are not using Git, download the snapshot of checkpoint2.1 (a zip file) from http://bit.ly/ng2be-checkpoint2-1. Refer to the README.md file in the trainer folder when setting up the snapshot for the first time.
We have are done with controller.
We started this article with the aim of creating an Angular app which is complex. The 7 Minute Workout app fitted the bill, and you learned a lot about the Angular framework while building this app.
We started by defining the functional specifications of the 7 Minute Workout app. We then focused our efforts on defining the code structure for the app. To build the app, we started off by defining the model of the app. Once the model was in place, we started the actual implementation, by building an Angular component. Angular components are nothing but classes that are decorated with a framework-specific decorator, @Component.
We now have a basic 7 Minute Workout app. For a better user experience, we have added a number of small enhancements to it too, but we are still missing some good-to-have features that would make our app more usable.
Further resources on this subject:
<hr noshade="noshade" size="1"