Menu Home

How to build a blog with MongoDB, Express, Angular 2 and Node.js on Ubuntu – Part 3

Spread the love

In Part 1 of “How to build a blog with MongoDB, Express, Angular 2 and Node.JS on Ubuntu” we set up the foundations for our application. In part 2 we’ve created our CRUD API. In part 3 we will create the frontend logic to piece it all together.

We’re building a very simple blog, in a Wikipedia style, everybody can manage this blog, there will be no ACLs.

1. Adding some style

We’ll install bootstrap just to add some basic styles

npm install --save bootstrap-sass

And then change the .angular-cli file to make angular aware of it

"styles": [
        "styles.css",
        "../node_modules/bootstrap-sass/assets/stylesheets/_bootstrap.scss"
      ],

 2. The blog service

The blog service will be the core of our application. Think of it as the bridge between the components and our API. To generate the Blog service we will use angular-cli. All files are created by angular-cli are stored in the src folder.

ng generate service Blog

The service created has only the basic declaration. Now we need to add our logic. Open the blog.service.ts file with your favourite editor and let’s add some methods. Don’t forget, Angular is written in typescript so all files have the .ts extension.

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

@Injectable()
export class BlogService {

    constructor() {
    }

    getPosts(): void {
    }

    getPost(postId): void {
    }

    savePost(id, data): void {
    }

    removePost(id): void {
    }

    private handleError(error: Response | any) {
    }

}

Angular comes pre-packed with an HTTP library, let’s use that to access our API. Import the HTTP library in the top of the Blog service, we’ll need Http and Response from “@Angular/http”

import {Http, Response} from '@angular/http';

Angular has a very neat dependency injection system, just add the declaration to the constructor and the library will be available as a property

constructor(private http: Http) {}

Now we need a Comment and a Post Object. Let’s add those to our service file, right before the @Injectable decorator. Further ahead we will move those to individual files, but for now, let’s keep it here.

export class Comment {
    _id: number;
    comment: string;
    created_at: Date;
}


export class Post {
    _id: number;
    name: string;
    body: string;
    created_at: Date;
    updated_at: Date | null;
    comments: Comment[];
}

We now have all we need to start retrieving data from the API.

The Http class methods return an observer, we will be using promises so we need to convert the observer to a promise, the only problem is that Angular’s Http service does not support promises natively, so we need to include an extension class.

Right after the last import, include this piece of code:

import 'rxjs/add/operator/toPromise';

Now change the getPosts method to:

getPosts(): Promise<Post[]> {
        return this.http
            .get('/api/blog')
            .toPromise()
            .then((response) => {
                return response.json().posts as Post[];
            })
            .catch(this.handleError);
    }

What we are saying is that getPosts will return a Promise that will resolve to an Array of Post. We then call the get method to retrieve the collection from our API. Do you remember the handleError method that we declared before? It’s being used to handle potential errors. Let’s write that:

private handleError(error: Response | any) {
    console.error('An error occurred', error);
    return Promise.reject(error.message || error);
}

All the other methods are similar, they return a promise that resolves either to a Post object, an Array of Post objects or a Response object. Here is the complete file.

import {Injectable} from '@angular/core';
import {Http, Response} from '@angular/http';
import 'rxjs/add/operator/toPromise';

export class Comment {
 author: string;
 comment: string;
 created_at: Date;
}

export class Post {
 _id: number;
 name: string;
 body: string;
 created_at: Date;
 updated_at: Date | null;
 comments: Comment[];
}

@Injectable()
export class BlogService {

    constructor(private http: Http) {
    }

    getPosts(): Promise<Post[]> {
        return this.http
            .get('/api/blog')
            .toPromise()
            .then((response) => {
                return response.json().posts as Post[];
            })
            .catch(this.handleError);
    }

    getPost(postId): Promise<Post> {
        return this.http
            .get('/api/blog/' + postId)
            .toPromise()
            .then(response => {
                if (response.json().posts.length === 0) {
                    throw response.json().message;
                }

                return response.json().posts[0] as Post;
            })
            .catch(this.handleError);
    }

    savePost(id, data): Promise<Post> {
        let promise: Promise<Response>;
        delete data._id;
        if (id) {
            promise = this.http.put('/api/blog/' + id, data)
                .toPromise();
        } else {
            promise = this.http.post('/api/blog', data)
                .toPromise();
        }
        return promise
            .then(response => {
                if (response.json().posts.length === 0) {
                    throw response.json().message;
                }

                return response.json().posts[0] as Post;
            })
            .catch(this.handleError);
    }

    removePost(id): Promise<Response> {
        return this.http.delete('/api/blog/' + id)
            .toPromise()
            .catch(this.handleError);
    }

    private handleError(error: Response | any) {
        console.error('An error occurred', error);
        return Promise.reject(error.message || error);
    }

}

3. The components

We will need to add 5 new components to our application. One to list all posts, another to view one post, another to edit a post, another to create a new post and a final one to show 404 page not found. Let’s do that

ng generate component blog/add &&
ng generate component blog/edit &&
ng generate component blog/list &&
ng generate component blog/view &&
ng generate page-not-found

