How to Create MEAN Stack Applications Smoothly (Part 3)
Konrad JaguszewskiReading Time: 6 minutes
Adding an Admin Panel
Parts of the series:
- Introduction to the MEAN stack
- Building a frontend in Angular
- Adding an Admin Panel (you are here)
Hello again! Thanks for following this series of tutorials. I bet you are doing a great job with creating your blog! We’re getting closer to the finish. Readers of our blog are able to read the posts, so our main goal (to share the greatest thoughts with the world) is accomplished. For now managing the posts is very hard and time-consuming. Have you ever had the best idea ever (like building a huge elevator which would connect Earth with Mars to make a colonization super fast, cheap and safe, and compete with SpaceX), but forgot about it, because your blog’s posts creation wasn’t easy enough? It happens to me all the time, too! Or have you ever had an idea so stupid (like round Earth theory) that you had to actually remove your post to not embarrass yourself? Well, it has never happened to me, but there is a first time for everything, so it’s better to be prepared. Anyway, this is the third part of the series and today we will add a way to create, edit and delete our posts in a blink of an eye!
Setup
We will start with a small refactoring, because like an old saying says - “To make things right you have to break them first” (actually, I just made that up, but it sounds reasonable, so maybe someone said that in the past).
I want the admin panel to look different than the rest of the blog, so we will need to change things a little. Let’s create a new component:
$ ng generate component components/base
This will be the base for the frontend side which we’ve created in the previous part. Now we have to populate it with the current content of the AppComponent
:
<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>
import { Component } from '@angular/core'; import { PageTitleService } from '../../services/page-title.service'; @Component({ selector: 'app-base', templateUrl: './base.component.html', }) export class BaseComponent { title = 'MEAN Blog'; constructor( private pageTitleService: PageTitleService ) { pageTitleService.title .subscribe(title => { this.title = title; }); } }
We can’t forget about cleaning the AppComponent
:
<router-outlet></router-outlet>
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent {}
Let’s add a link to the Admin Panel in 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> <ul class="navbar-nav ml-auto"> <li class="nav-item"> <a class="nav-link" routerLink="/admin">Admin Panel</a> </li> </ul> </div> </nav>
We should also change the router to match the new structure. Replace the appRoutes
in the AppModule
with following code:
const appRoutes: Routes = [ { path: '', component: BaseComponent, children: [ { path: '', component: PostsListComponent }, { path: 'post/:id', component: PostDetailsComponent } ] } ];
Now the whole app should render in the AppComponent
and the blog part should render in the BaseComponent
.
Creating a new module
It’s time for the admin panel! We will create a separate module for that. And once more Angular CLI comes in handy:
$ ng generate module Admin --routing
This way we will have the admin area code nicely separated from the rest of the app, but we’re still going to reuse some of the code. In case of small applications one module may be enough, but in case of bigger apps it may be a good idea move different parts into separate modules. It makes the code easier to read, maintain and scale.
While we are still in the terminal, let’s generate a bunch of components:
$ ng generate component admin/admin --module=admin $ ng generate component admin/admin-dashboard --module=admin $ ng generate component admin/admin-header --module=admin $ ng generate component admin/admin-post-create --module=admin $ ng generate component admin/admin-post-edit --module=admin $ ng generate component admin/admin-post-form --module=admin $ ng generate component admin/admin-posts --module=admin $ ng generate component admin/admin-posts-list --module=admin
That’s a lot of components! I promise we won’t create more components in this part of the tutorial. We have the boilerplates for our new module generated and we can go straight to coding.
As the first thing we will write the routes for the admin panel. We already know how the router works from the previous part, so here’s how the src/app/admin/admin-routing.module.ts
file should look:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AdminComponent } from './admin/admin.component'; import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component'; import { AdminPostEditComponent } from './admin-post-edit/admin-post-edit.component'; import { AdminPostCreateComponent } from './admin-post-create/admin-post-create.component'; import { AdminPostsComponent } from './admin-posts/admin-posts.component'; const routes: Routes = [ { path: 'admin', component: AdminComponent, children: [ { path: '', children: [ { path: '', component: AdminDashboardComponent }, { path: 'posts', component: AdminPostsComponent }, { path: 'post/edit/:id', component: AdminPostEditComponent }, { path: 'post/create', component: AdminPostCreateComponent } ] } ] } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class AdminRoutingModule { }
Rendering and deleting the posts
We have our routes defined, but we don’t have a place to render them. So let’s go to the AdminComponent
. This is our root component for the whole admin area. The layout is very simple and clean:
<app-admin-header></app-admin-header> <div class="container"> <div class="row"> <div class="col-sm-12 py-5"> <router-outlet></router-outlet> </div> </div> </div>
Nothing unusual. Just a header with a content container. This leads us to the next component in line - AdminHeader
. It’s a typical Bootstrap navbar, very similar to what we already have:
<nav class="navbar navbar-expand-lg navbar-light bg-light"> <a routerLink="/admin" class="navbar-brand">Admin Panel</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="/admin/posts" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Edit posts</a> </li> </ul> <ul class="navbar-nav ml-auto"> <li class="nav-item"> <a class="nav-link" routerLink="/">Return to the blog</a> </li> </ul> </div> </nav>
Let’s take care of the AdminDashboard
component. We will show last 5 posts there and a link to the post creation page. Here’s the template:
<h1>Admin Panel - Dashboard</h1> <p class="mb-5">Showing the last 5 posts.</p> <a routerLink="/admin/post/create" class="btn btn-success mb-4">Add new post</a> <app-admin-posts-list [limit]="5"></app-admin-posts-list>
It renders the AdminPostsList
component with a property limit
, so we need to write it next. This component will be a little more interesting than the previous three. Besides the template we’ll also write fetching and deleting the posts. But if you look closely at the PostsService
, you will see that we don’t have a delete
method there. We have to add it to the service:
public deletePost(id: string): Observable<Post> { return this.http .delete(<code>${this.postsUrl}/${id}</code>) .map(response => response['data'] as Post); }
Thanks to the HttpClient the code is very simple. We are calling the DELETE
method on our API and returning the response data as Post. That’s all we need for deleting posts!
Let’s go back to the list of posts. We’ll start with some TypeScript code:
import { Component, Input, OnInit } from '@angular/core'; import Post from '../../models/post.model'; import { PostService } from '../../services/post.service'; @Component({ selector: 'app-admin-posts-list', templateUrl: './admin-posts-list.component.html' }) export class AdminPostsListComponent implements OnInit { @Input() limit = 0; postsList: Post[]; constructor( private postService: PostService ) {} ngOnInit() { this.postService.getPosts() .subscribe((posts: Post[]) => { this.postsList = posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); if (this.limit > 0) { this.postsList = this.postsList.slice(0, this.limit); } }); } onDeleteClick(id) { this.postService.deletePost(id) .subscribe(() => { this.postsList = this.postsList.filter((post) => post._id !== id); }); } }
On the top we are importing everything we’ll need. Then in the class definition we have the component’s properties defined. @Input()
next to the limit
means that this property can be set from the parent component. In the constructor
we are injecting the PostService
, retrieving all the posts, sorting them by date and limiting accordingly to the property. In the onDeleteClick
method we are deleting a post by its id - we will call it whenever an user clicks a “delete” button. So let’s move to the template now.
<table class="table"> <thead> <tr> <th>Title</th> <th>Content</th> <th>Author</th> <th>Published</th> <th>Created At</th> <th>Actions</th> </tr> </thead> <tbody> <tr *ngFor="let post of postsList"> <td>{{post.title}}</td> <td>{{post.body | trim:5}}</td> <td>{{post.author}}</td> <td>{{post.isPublished}}</td> <td>{{post.date | date}}</td> <td> <a routerLink="/admin/post/edit/{{post._id}}" class="btn btn-primary">Edit</a> <button (click)="onDeleteClick(post._id)" type="button" class="btn btn-danger">Delete</button> </td> </tr> </tbody> </table>
This is a table with our posts. ngFor
is going through the list of posts and renders each one in a separate row. The last column contains a button for removing it and a link leading to a post edition page.
In the column containing post’s body you can notice that we are using the Trim
pipe which was created in the previous part of the tutorial. Unfortunately it will not work in the Admin Module immediately. To make it work we will create a shared module. Then we’ll be able to reuse the pipe in our AdminModule
and AppModule
. First let’s move the Trim
pipe from src/app/utils/trim.pipe.ts
to src/app/pipes/trim/trim.pipe.ts
. After that we need to write a module for the pipe. We’ll call it PipesModule
(because it’s a module for pipes!):
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { TrimPipe } from './trim/trim.pipe'; @NgModule({ imports: [ CommonModule ], declarations: [ TrimPipe ], exports: [ TrimPipe ] }) export class PipesModule { }
And that’s the whole file! Now we just need to include it in other modules. Let’s import it in the src/app/app.module.ts
and src/app/app.module.ts
files:
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 { AdminModule } from './admin/admin.module'; import { BaseComponent } from './components/base/base.component'; import { PipesModule } from './pipes/pipes.module'; const appRoutes: Routes = [ { path: '', component: BaseComponent, children: [ { path: '', component: PostsListComponent }, { path: 'post/:id', component: PostDetailsComponent } ] } ]; @NgModule({ declarations: [ AppComponent, PostsListComponent, HeaderComponent, BlogDescriptionComponent, PostDetailsComponent, BaseComponent, ], imports: [ BrowserModule, HttpClientModule, AdminModule, PipesModule, RouterModule.forRoot(appRoutes) ], providers: [ PostService, PageTitleService ], bootstrap: [AppComponent] }) export class AppModule { }
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AdminRoutingModule } from './admin-routing.module'; import { AdminComponent } from './admin/admin.component'; import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component'; import { AdminHeaderComponent } from './admin-header/admin-header.component'; import { PipesModule } from '../pipes/pipes.module'; import { AdminPostEditComponent } from './admin-post-edit/admin-post-edit.component'; import { AdminPostCreateComponent } from './admin-post-create/admin-post-create.component'; import { AdminPostFormComponent } from './admin-post-form/admin-post-form.component'; import { AdminPostsListComponent } from './admin-posts-list/admin-posts-list.component'; import { AdminPostsComponent } from './admin-posts/admin-posts.component'; @NgModule({ imports: [ CommonModule, AdminRoutingModule, PipesModule ], declarations: [ AdminComponent, AdminDashboardComponent, AdminHeaderComponent, AdminPostEditComponent, AdminPostCreateComponent, AdminPostFormComponent, AdminPostsListComponent, AdminPostsComponent, ], bootstrap: [AdminComponent] }) export class AdminModule { }
If everything went fine, the app should compile and we should be able to access the Admin Panel without problems. We can now test our new and shiny delete functionality. So go ahead and delete all the posts! You did? Cool, that’s it for this part. In the next one we will implement post creation form so you will be able to write new texts. See you in a couple of months!
No, I’m just kidding. We will do it right now!
Creating new posts
First we need to create a new method in the PostService
:
public addPost(post: Post): Observable<Post> { return this.http .post<Post>(this.postsUrl, { data: post }); }
Simple and elegant! Just a POST
request to the API with data.
We also need to import the FormsModule
for the post form:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AdminRoutingModule } from './admin-routing.module'; import { AdminComponent } from './admin/admin.component'; import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component'; import { AdminHeaderComponent } from './admin-header/admin-header.component'; import { PipesModule } from '../pipes/pipes.module'; import { AdminPostEditComponent } from './admin-post-edit/admin-post-edit.component'; import { AdminPostCreateComponent } from './admin-post-create/admin-post-create.component'; import { AdminPostFormComponent } from './admin-post-form/admin-post-form.component'; import { AdminPostsListComponent } from './admin-posts-list/admin-posts-list.component'; import { AdminPostsComponent } from './admin-posts/admin-posts.component'; @NgModule({ imports: [ CommonModule, AdminRoutingModule, PipesModule, FormsModule ], declarations: [ AdminComponent, AdminDashboardComponent, AdminHeaderComponent, AdminPostEditComponent, AdminPostCreateComponent, AdminPostFormComponent, AdminPostsListComponent, AdminPostsComponent, ], bootstrap: [AdminComponent] }) export class AdminModule { }
We can now proceed with the form. We will start in the src/app/admin/admin-post-form/admin-post-form.component.ts
file:
import { Component, Input } from '@angular/core'; import { Location } from '@angular/common'; import Post from '../../models/post.model'; import { PostService } from '../../services/post.service'; @Component({ selector: 'app-admin-post-form', templateUrl: './admin-post-form.component.html' }) export class AdminPostFormComponent { @Input() post: Post = { _id: '', title: '', body: '', author: '', date: new Date(), isPublished: false }; constructor( private postService: PostService, private location: Location ) { } onSubmit() { this.postService.addPost(this.post).subscribe(() => { this.location.back(); }); } onCancelClick() { this.location.back(); } }
We are declaring a post
property here with its initial values. Then we are injecting the PostService
and Location
service to interact with a browser’s URL. The onSubmit
method calls the addPost
method of the PostService
saving a new post in the database and redirecting an user to the previous page. The last method just moves user to the last visited page without saving the post.
We can now populate the related template file:
<form *ngIf="post" (ngSubmit)="onSubmit()"> <div class="form-group"> <label for="title">Title</label> <input class="form-control" type="text" name="title" id="title" placeholder="Title" [(ngModel)]="post.title"> </div> <div class="form-group"> <label for="content">Content</label> <textarea name="content" id="content" rows="10" class="form-control" [(ngModel)]="post.body"></textarea> </div> <div class="form-group"> <label for="author">Author</label> <input type="text" name="author" id="author" class="form-control" placeholder="Author" [(ngModel)]="post.author"> </div> <div class="form-group form-check"> <input type="checkbox" name="published" id="published" class="form-check-input" [(ngModel)]="post.isPublished"> <label for="published" class="form-check-label">Published</label> </div> <button type="submit" class="btn btn-primary">Save</button> <button type="button" (click)="onCancelClick()" class="btn btn-secondary">Cancel</button> </form>
A lot of Bootstrap’s classes here. The important parts are (ngSubmit)
, inputs with [(ngModel)]
and the “cancel” button. The “cancel” button is fairly simple - it just calls the onCancelClick
method on click. On the other hand the onSubmit
method is called when the form is submitted. The ngModel
thing is a two-way data binding. Whenever we change something in one of the inputs the related post’s property will be updated. It will also help in the near future, when we add the ability to edit the posts - the inputs are going to be filled with a requested post’s data.
Let’s not forget about including the form in the AdminPostCreate
component:
<h1 class="mb-5">Admin Panel - Create post</h1> <app-admin-post-form></app-admin-post-form>
We can now create and delete the posts on our blog! How awesome is that? The only thing left is editing them. We will use the AdminPostForm
for this. We just need to modify it a little.
Editing the existing posts
First we need to add a new method to the PostService
:
public editPost(id: string, post: Post): Observable<Post> { return this.http .put<Post>(<code>${this.postsUrl}/${id}</code>, { data: post }); }
It will send a PUT
request to our API and update the requested post.
Now let’s change the AdminPostForm
component to fulfill our needs. We just need to add a couple lines of code. To have a distinction between the “edit” form and “create” form we will declare a new property called isEditing
, right below the post
property:
@Input() isEditing = false;
Then on the top of the onSubmit
we’ll check it and call the proper method of the PostService
:
if (this.isEditing) { this.postService.editPost(this.post._id, this.post).subscribe(() => { this.location.back(); }); return; }
The AdminPostEdit
component should look like this:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { PostService } from '../../services/post.service'; import Post from '../../models/post.model'; @Component({ selector: 'app-admin-post-edit', templateUrl: './admin-post-edit.component.html', }) export class AdminPostEditComponent implements OnInit { post: Post; originalTitle = ''; constructor( private route: ActivatedRoute, private postService: PostService ) {} ngOnInit() { const id = this.route.snapshot.params['id']; this.postService.getPostById(id) .subscribe((post: Post) => { this.post = post; this.originalTitle = post.title; }); } }
The ActivatedRoute
service helps us with getting the id
parameter from the route. The reason for the originalTitle
property is that I would like to have the original title of the post in the template, not binded with the post.title
. The template is straightforward:
<h1 class="mb-5">Admin Panel - Editing post "{{ originalTitle }}"</h1> <app-admin-post-form [post]="post" [isEditing]="true"></app-admin-post-form>
And that’s it! We’ve covered the whole Admin Panel in this part. Our blog is great! Users can read posts and we can write anything we want! But wait… Right now everyone can access our sweet Admin Panel and delete everything they want! That’s not good for sure! We will need to take care of that in the following part. For now enjoy your blog, write some posts, add new features. You can find the complete code from this part here. If you need any help, feel free to drop a comment under this article.
Thanks for following the tutorial and see you in the next part!