Up to this point, we have reviewed fundamentals and requirements and set up prerequisites in order to host, store data, and run functions in the cloud. It may sound a bit boring to you, but as we continue to each chapter, it gets more exciting because we will gradually build a real PWA together by adding more features.
Now, it’s time to step into the real world and create an app that works. In this chapter, we are going to implement logics to yield an app that saves personal notes in Firebase. This app will have user authentication functionalities and let a user save, edit, and delete notes in their personal account. We will create UIs and routes, respectively, for each of these functionalities.
Furthermore, there are two goals behind this chapter. First, you will see how we can start an app from scratch and understand how we proceed to convert it to a PWA as we continue to the next chapters. Secondly, you will see how we are going to convert an existing app to a PWA. So, what are we waiting for? Let’s get started.
Implementing Our UI
First, we need to create an app that looks good. What we select for our UI must at least contain the following characteristics: modern, fast, consistent, versatile, flexible,mobile first, responsive, and user friendly. Angular Material1 is one of the best, which perfectly fits in Angular and helps us to rapidly develop our app while it looks nice and fulfills our needs.
Installing and Setting Up Angular Material, CDK, and Animations
Angular CLI 6+ provides a new command ng add in order to update an Angular project with correct dependencies, perform configuration changes, and execute initialization code, if any.
Installing @angular/material Automatically with Angular CLI
We can now use this command to install @angular/material:
added 2 packages from 1 contributor and audited 24256 packages in 7.228s
found 12 vulnerabilities (9 low, 3 high)
run `npm audit fix` to fix them, or `npm audit` for details
Installed packages for tooling via npm.
UPDATE package.json (1445 bytes)
UPDATE angular.json (3942 bytes)
UPDATE src/app/app.module.ts (907 bytes)
UPDATE src/index.html (477 bytes)
UPDATE src/styles.scss (165 bytes)
added 1 package and audited 24258 packages in 7.297s
Awesome – Angular cli has taken care of all configurations for us. However, for a better understanding of how it works in detail, I will also continue to add Angular material to my project manually, as described below.
Installing @angular/material Manually
You can use either NPM or YARN to install packages, so use whichever is most appropriate for your project. I will continue with npm.
AngularFirestoreModule, // needed for database features
AngularFireAuthModule, // needed for auth features,
BrowserAnimationsModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
To enable animation support once packages are installed, BrowserAnimationsModule should be imported.
Fonts and icons help our app look nicer and feel better. Therefore, we will add Roboto and Material Icons fonts into our application. To include them, modify index.html, and add the following links between <head></head>:
Finally, we need to include a theme. There are prebuilt themes in the @angular/material library, which at the time I am writing this book, are the following:
deeppurple-amber.css
indigo-pink.css
pink-bluegrey.css
purple-green.css
Open angular.json, and add one of the theme CSS files to architect ➤ build ➤ styles, so it looks like the following configuration:
Great – we have added what we need for our UI; now let’s create a basic skeleton for our app.
Creating a Core Module / Shared Module
One of the common ways in Angular to benefit from lazy loading and code splitting is to modularize an application while it still keeps its components-based approach. It means that we will encapsulate as many components as make sense into one module and will reuse this module by importing into other modules. To start, we will generate SharedModule to import into all other modules and expose all common components and modules that will be reused across our app and CoreModule, which will only be imported once in our root module, AppModule, and contains all providers that are singletons and will initialize as soon as the application starts.
Run the following commands to generate a core module.
Angular CLI generates CoreModule located in the modules folder. Let’s do this command one more time to generate SharedModule located in themodules folder:
throw new Error(`CoreModule has already been loaded. Import Core modules in the AppModule only.`);
}
}
}
Let’s import CoreModule into AppModule. Now we are ready to start creating our first shared components.
Header, Footer, and Body Components
In this section, we are going to create our first application – a main application layout – based on the simple sketch that is shown in Figure 3-1.
We will continue developing while we have this sketch in mind. To begin, let’s create a module named LayoutModule that contains a footer, header, and menu components and then import this module into AppModule to reuse header/footer in the app.component.ts file.
ng g m modules/layout
import LayoutModule into AppModule:
...imports: [
CoreModule,
LayoutModule,...
By running the following command, footer and header components are generated, respectively.
ng generate component modules/layout/header
ng generate component modules/layout/footer
We have already created SharedModule; however, we need some changes in this module. First, what we imported as share modules or share components should be exported, too. Angular Material is a modular package; with that said, we should import modules that are needed for our UI. Then, I will add as many modules from Angular Material as we need in this application. It will be possible to add or remove modules and components later.
Lastly, our SharedModule looks like the code below:
We need to create pages so that my users can register, log in, and see their profiles. To begin, we create UserModule, including routing:
ng generate module modules/user --routing
As we are going to lazy load this module, we need at least one path and one component. To generate a component, continue running the following command:
ng generate component modules/user/userContainer --flat
flag --flat ignores creating a new folder for this component.
Once the component is generated, we should add it to UserModule declarations and then define our path in UserModuleRouting – path /user could be lazy loaded in AppRoutingModule accordingly.
// UserModuleRouting
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { UserContainerComponent } from './user-container.component';
const routes: Routes = [
{
path: '',
component: UserContainerComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class UserRoutingModule { }
//AppModuleRouting
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
Adding Login, Signup, and Profile UI and Functionalities
Before we continue to add login/signup functionalities, we must activate Sign-in providers in Firebase. Hence, go to your project Firebase console, find Authentication under the develop group on the left menu list, and then move the current tab to Sign-in methods. To keep it simple, we will use Email/Password providers; however, you should be able to add more providers as you wish (see Figure 3-3).
Let’s move on and create an Angular service that handles all Firebase authentication methods. Continue by running the following command:
ng generate service modules/core/firebaseAuthService
We need to write several methods, checking the user login state and doing log in, sign up, and log out.
Take your time and look at Listing 3-1 where we implement FirebaseAuthService in order to invoke necessary methods from AngularFireAuth service and share the state across the app. The service methods are self-explanatory.
export class AuthService {
// expose all data
public authErrorMessages$ = new Subject<string>();
public isLoading$ = new BehaviorSubject<boolean>(true);
Lastly, the application should provide a UI to log in and sign up as well as user information. Going back to our userContainerComponent, we will implement UI and methods respectively. Listings 3-2 through 3-4 show our TypeScript, HTML, and CSS.
export class UserContainerComponent implements OnInit {
public errorMessages$ = this.afAuthService.authErrorMessages$;
public user$ = this.afAuthService.user$;
public isLoading$ = this.afAuthService.isLoading$;
Although what we need to proceed has been achieved, you are not limited and can continue adding more and more Firebase features such as forgot password link, password-less login, and other providers for log in.
In the following section, we are going to work on different views and methods in order to list, add, delete, and update notes in our application; let’s do it step by step.
Set Up Firestore Database
First things first: a quick start to show how to set up our Firestore database.
1.
Open your browser and go to Firebase project console.
2.
In the Database section, click the Get Started or Create database button for Cloud Firestore.
3.
Select Locked mode for your Cloud Firestore Security Rules.4
Below is the Database schema5 that we aim to create in order to store our users and their notes.
----- users // this is a collection
------- [USER IDs] // this is a document
------ notes // this is a collection
----- [NOTE DOCUMENT]
----- [NOTE DOCUMENT]
----- [NOTE DOCUMENT]
------- [USER IDs] // this is a document
------ notes // this is a collection
----- [NOTE DOCUMENT]
----- [NOTE DOCUMENT]
----- [NOTE DOCUMENT]
It is possible to create collections and documents in Firestore manually; but we will do it programmatically later by implementing proper logics in our application (see Figure 3-6).
The last step is to set Firestore rules to require a user unique id (uid) in request in order to give sufficient permission to do create/read/update/delete actions. Click on the Rules tab and copy and paste the following rules (see Figure 3-7).
service cloud.firestore {
match /databases/{database}/documents {
// Make sure the uid of the requesting user matches name of the user
// document. The wildcard expression {userId} makes the userId variable
// available in rules.
match /users/{userId} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
// make sure user can do all action for notes collection if userID is matched
match /notes/{document=**} {
allow create, read, update, delete: if request.auth.uid == userId;
}
}
}
}
List, Add, and Detail Note Views
The next step, once the Firestore setup is done, is to create our components in order to show a list of notes, add a note, and detail the note view along with their relevant functionalities.
To begin, generate a notes module, including routing, by running the following command:
ng generate module modules/notes --routing
Let’s take a look at NotesRoutingModule:
const routes: Routes = [
{
path: "",
component: NotesListComponent
},
{
path: "add",
component: NotesAddComponent
},
{
path: ":id",
component: NoteDetailsComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class NotesRoutingModule {}
As you see, three paths have been defined; therefore we should generate related components by running each command separately:
ng generate component modules/notes/notesList
ng generate component modules/notes/notesAdd
ng generate component modules/notes/noteDetails
Finally, lazy load NotesModule by adding NotesRoutingModule into the AppRoutingModule:
The authentication service is used to log in, log out, and sign up and check if the user has already been authenticated for the application. The credentials were sent to Firebase by calling proper methods on the AngularFire Auth service to perform each function accordingly.
AuthService is required to be injected6 in order to handle the authentication layer in our app:
ng generate service modules/core/auth
The following code shows the logic for AuthService:
// auth.service.ts
interface User {
uid: string;
email: string;
}
@Injectable({
providedIn: "root"
})
export class AuthService {
public authErrorMessages$ = new BehaviorSubject<string>(null);
public isLoading$ = new BehaviorSubject<boolean>(true);
This service contains a standard set of CRUD methods (Create, read, update and delete). Functionalities such as fetching all notes; add, update and delete; and fetch detail note by calling proper methods or requesting from proper APIs. In fact, it acts as an interface between the Angular application and the back-end APIs.
To generate DataService, run the command below:
ng generate service modules/core/data
The following code shows the logic for DataService:
// data.service.ts
interface Note {
id: string;
title: string;
content: string;
}
@Injectable({
providedIn: "root"
})
export class DataService {
protected readonly USERS_COLLECTION = "users";
protected readonly NOTES_COLLECTION = "notes";
public isLoading$ = new BehaviorSubject<boolean>(true);
We have prepared all the service layers and routing that are needed in the application. The rest of the application is to just implement proper UI and components logics for NotesList, NoteAdd, and NoteDetail components (Listings 3-5 through 3-13). Since it’s easy, I would like you to just take a look at the components, and at the end, Figure 3-8 will demonstrate the result.
export class NotesListComponent implements OnInit {
notes$: Observable<Note[]>;
isDbLoading$;
constructor(private db: DataService) {}
ngOnInit() {
this.notes$ = this.db.getNotes();
this.isDbLoading$ = this.db.isLoading$;
}
}
Listing 3-5
// Notes-list.component.ts
<div *ngIf="notes$ | async as notes; else notFound">
If you are comfortable, check out the final code. You will find it in github.com/mhadaily/chapter03/03-note-list-add-edit-update-delete/. Clone the project and navigate to the folder. Then run the following commands:
npm install // to install dependencies
npm start // to run development server
npm run deploy // to deploy to firebase
Summary
The first three chapters’ goal was to reveal PWA fundamentals; tools; and creating an app, step by step, together. It may sound unrelated to PWA; however, as we continue in this book, chapter by chapter, section by section, we will try to make our app progressively better to finally have a great PWA with Angular.
Beginning with the next chapter, we will dive into implementing offline capabilities, caches, push notifications, new modern browsers’ APIs, and more just to create a native-like app for better user experiences on the mobile and web. While this was not possible just a few years ago, these days it’s widely supported in major browsers.