A picture of Konrad

How to Create MEAN Stack Applications Smoothly (Part 3)

Adding an Admin Panel

Parts of the series:

  1. Introduction to the MEAN stack
  2. Building a frontend in Angular
  3. 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).

2x2 Meme with Konrad with the same image of Konrad in four parts of the image. Text on these images reads That's a very nice app you have there. It would be a shame. If someone. Broke it.

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!

FacebookTwitterPinterest

Konrad Jaguszewski

Front-end Developer

I'm a self-taught web developer. Since childhood I was amazed by computers. I love creating and learning new stuff, be that programming, cooking, playing guitar or doing crafts.