The first component we will be working on is the list component, this is our index of posts, the entry point of our blog. But to do this we first need to define routes our routes. Remember when we defined the wildcard route back in express? That made sure that every non-API URL that we call will be handled by the Angular routes.

Edit the app.module.js file inside the src folder. You will notice that angular-cli already imported your new components and added them to the declarations.

Let’s define our routes. Import the RouterModule to your project

import {RouterModule, Routes} from '@angular/router';

And then define your routes array

const appRoutes: Routes = [
    {path: '', redirectTo: '/blog', pathMatch: 'full'},
    {path: 'blog', component: ListComponent},
    {path: 'post/:id', component: ViewComponent},
    {path: 'post/:id/edit', component: EditComponent},
    {path: 'post', component: AddComponent},
    {path: '**', component: PageNotFoundComponent}
];

You might have realised that we redirect “/” to “/blog” and that there is a “**” route which is basically a 404 catchall route. Now tell you application to use the router module with your routes. by adding it to the app imports

RouterModule.forRoot(appRoutes)

Now that we have our routes defined, Angular will know which component to load in which route. We sill need to modify the main component template to include the router outlet (outlet is what you call component XML type tags)

<h1>
  {{title}}
</h1>
<a class="small" [routerLink]="['/blog']">View all posts</a>
<a class="small" [routerLink]="['/post']">Add post</a>

<router-outlet></router-outlet>

Remember the Post and Comment classes? Let’s move them outside the Blog service. Create two files inside “src/blog”; “post.ts” and “comment.ts”. Past the Comment class inside “src/blog/comment.ts” and the Post class inside “src/blog/post.ts” now include Comment inside Post and Post inside the Blog service.

This is how “src/blog/comment.ts” should look like

export class Comment {
    _id: number;
    comment: string;
    created_at: Date;
}

And “src/blog/post.ts”

import {Comment} from './comment';

export class Post {
    _id: number;
    name: string;
    body: string;
    created_at: Date;
    updated_at: Date | null;
    comments: Comment[];
}

And the final version of the Blog service

import {Injectable} from '@angular/core';
import {Http, Response} from '@angular/http';
import 'rxjs/add/operator/toPromise';
import {Post} from './blog/post';

@Injectable()
export class BlogService {

    constructor(private http: Http) {
    }

    getPosts(): Promise<Post[]> {
        return this.http
            .get('/api/blog')
            .toPromise()
            .then((response) => {
                return response.json().posts as Post[];
            })
            .catch(this.handleError);
    }

    getPost(postId): Promise<Post> {
        return this.http
            .get('/api/blog/' + postId)
            .toPromise()
            .then(response => {
                if (response.json().posts.length === 0) {
                    throw response.json().message;
                }

                return response.json().posts[0] as Post;
            })
            .catch(this.handleError);
    }

    savePost(id, data): Promise<Post> {
        let promise: Promise<Response>;
        delete data._id;
        if (id) {
            promise = this.http.put('/api/blog/' + id, data)
                .toPromise();
        } else {
            promise = this.http.post('/api/blog', data)
                .toPromise();
        }
        return promise
            .then(response => {
                if (response.json().posts.length === 0) {
                    throw response.json().message;
                }

                return response.json().posts[0] as Post;
            })
            .catch(this.handleError);
    }

    removePost(id): Promise<Response> {
        return this.http.delete('/api/blog/' + id)
            .toPromise()
            .catch(this.handleError);
    }

    private handleError(error: Response | any) {
        console.error('An error occurred', error);
        return Promise.reject(error.message || error);
    }

}

With the Post class now reusable, we can build the list component. We will use the Blog service that we created before. Like we did with the Http class, we will inject the blog service using Angular DI

constructor(private heroService: BlogService) {}

In this case, because it’s our custom service, we also need to tell the component how to load the injected class. So we need to add the BlogService to the providers.

You might have noticed that angular-cli automatically created a method named ngOnInit. Angular is an event-driven framework, I won’t get into much detail about it, but what it means is that throughout its life cycle a component goes through several states and each state triggers an event. ngOnInit is triggered when the component is ready to use and that is when we will request our posts.

posts: Post[];
constructor(private heroService: BlogService) {}

ngOnInit() {
    this.getPosts();
}

getPosts() {
    this.heroService.getPosts()
        .then(posts => {
            this.posts = posts;
        });
}

Basically the getPosts() method call’s the Blog service getPosts() method and handles the resolution by assigning the result to a property of type array of Post that we also declared.

Now, all we need to do is load everything on the view. A simple *ngFor should do it.

<div *ngFor="let post of posts">
    <h2><a [routerLink]="['/post', post._id]">{{post.name}}</a></h2>
    <div>
        <div>{{post.substr(1, 150)}}</div>
        <footer>- By {{post.author || 'Unknown'}} @ {{post.created_at.toLocaleString()}}</footer>
    </div>
    <div>
        <a [routerLink]="['/post', post._id]">View</a>
        <a [routerLink]="['/post', post._id, 'edit']">Edit</a>
    </div>
</div>

We use the RouterLink property to define the href of every link.

