Quick Angular Website
I dipped my toe into angular and was able to quickly build a static website with some responsive features. This post shares what I learned.
Getting started is easy. After installing angular (I used homebrew on my Mac with brew install
), you can quickly start a new project using the ng
tool with the new
command.
My project is going to be a simple website for displaying information about Zombie Movies. It will use Bootstrap styles and be reactive with JQuery. There will be simple filtering and searching used to make finding the next movie to watch an easy process.
To start, generate the base project and install some dependencies:
ng new the-zombie-movie-club
npm install --save jquery
npm install --save bootstrap
The --save
option means that the dependency will be installed for just this project rather than globally. You can also use the --save-dev
to let the installer know that the dependency should not be included in the distributed files. For example, test and build packages do not need to be deployed to the production server.
To ensure that the all the required .js
and .css
files are included, we need to make a few additions to the angular.json
file so that they are added to the distribution. They can be added under build->options like this:
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.css"
],
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"node_modules/bootstrap/dist/js/bootstrap.min.js"
]
Note that I found that the order the .js
files are added is important.
Loading the Data
The ng
command helps build up the components of the application. First I’m going to add generate a service to load JSON data containing information about the movies with:
ng generate service data
This creates a couple of new files, the service and a test. I’m going to use a simple JSON file for the data for now so the data.service.ts
file will end up looking like this.
import { Injectable } from '@angular/core';
import * as zombieMovies from './data-zombie-movies.json';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor() { }
getZombieMovies(): any {
// assigning an arbitrary id for linking to modals
var movies = (zombieMovies as any).default;
var index: number = 0;
var movieList = movies['film_list'];
for (let i=0; i < movieList.length; i++) {
movieList[i]['ID'] = i;
}
return movies;
}
}
The getZombieMovies()
and the associated file import is what I’ve added along with the file containing the movie information. This is in the format returned by the Open Move DB so I won’t repeat it here. Later this might get the data from a database or elsewhere.
Note that importing a JSON file like this needs a change to let the compiler know that it should allow imports from .json
files. This can be done by adding the following compiler option to the tsconfig.json
file in the root folder of the project.
"resolveJsonModule": true
Before moving on to display the movies, I’ll add a basic test to check that the data service is giving back a list of movies as expected. This checks that the first movie in the film_list
list of the JSON object has the title “The 8th Plague”.
it('should load the movies where The 8th Plague is the title of the first', () => {
expect(service.getZombieMovies()['film_list'][0]['Title']).toEqual("The 8th Plague");
});
Displaying the Movies
Next the application needs a component to display the data with. Again, we can use the ng
command to build this and tie it into the application code.
ng generate component movie-list
The app’s main html page is generated with a sample page, we can replace that with some basic HTML to show the new component beneath the title for the site:
<div class="container">
<h1>The Zombie Movie Club</h1>
<app-movie-list></app-movie-list>
</div>
Note that the HTML tag app-movie-list
is now available to use within the app. The site can be tested in development mode with ng serve
. Opening Chrome and navigating to http://localhost:4200 will reveal a simple site showing the new element.
For the new component to display the data, it needs to use the data service to load it. This can be injected into movie-list.component.ts
, the file created by the generation command above. Firstly, add an import for the service:
import { DataService } from '../data.service';
Then adjust the constructor to use the service. Angular will inject the appropriate object at instantiation:
constructor(private dataService: DataService) {}
We are going to store the movies in a local private variable and provide a method to load them.
movies: any;
loadMovies(): void {
if (this.movies == null) {
this.movies = this.dataService.getZombieMovies();
}
}
Finally, we will update the initialisation function to call the load function so that the data is available to the template.
ngOnInit(): void {
this.loadMovies();
}
More Components
The basic layout will be all the zombie movies with a synopsis and some information. The user will be able to click a button to open a modal dialog with more data. The sub-component needs to be able to be passed the film so that it can display the data. It also needs a reference so that the button can toggle it to show.
First generate a new component called info-modal
with ng generate component info-modal
. There is a small change to make to the file generated, info-modal.component.ts
, to allow it to accept parameters. This is done with the @Input
annotation which needs to be imported first:
import {Input} from '@angular/core';
...
@Input()
film : any;
@Input()
modalId : string;
When including this in the movie-list.component.html
, we can now pass film and id to the sub-component as follows:
<app-info-modal [modalId]="'modal' + film.ID" [film]="film"></app-info-modal>
That is is for the code. This post won’t go into how the contents will be displayed. The HTML is below, and simply uses Bootstrap cards and modals to list all the films in the database. I’ll add filtering and searching in a later post. Also I’ll add/fix the tests and show how to package and deploy the site.
Here is the contents of movie-list.component.html
:
<div>
<div class="card-columns">
<div class="card" style="width: 18rem;" *ngFor="let film of movies.film_list; let i=index">
<img class="card-img-top" src="{{film.Poster}}" alt="{{film.Title}}">
<div class="card-body">
<h5 class="card-title">{{film.Title}} ({{film.Year}})</h5>
<p class="card-text">{{film.Plot}}</p>
<div>
<p><em>IMDB Rating:</em> {{film.imdbRating}} ({{film.imdbVotes}} votes)</p>
</div>
<button
type="button"
class="btn btn-primary"
data-toggle="modal"
[attr.data-target]="'#modal' + film.ID">
More Info
</button>
</div>
<app-info-modal [modalId]="'modal' + film.ID" [film]="film"></app-info-modal>
</div>
</div>
</div>
Which renders as follows:
Here is the content of the modal popup, info-modal.component.html
:
<div class="modal fade" tabindex="-1" [attr.id]="modalId" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{{ film.Title }} ({{ film.Year }})</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-4"><img class="card-img-top" src="{{film.Poster}}" alt="{{film.Title}}"></div>
<div class="col-md-8">
<p><em>Director:</em> {{ film.Director }}</p>
<p><em>Writer:</em> {{ film.Writer }}</p>
<p><em>Actors:</em> {{ film.Actors }}</p>
<p><em>Genre:</em> {{ film.Genre }}</p>
<p><em>Language:</em> {{ film.Language }}</p>
</div>
</div>
<h3>Plot</h3>
<p>{{ film.Plot }}</p>
<h3>Ratings</h3>
<table class="table">
<tr *ngIf="film.Ratings.length > 1">
<th>Source</th>
<th>Rating</th>
</tr>
<tr *ngFor="let rating of film.Ratings">
<td>{{rating.Source}}</td>
<td>{{rating.Value}}</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
The modal screen looks like this:
Next up will be to package the application up and deploy it so that it can be served by Nginx. Then the site will be improved to allow searching by name or genre.