ng generate guard guards/editor-auth
Update the content of the newly generated `src/app/guards/editor-auth.guard.ts`:
import { Injectable } from '@angular/core';
import { CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { AngularFireAuth } from 'angularfire2/auth';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
@Injectable()
export class EditorAuthGuard implements CanActivateChild {
constructor(private afAuth: AngularFireAuth, private router: Router) {}
canActivateChild(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.afAuth.authState
.take(1)
.map(user => {
return !!user;
})
.do(loggedIn => {
if (!loggedIn) {
this.router.navigate(['/auth/login'], { queryParams: { next: state.url } } );
}
});
}
}
In the code snippet above, we define a class `EditorAuthGuard` which implements `CanActivateChild` imported from `@angular/router`. `CanActivateChild` is an interface that a class can implement to be a guard deciding if a child route can be activated.
In the constructor for `EditorAuthGuard` we inject `afAuth` an instance of `angularfire2/auth/AngularFireAuth`, and router an instance of `@angular/router/Router`. The class has a method `canActivateChild` that checks if a user is logged in.
Create Auth Component
In the menu select File > New > Component, enter `/FireBlog/src/app/components/auth` as the source folder and `Auth` as the component name. If you get an error, which reads as more than one modules match, then run the code below in Terminal+:
ng g component components/auth --skip-import
Using the auth component, when a user is logged out, we will display a login button; else we display a log out button. Update the content of `src/app/components/auth/auth.component.html`:
<div *ngIf="afAuth.authState | async as user; else showLogin">
<button (click)="logout()">Logout</button>
</div>
<ng-template #showLogin>
<p>Please login to edit posts.</p>
<button (click)="login()">Login with Google</button>
</ng-template>
Update the content of `src/app/components/auth/auth.component.ts`:
import { Component, OnInit } from '@angular/core';
import { AngularFireAuth } from 'angularfire2/auth';
import {Router, ActivatedRoute } from '@angular/router';
import * as firebase from 'firebase/app';
@Component({
selector: 'app-auth',
templateUrl: './auth.component.html',
styleUrls: ['./auth.component.css']
})
export class AuthComponent implements OnInit {
constructor(public afAuth: AngularFireAuth, private router: Router, private route: ActivatedRoute) { }
ngOnInit() {
}
login() {
this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider()).then((function(router, route) {
return function() {
route.queryParams.subscribe(
data => router.navigate( [data['next']]) );
};
})(this.router, this.route));
}
logout() {
this.afAuth.auth.signOut();
}
}
In the code snippet above, we use methods provided by AngularFire2 to implement Login with Google functionality.
Guard routes for editor components
Now we will use this guard in `src/app/app-routing.module.ts`, the updated content is below:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { EditorAuthGuard } from './guards/editor-auth.guard';
import { PageNotFoundComponent } from './components/page-not-found/page-not-found.component';
import {AuthComponent} from './components/auth/auth.component';
const appRoutes: Routes = [
{
path: 'auth',
children: [
{ path: 'login', component: AuthComponent},
{ path: 'logout', component: AuthComponent}
]
},
{
path: '',
loadChildren: 'app/modules/reader/reader.module#ReaderModule'
},
{
path: 'editor',
canActivateChild: [EditorAuthGuard],
loadChildren: 'app/modules/editor/editor.module#EditorModule'
},
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule],
providers: [EditorAuthGuard]
})
export class AppRoutingModule {
}
In the code snippet above, we have added route rules for the /auth routes, and imported `EditorAuthGuard` and set it as a route guard for the `/editor` routes.
Also update `src/app/app.module.ts`:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AngularFireModule} from 'angularfire2';
import { AngularFirestore } from 'angularfire2/firestore';
import { AngularFireAuthModule } from 'angularfire2/auth';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { PageNotFoundComponent } from './components/page-not-found/page-not-found.component';
import {AuthComponent} from './components/auth/auth.component';
@NgModule({
declarations: [
AppComponent,
PageNotFoundComponent,
AuthComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'fire-blog' }),
AppRoutingModule,
AngularFireModule.initializeApp(environment.firebase, 'fire-blog'),
AngularFireAuthModule,
],
providers: [AngularFirestore],
bootstrap: [AppComponent]
})
export class AppModule { }
Editor Posts View
The route `/editor/posts` should allow us to create a new post and show a list of posts that have been created in the blog. Update the `editor-posts` component to enable this functionality.
<!-- <p>Logged in as {{ user.displayName }}!</p> -->
<h4>Create New Post</h4>
<form class="py-2" #f="ngForm">
<div class="form-group">
<input class="form-control" type="text" name="post-title" [(ngModel)]="postTitle" #new_post_title="ngModel" placeholder="Enter Post Title" (keyup.enter)="onEnter(new_post_title);" required>
</div>
<button class="btn btn-dark" (click)="onEnter()" [disabled]="new_post_title.invalid">Save</button>
</form>
<h3>Posts</h3>
<ul class="list-unstyled">
<li class="mb-3 pb-2" *ngFor="let post of posts | async">
{{ post.data.title }} <a class="btn btn-sm btn-dark" href="/editor/post/{{ post.id }}">Open</a>
</li>
</ul>
<p class="text-right small"><a href="/auth/logout">Logout</a></p>
In this view, we have two sections. The top section provides a form for creating a new post by typing in the title in the input box and hitting Enter. Notice that we bind the keyup.enter event to the component instance method onEnter.
In the lower section, we have the markup to display a list of posts available. If there are no posts available, we would like to display a help message. This help message can be displayed using CSS as in the code below. Update the content of `src/app/modules/editor/components/editor-posts/editor-posts.component.css`:
ul:empty::after {
content: "Create a post Luke"
}
ul li {
border-bottom: 1px solid #000;
}
Using CSS we are displaying the text “Create a post Luke” when the user is yet to create a post, and the list of posts is empty.
Now update the content of `src/app/modules/editor/components/editor-posts/editor-posts.component.ts`:
import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from 'angularfire2/firestore';
import { Observable } from 'rxjs/Observable';
export interface Post {title: string; content: string; }
@Component({
selector: 'app-editor-posts',
templateUrl: './editor-posts.component.html',
styleUrls: ['./editor-posts.component.css']
})
export class EditorPostsComponent implements OnInit {
private postsCollection: AngularFirestoreCollection<Post>;
posts: Observable<any[]>;
postTitle: string;
constructor( private afs: AngularFirestore) {
this.postTitle ='';
this.postsCollection = afs.collection<Post>('posts');
this.posts = this.postsCollection.snapshotChanges()
.map(actions => {
return actions.map(a => {
const data = a.payload.doc.data() as Post;
const id = a.payload.doc.id;
return { id, data };
});
});
}
ngOnInit() {
}
onEnter() {
const post: Post = {title: this.postTitle, content: ''};
var _this = this;
this.addItem(post, (function() {
return function(data) {
_this.postTitle = ''; // empty dom input
alert('New Post Added');
// TODO: Redirect to post with post id
};
})() );
// If post request is successful: clear input; notify user
}
addItem(post: Post, successCb?, errCb?) {
// TODO: Implement loading Animation
this.postsCollection.add(post).then(data => {
successCb(data);
}).catch(err => {
if (errCb) {
errCb(err);
} else {
console.log(err);
}
});
}
}
In the constructor of EditorPostsComponent, we inject `afs` an
instance of AngularFirestore. Instance variables postsCollection and
posts are then assigned values. The onEnter method
takes new_post_title as a parameter, we then create a new Post object which is used as an argument to the addItem method. The addItem method uses the add method of AngularFirestoreCollection to persist the new Post object to the Firestore backend.
Editor Post View
The route `/editor/post` should allow us to edit a post. Update the editor-post component to enable this functionality. The file is located at `src/app/modules/editor/components/editor-post/editor-post.component.html`:
<div>
<div class="form-group">
<label>Title</label>
<input class="form-control" type="text" [(ngModel)]="formModel.title" (change)="update()">
</div>
<div class="form-group">
<label>Content</label>
<textarea class="form-control" [(ngModel)]="formModel.content" (change)="update()"></textarea>
</div>
</div>
In this markup, we have a simple form to edit the title and content of a single post; all changes made are auto-saved. Notice that we bind the `change` event to the `update` method.
Now, update the content of file located at src/app/modules/editor/components/editor-post/editor-post.component.ts
import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreDocument } from 'angularfire2/firestore';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import {Post} from '../editor-posts/editor-posts.component';
@Component({
selector: 'app-editor-post',
templateUrl: './editor-post.component.html',
styleUrls: ['./editor-post.component.css']
})
export class EditorPostComponent implements OnInit {
private postDoc: AngularFirestoreDocument;
formModel: Post;
constructor(private afs: AngularFirestore, private route: ActivatedRoute) {}
update() {
if (typeof this.postDoc !== "undefined") {
// TODO throttle update
this.postDoc.update(this.formModel);
}
}
ngOnInit() {
this.formModel = {title: '', content: ''};
// subscribe to the parameters observable
this.route.paramMap.subscribe(params => {
this.getPost(params.get('id'));
});
}
getPost(postId) {
this.postDoc = this.afs.doc('posts/' + postId);
this.postDoc.valueChanges().subscribe(v => {
this.formModel = v;
});
}
}
The `getPost` method simply gets a post from Firestore using the postId parameter. In `ngOnInit`, we observe `this.route.paramMap` so we can retrieve the post id from the route parameter, after which we call the `getPost` method. The `getPost` method assigns an `AngularFirestoreDocument` to the instance variable `postDoc`. We then observe the document, so that once its value is available, we assign it to the class variable `formModel`.
The update method serves to persist the changes to our post document via the `AngularFirestoreDocument` update method.
Reader Posts View
The route /reader/posts should allow us to read a list of titles of all posts on the blog. Update the reader-posts component to enable this functionality. The file is located at `src/app/modules/reader/components/reader-posts/reader-posts.component.html`:<h3>Posts</h3>
<ul class="list-unstyled">
<li class="mb-3 pb-2" *ngFor="let post of posts | async">
<a href="/post/{{ post.id }}">{{ post.data.title }}</a>
</li>
</ul>
In the constructor `ReaderPostsComponent`, we inject `afs` an instance of `AngularFirestore`, we use the collection method from `AngularFirestore` to create an instance of `AngularFirestoreCollection` stored in the variable `postsCollection`. We then create an observable which will return the array of posts to the posts instance variable.
Reader Post View
The route /reader/post should allow us to read a single post on the blog. Update the reader-post component to enable this functionality. The file is located at `src/app/modules/reader/components/reader-post/reader-post.component.html`:
<h1>{{(post | async)?.title}}</h1>
<p>{{(post | async)?.content}}</p>
Now update the content of `src/app/modules/reader/components/reader-post/reader-post.component.ts`:
import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreDocument } from 'angularfire2/firestore';
import { ActivatedRoute, ParamMap } from '@angular/router';
@Component({
selector: 'app-reader-post',
templateUrl: './reader-post.component.html',
styleUrls: ['./reader-post.component.css']
})
export class ReaderPostComponent implements OnInit {
private postDoc: AngularFirestoreDocument;
post: any;
constructor(private afs: AngularFirestore, private route: ActivatedRoute) { }
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.getPost(params.get('id'));
});
}
getPost(postId) {
this.postDoc = this.afs.doc('posts/' + postId);
this.post = this.postDoc.valueChanges();
}
}
In the constructor, we inject `afs` and route. In the `ngOnInit` method, we observe route and execute the `getPost` method, once the route parameters are available. In the `getPost` method, we get the post to be displayed from Firestore.
Running the app
In the Server window, right click on FireBlog and click the green icon with label `Start Server`.
Live Demo