How to Create MEAN Stack Applications Smoothly (Part 2)
Konrad JaguszewskiReading Time: 6 minutes
Building a frontend in Angular
Parts of the series:
1. Introduction to the MEAN stack
2. Building a frontend in Angular (you are here)
3. Adding an Admin Panel
Welcome back! In the previous part of the tutorial we’ve built an API for the posts. But what is the point in having it, if we can’t share our writings with the world? People are waiting to read them! We need a way to share it with our families and friends. We need a frontend to our backend. Fortunately the second part of the tutorial focuses just on that!
Today we will learn how to create and use components, use the Angular Router, create services, fetch data from an API and render it and finally how to create a pipeline for a text formatting.
If you followed the first part of the tutorial, you should have the server and the API working. If not, don’t worry, you can read the tutorial here or get the code from here.
Setup
We are going to prepare a couple of things now. We will use Bootstrap for styling, so let’s include it in the src/index.html
file. Styles in the <head>
element:
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
And scripts at the bottom of the <body>
:
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
Next step is adding the API URL to the src/environments/environment.ts
file:
export const environment = { production: false, apiUrl: 'http://localhost:3000/api' };
By using Angular’s environment variables we can have a different URL for every environment and we can easily change it.
We will create a bunch of components now using Angular CLI. Simply run these commands:
$ ng generate component components/blog-description $ ng generate component components/header $ ng generate component components/post-details $ ng generate component components/posts-list
The components were automatically generated and added to the AppModule
. Using Angular CLI helps save some time that we would normally be spent on creating the components manually.
Fetching data from an API
Components shouldn’t know how to connect to an API, they should only present data. That’s why we are going to use a service for fetching the posts. Services are a perfect place for that and we can reuse them in different components by injecting.
First we will need to define a Post model that we will be used all across our app. Let’s do it in src/app/models/post.model.ts
:
export default class Post { _id: string; title: string; body: string; author: string; date: Date; isPublished: boolean; }
Next we have to import the HttpClientModule
in order to use the HttpClient
in our service. We can do it in the AppModule
:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { AppComponent } from './app.component'; import { PostsListComponent } from './components/posts-list/posts-list.component'; import { HeaderComponent } from './components/header/header.component'; import { BlogDescriptionComponent } from './components/blog-description/blog-description.component'; import { PostDetailsComponent } from './components/post-details/post-details.component'; @NgModule({ declarations: [ AppComponent, PostsListComponent, HeaderComponent, BlogDescriptionComponent, PostDetailsComponent, ], imports: [ BrowserModule, HttpClientModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
We can now proceed with writing the Post Service. To create our first service, run this command:
$ ng generate service services/post
Without wasting any time we will write a method for fetching the posts:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import { environment } from '../../environments/environment'; import Post from '../models/post.model'; @Injectable() export class PostService { postsUrl = `${environment.apiUrl}/posts`; constructor( private http: HttpClient ) { } public getPosts(): Observable<Post[]> { return this.http .get(this.postsUrl) .map(response => response['data'] as Post[]); } }
At the top there are all the imports required by the service. We are defining postsUrl
as property of the PostService
(notice how we are using the environmental variable here).
Next in the constructor we are injecting the HttpClient
.getPosts()
method is where the magic happens. Observable is a class of RxJS. Thanks to it we can fetch data asynchronously. We are making GET
request to the posts API. From the response we are getting the data and returning it as an array of Post models.
In order to use our service, we have to inject it. We can add it to the providers list in the AppModule
:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { AppComponent } from './app.component'; import { PostsListComponent } from './components/posts-list/posts-list.component'; import { HeaderComponent } from './components/header/header.component'; import { BlogDescriptionComponent } from './components/blog-description/blog-description.component'; import { PostDetailsComponent } from './components/post-details/post-details.component'; import { PostService } from './services/post.service'; @NgModule({ declarations: [ AppComponent, PostsListComponent, HeaderComponent, BlogDescriptionComponent, PostDetailsComponent, ], imports: [ BrowserModule, HttpClientModule ], providers: [ PostService ], bootstrap: [AppComponent] }) export class AppModule { }
Router
While we are in the AppModule
, let’s quickly add a router to our blog. To configure the router we just need to define our routes and pass them to the RouterModule.forRoot
method. It’s only a few lines of code:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { PostsListComponent } from './components/posts-list/posts-list.component'; import { HeaderComponent } from './components/header/header.component'; import { BlogDescriptionComponent } from './components/blog-description/blog-description.component'; import { PostDetailsComponent } from './components/post-details/post-details.component'; import { PostService } from './services/post.service'; const appRoutes: Routes = [ { path: '', component: PostsListComponent }, { path: 'post/:id', component: PostDetailsComponent } ]; @NgModule({ declarations: [ AppComponent, PostsListComponent, HeaderComponent, BlogDescriptionComponent, PostDetailsComponent, ], imports: [ BrowserModule, HttpClientModule, RouterModule.forRoot(appRoutes) ], providers: [ PostService ], bootstrap: [AppComponent] }) export class AppModule { }
Now we need to show the router where it should render the components. We will also write a layout for the app, by the way. The right place for that is the template of the AppComponent
. In src/app/app.component.html
:
<app-header></app-header> <div class="container pt-5 pb-5"> <h1 class="mb-5">{{title}}</h1> <div class="row justify-content-between"> <div class="col-12 col-md-7"> <router-outlet></router-outlet> </div> <div class="col-12 col-md-4"> <app-blog-description></app-blog-description> </div> </div> </div>
Rendering a component is done simply by using its selector in HTML. We want to have the Header
and the BlogDescription
components visible on every page of the app, that’s why we are putting them here. <router-outlet>
is a placeholder for the components that we defined in the routes. With double-curly braces we can render component’s property - {{title}}
(which is defined in the AppComponent
). The class names are just used to style the page with Bootstrap.
And that’s it! The app doesn’t look too good for now and it doesn’t show any valuable information. The components show just the default messages. We have to take care of that!
The header and the blog description
We are going to template our Header
and BlogDescription
components. They are very simple and static components. The Header
will contain a navigation for the website and in the BlogDescription
we will write a welcome message for our readers.
Let’s start with the header:
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <a routerLink="/" class="navbar-brand">MEAN Blog</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNavDropdown"> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Posts</a> </li> </ul> </div> </nav>
One unusual thing is routerLink
inside the anchor tag. routerLink
is a Directive for linking routes. Using standard href
would work, but it would cause the page to reload. This is not desired for SPA, so we are using the router. routerLinkActive
sets active class to an element when a user goes to this route. In routerLinkActiveOptions
we are setting exact
to true
. This means that the active class should be added only when an user is on the homepage. We are going to show the posts' list on /
, so by default the active class would be added here on every other route (that’s not what we want).
Next is the BlogDescription
component:
<div class="pb-3 pt-3"> <h2 class="mb-5">This is my blog</h2> <img src="http://placekitten.com/150/150" alt="Photo of me" class="rounded-circle centered mx-auto d-block mb-4"> <p>Welcome to my blog!<p> <p>First if all I wanted to thank me, because without me, I wouldn't be myself. I'm the main author of this blog and I'm its owner. The blog is very nice, because it's mine.</p> </div>
Nothing much here. Only simple HTML with some Bootstrap classes.
We should see the header and the description now. To complete the homepage we only need to show the posts.
Showing the posts’ list
Let’s jump to the PostsListComponent
.
We will get the posts from the API through our newly created PostService
and assign them to a component property.
import { Component } from '@angular/core'; import { PostService } from '../../services/post.service'; import Post from '../../models/post.model'; @Component({ selector: 'app-posts-list', templateUrl: './posts-list.component.html' }) export class PostsListComponent { postsList: Post[]; constructor( private postService: PostService ) { postService.getPosts() .subscribe((posts: Post[]) => { this.postsList = posts; }); } }
First thing we do in the class is defining the postsList
as array of Post models. To use the PostService
, we need to inject it - and that’s what we are doing in the constructor method parameter. Inside the constructor we are subscribing to the getPosts
method. This way we can assign the returned data to the component’s postsList
property, when it gets fetched from the API.
The template:
<div class="posts-list"> <div *ngFor="let post of postsList" class="pt-3 pb-3 mb-5"> <h2 class="mb-4"><a routerLink="/post/{{post._id}}" class="text-dark">{{post.title}}</a></h2> <div class="text-secondary mb-3 text-right"> {{post.date | date: 'longDate'}} <br> Author: {{post.author}} </div> <div class="mb-4">{{post.body}}</div> <a routerLink="/post/{{post._id}}" class="btn btn-outline-dark">Read more!</a> </div> </div>
We have two new things here. So let’s take a look at them:
- The first thing is
*ngFor
. It is used for rendering a list of items. It iterates the list of posts and creates a block for each item. | date: ‘longDate’
is a Pipe. It formats a date to a desired look. We will learn how to make custom pipes shortly.
We should be able to see the posts on the homepage now. But the view isn’t very nice. It shows all the posts, even the unpublished ones. They are ordered from the oldest to the newest. Also the whole body of a post is not required here. We need to change that. We want our readers to see only fresh content with a couple of sentences.
Creating a pipe
First of all, let’s trim the post’s body. We need to create our own pipe for that. We can use Angular CLI once again:
$ ng generate pipe utils/trim
We can populate a new file now:
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'trim' }) export class TrimPipe implements PipeTransform { private forbiddenChars: string[] = [',', '.']; transform(string: string, limit: number = 50, ellipsis: string = '...'): any { const strippedString = this.stripHTMLTags(string); let limitedString = strippedString.split(' ').slice(0, limit).join(' '); if (this.endsWithForbiddenChar(limitedString)) { limitedString = limitedString.slice(0, -1); } return `${limitedString}${ellipsis}`; } private stripHTMLTags(string: string): string { const tmpDiv = document.createElement('div'); tmpDiv.innerHTML = string; return tmpDiv.textContent; } private endsWithForbiddenChar(string: string): boolean { return this.forbiddenChars.includes(string.slice(-1)); } }
Here’s what’s happening. The most important method is transform
- it accepts an input and returns transformed value. Other methods are just for the sake of readability. We are stripping the HTML tags here (we don’t need them for the post’s teaser on the homepage). Next we are removing unnecessary words (by default we want to show 50 words). Then we have to check if the trimmed text doesn’t end with a comma or period (we are removing it, if it does). Finally we are adding an ellipsis at the end of the string and returning it.
We can now use our pipe in a template like any other pipe:
{{post.body | trim}}
Filtering and sorting posts
Next thing we would like to achieve is showing only the published posts from newest to oldest. Let’s add a new method to the PostService
:
public getPublishedPosts(): Observable<Post[]> { return this.getPosts() .map(posts => { return posts .filter(post => post.isPublished) .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); }); }
Now we only need to change it in the PostsListComponent
constructor method to:
postService.getPublishedPosts()
That’s it! We’ve made a nice list of posts for our readers.
Showing a single post
Okay, we have the homepage where users can see the list of posts with short description, but they are still unable to read the whole post. We have already prepared the router and it shows the PostDetails
component. We need to populate it with data now.
First let’s add a new method to the PostService
. This method will fetch a single post by its ID.
public getPostById(id: string): Observable<Post> { return this.http .get(`${this.postsUrl}/${id}`) .map(response => response['data'] as Post); }
We already know what is going on here from the getPosts
method, we only have a different URL.
In order to show the data in the component we need to make use of the newly created method in the PostDetailsComponent
:
import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import Post from '../../models/post.model'; import { PostService } from '../../services/post.service'; @Component({ selector: 'app-post-details', templateUrl: './post-details.component.html' }) export class PostDetailsComponent { post: Post; constructor( private route: ActivatedRoute, private postService: PostService ) { const id = route.snapshot.params['id']; postService.getPostById(id) .subscribe((post: Post) => { this.post = post; }); } }
This is very similar to the PostsList
component. The difference is that we need to inject the ActivatedRoute
in order to get the ID from the current route and pass it to the service.
Time for a template:
<div class="post-details pt-3 pb-3"> <div class="text-secondary mb-4 text-right"> {{post?.date | date: 'longDate'}}<br> Author: {{post?.author}} </div> <div [innerHTML]="post?.body"></div> </div>
You’ve probably noticed the ?
sign beside the post
. It’s the “safe navigation operator”. We are fetching the post's data asynchronously, so it’s not available in the component instantly. Before displaying the data, we need to check if the value is not null or undefined. {{post?.author}}
is a shorter and a cleaner way of writing {{post && post.author}}
.
The last thing in this part will be to change the title of the page. We need to set it to the title of the currently viewed post.
Changing the title
We have a small problem here. The title is in the App
component, but we need to change it from the PostDetails
component. Once again service and RxJS come to the rescue!
We already know the command for creating a new service:
$ ng generate service services/page-title
We just need to add a couple of lines of code:
import { Injectable } from '@angular/core'; import { Subject } from 'rxjs/Subject'; @Injectable() export class PageTitleService { title = new Subject<string>(); public changeTitle(title: string) { this.title.next(title); } }
A Subject is both Observer and Observable. Which means that we can subscribe to it and also change its value. In the PageTitle
service we're creating a new Subject and we're writing a method to change its value.
We need to provide a new service in AppModule
:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { PostsListComponent } from './components/posts-list/posts-list.component'; import { HeaderComponent } from './components/header/header.component'; import { BlogDescriptionComponent } from './components/blog-description/blog-description.component'; import { PostDetailsComponent } from './components/post-details/post-details.component'; import { PostService } from './services/post.service'; import { PageTitleService } from './services/page-title.service'; import { TrimPipe } from './utils/trim.pipe'; const appRoutes: Routes = [ { path: '', component: PostsListComponent }, { path: 'post/:id', component: PostDetailsComponent } ]; @NgModule({ declarations: [ AppComponent, PostsListComponent, TrimPipe, HeaderComponent, BlogDescriptionComponent, PostDetailsComponent, ], imports: [ BrowserModule, HttpClientModule, RouterModule.forRoot(appRoutes) ], providers: [ PostService, PageTitleService ], bootstrap: [AppComponent] }) export class AppModule { }
We can now update the title from the PostDetails
and PostsList
components. In the PostDetailsPostDetails
component we have to do this after we have the post title from the API. We can do this in the subscribe()
method, in the constructor (don't forget about importing a new service and injecting it):
import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import Post from '../../models/post.model'; import { PostService } from '../../services/post.service'; import { PageTitleService } from '../../services/page-title.service'; @Component({ selector: 'app-post-details', templateUrl: './post-details.component.html' }) export class PostDetailsComponent { post: Post; constructor( private route: ActivatedRoute, private postService: PostService, private pageTitleService: PageTitleService ) { const id = route.snapshot.params['id']; postService.getPostById(id) .subscribe((post: Post) => { this.post = post; this.pageTitleService.changeTitle(this.post.title); }); } }
It’s similar for the PostsList
component:
constructor( private postService: PostService, private pageTitleService: PageTitleService ) { postService.getPublishedPosts() .subscribe((posts: Post[]) => { this.postsList = posts; }); pageTitleService.changeTitle('MEAN Blog'); }
In the AppComponent
we need to watch for changes in the title
:
constructor( private pageTitleService: PageTitleService ) { pageTitleService.title .subscribe(title => { this.title = title; }); }
We have completed the final step! Our blog shows the posts' list and serves them on separate pages for users to read them. If your version doesn’t work as expected, leave us a comment and check the repo here.
This concludes the second part of the MEAN stack tutorial. We have learnt a lot today! Some things could be new for you, thus keep practicing them by building your own projects. In the next part of this series' tutorials we will add an Admin Panel to our blog, which will make adding new posts and editing them much easier. Stay tuned 🙂