The next component we’ll build is the view component. It’s very similar to the list component, the template is very straightforward

<div *ngIf="post">
    <h2>{{post.name}}</h2>
    <div class="row">
        <div class="col-xs-12">
            {{post.body}}
            <div>
                <a [routerLink]="['/post', post._id, 'edit']">Edit</a>
                <a [routerLink]="['/blog']">Back to list</a>
            </div>
        </div>
    </div>
</div>

And like the list component, we also inject some libraries

constructor(protected blogService: BlogService,
                protected activatedRouter: ActivatedRoute,
                protected router: Router, protected location: Location) {}

I’ve compiled it all up in a GitHub repository. Feel free to clone it and play around with. I’ll leave the URL in the end.

The edit component is slightly different, this time, instead of leaving the component class declaration as was created by angular-cli, we’re going to change to extend the view method. This way we’ll be able to reuse all that logic in a very OO manner.

@Component({
    selector: 'app-edit',
    templateUrl: './edit.component.html',
    styleUrls: ['./edit.component.css'],
    providers: [BlogService]
})
export class EditComponent extends ViewComponent {
    @Input() post: Post;
    status = '';
}

Notice that we no longer have a constructor or a ngOnInit method, these are inherited from the ViewComponent. We have however redeclared the post variable and made it an input decorator. This will allow us to bind the post to a form in the view. We also added a status property that will provide feedback to the user.

We now can modify the view and create the form.

<div *ngIf="post" class="row">
    <div class="col-xs-12">
        <h2>{{post.id ? 'Edit "'+post.name+'"' : 'Add post'}}</h2>
        <strong *ngIf="status">{{status}}</strong>
        <form #postForm="ngForm" (ngSubmit)="save()">
            <div class="form-group">
                <label for="name">Name:</label>
                <input type="text" class="form-control" id="name"
                        required
                        [(ngModel)]="post.name" name="name"
                        #name="ngModel"
                />
            </div>
            <div class="form-group">
                <label for="body">Body:</label>
                <textarea class="form-control" id="body"
                        required
                        [(ngModel)]="post.body" name="body"
                        rows="10"
                        #name="ngModel"
                ></textarea>
            </div>
            <button type="submit" class="btn btn-primary" [disabled]="!postForm.form.valid">Save</button>
            <button type="button" *ngIf="post._id" (click)="remove()" class="btn btn-danger pull-right">Delete</button>
        </form>
    </div>
</div>

You might have noticed that the ngSubmit attribute in the form and the click attributes in the buttons call a function. These are the bindings that will allow us to perform tasks with our posts. Let’s write those:

@Component({
    selector: 'app-edit',
    templateUrl: './edit.component.html',
    styleUrls: ['./edit.component.css'],
    providers: [BlogService]
})
export class EditComponent extends ViewComponent {
    @Input() post: Post;
    status = '';

    save(): void {
        this.status = 'Saving...';
        this.blogService.savePost(this.post._id, this.post)
            .then((post) => {
                this.post = post;
                this.status = 'saved';
                this.location.replaceState('/post/' + post._id + '/edit');
                setTimeout(() => this.status = '', 3000);
            })
            .catch(err => {
                alert(err);
            });
    }

    remove(): void {
        if (confirm('Are you sure you want to delete this item?')) {
            this.status = 'Deleting...';
            this.blogService.removePost(this.post._id)
                .then(() => {
                    this.router.navigateByUrl('/blog');
                });
        }
    }

    cancel(): void {
        this.router.navigateByUrl('/blog');
    }

}

Once again, pretty much all they do is call the Blog service and handle the promises.

We can now, list all posts, view a specific post and edit that post. All we need now is the ability to create a post from scratch.

Let’s do that by editing the add component.

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

import {Post} from '../post';
import {EditComponent} from '../edit/edit.component';
import {BlogService} from '../../blog.service';

@Component({
    selector: 'app-add',
    templateUrl: '../edit/edit.component.html',
    styleUrls: ['../edit/edit.component.css'],
    providers: [BlogService]
})
export class AddComponent extends EditComponent {

    ngOnInit() {
        this.post = new Post;
    }

}

That’s it. Really it’s just that. Because we are extending the ViewComponent, all we need is to override the ngOnInit method and tell it to initiate and empty Post object instead of retrieving one post from the API. We also told this component to use the edit component template and CSS as they are the same.

After it’s all pieced together you can go to your favourite browser navigate to http://server-host-or-ip:3000 and you should see your new blog.

4. What about comments?

We are using MongoDB as our database engine. Because of the nature of MongoDB, it is best to store feedback as a sub-collection of a post. This means that adding comments to the database is a simple as adding the comment to the post objects and then storing it. I’m not going into detail on how to do it, instead, I’ll let you explore the GitHub repository that I’ve created for this tutorial.

I initially planned this tutorial to be two parts. It turned out to be 3 part and I have this nagging voice telling me that I missed something. In any case, hope you enjoyed it and feel free to give your opinion.

Categories: Tutorials

Tagged as:

Claudio Pinto

Leave a Reply

Your email address will not be published. Required fields are marked *