Firebase Angular Web Frameworks Codelab

1. What you'll create

In this codelab, you'll be building a traveling blog with a real-time collaborative map with the latest from our Angular library: AngularFire. The final web app will consist of a travel blog where you can upload images to each location that you've traveled to.

AngularFire will be used to build the web app, Emulator Suite for local testing, Authentication to keep track of user data, Firestore and Storage to persist data and media, powered by Cloud Functions, and finally, Firebase Hosting to deploy the app.

What you'll learn

  • How to develop with Firebase products locally with Emulator Suite
  • How to enhance your web app with AngularFire
  • How to persist your data in Firestore
  • How to persist media in Storage
  • How to deploy your app to Firebase Hosting
  • How to use Cloud Functions to interact with your databases and APIs

What you'll need

  • Node.js version 10 or higher
  • A Google Account for the creation and management of your Firebase Project
  • The Firebase CLI version 11.14.2 or later
  • A browser of your choice, such as Chrome
  • Basic understanding of Angular and Javascript

2. Get the sample code

Clone the codelab's GitHub repository from the command line:

git clone https://github.com/firebase/codelab-friendlychat-web

Alternatively, if you do not have git installed, you can download the repository as a ZIP file.

The Github repository contains sample projects for multiple platforms.

This codelab only uses the webframework repository:

  • 📁 webframework: The starting code that you'll build upon during this codelab.

Install dependencies

After cloning, install dependencies in the root and functions folder before building the web app.

cd webframework && npm install
cd functions && npm install

Install Firebase CLI

Install Firebase CLI using this command in a terminal:

npm install -g firebase-tools

Double check that your Firebase CLI version is greater than 11.14.2 using:

firebase  --version

If your version is lower than 11.14.2, please update using:

npm update firebase-tools

3. Create and set up a Firebase project

Create a Firebase project

  1. Sign in to Firebase.
  2. In the Firebase console, click Add Project, and then name your Firebase project <your-project>. Remember the project ID for your Firebase project.
  3. Click Create Project.

Important: Your Firebase project will be named <your-project>, but Firebase will automatically assign it a unique Project ID in the form <your-project>-1234. This unique identifier is how your project is actually identified (including in the CLI), whereas <your-project> is simply a display name.

The application that we're going to build uses Firebase products that are available for web apps:

  • Firebase Authentication to easily allow your users to sign into your app.
  • Cloud Firestore to save structured data on the cloud and get instant notification when data changes.
  • Cloud Storage for Firebase to save files in the cloud.
  • Firebase Hosting to host and serve your assets.
  • Functions to interact with internal and external APIs.

Some of these products need special configurations or need to be enabled using the Firebase console.

Add a Firebase web app to the project

  1. Click the web icon to create a new Firebase web app.
  2. On the next step, you'll see a configuration object. Copy the contents of this object into the environments/environment.ts file.

Enable Google sign-in for Firebase Authentication

To allow users to sign in to the web app with their Google accounts, we'll use the Google sign-in method.

To enable Google sign-in:

  1. In the Firebase console, locate the Build section in the left panel.
  2. Click Authentication, then click the Sign-in method tab (or click here to go directly there).
  3. Enable the Google sign-in provider, then click Save.
  4. Set the public-facing name of your app to <your-project-name> and choose a Project support email from the dropdown menu.

Enable Cloud Firestore

  1. In the Firebase console's Build section, click Firestore Database.
  2. Click Create database in the Cloud Firestore pane.
  3. Set the location where your Cloud Firestore data is stored. You can leave this as the default or choose a region close to you.

Enable Cloud Storage

The web app uses Cloud Storage for Firebase to store, upload, and share pictures.

  1. In the Firebase console's Build section, click Storage.
  2. If there's no Get Started button, it means that Cloud storage is already

enabled, and you don't need to follow the steps below.

  1. Click Get Started.
  2. Read the disclaimer about security rules for your Firebase project, then click Next.
  3. The Cloud Storage location is preselected with the same region you chose for your Cloud Firestore database. Click Done to complete the setup.

With the default security rules, any authenticated user can write anything to Cloud Storage. We'll make our storage more secure later in this codelab.

4. Connect to your Firebase project

The Firebase command-line interface (CLI) allows you to use Firebase Hosting to serve your web app locally, as well as to deploy your web app to your Firebase project.

Make sure that your command line is accessing your app's local webframework directory.

Connect the web app code to your Firebase project. First, log in to the Firebase CLI in command line:

firebase login

Next run the following command to create a project alias. Replace $YOUR_PROJECT_ID with the ID of your Firebase project.

firebase  use  $YOUR_PROJECT_ID

Add AngularFire

To add AngularFire to the app, run the command:

ng add @angular/fire

Then, follow the command line instructions, and select the features that exists in your Firebase project.

Initialize Firebase

To initialize the Firebase project, run:

firebase init

Then, following the command line prompts, select the features and emulators that were used in your Firebase project.

Start the emulators

From the webframework directory, run the following command to start the emulators:

firebase  emulators:start

Eventually you should see something like this:

$  firebase  emulators:start

i  emulators:  Starting  emulators:  auth,  functions,  firestore,  hosting,  functions

i  firestore:  Firestore  Emulator  logging  to  firestore-debug.log

i  hosting:  Serving  hosting  files  from:  public

✔  hosting:  Local  server:  http://localhost:5000

i  ui:  Emulator  UI  logging  to  ui-debug.log

i  functions:  Watching  "/functions"  for  Cloud  Functions...

✔  functions[updateMap]:  firestore  function  initialized.

  

┌─────────────────────────────────────────────────────────────┐

│  ✔  All  emulators  ready!  It  is  now  safe  to  connect  your  app.  │

│  i  View  Emulator  UI  at  http://localhost:4000  │

└─────────────────────────────────────────────────────────────┘

  

┌────────────────┬────────────────┬─────────────────────────────────┐

│  Emulator  │  Host:Port  │  View  in  Emulator  UI  │

├────────────────┼────────────────┼─────────────────────────────────┤

│  Authentication  │  localhost:9099  │  http://localhost:4000/auth  │

├────────────────┼────────────────┼─────────────────────────────────┤

│  Functions  │  localhost:5001  │  http://localhost:4000/functions  │

├────────────────┼────────────────┼─────────────────────────────────┤

│  Firestore  │  localhost:8080  │  http://localhost:4000/firestore  │

├────────────────┼────────────────┼─────────────────────────────────┤

│  Hosting  │  localhost:5000  │  n/a  │

└────────────────┴────────────────┴─────────────────────────────────┘

Emulator  Hub  running  at  localhost:4400

Other  reserved  ports:  4500

  

Issues?  Report  them  at  https://github.com/firebase/firebase-tools/issues  and  attach  the  *-debug.log  files.

Once you see the ✔All emulators ready! message, the emulators are ready to use.

You should see your travel app's UI, which is not (yet!) functioning:

Now let's get building!

5. Connect the web app to the emulators

Based on the table in the emulator logs, Cloud Firestore emulator is listening on port 8080 and the Authentication emulator is listening on port 9099.

Open the EmulatorUI

In your web browser, navigate to http://127.0.0.1:4000/. You should see the Emulator Suite UI.

Route the app to use the emulators

In src/app/app.module.ts, add the following code to AppModule's list of imports:

@NgModule({
	declarations: [...],
	imports: [
		provideFirebaseApp(() =>  initializeApp(environment.firebase)),

		provideAuth(() => {
			const  auth = getAuth();
			if (location.hostname === 'localhost') {
				connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings:  true });
			}
			return  auth;
		}),

		provideFirestore(() => {
			const  firestore = getFirestore();
			if (location.hostname === 'localhost') {
				connectFirestoreEmulator(firestore, '127.0.0.1', 8080);
			}
			return  firestore;
		}),

		provideFunctions(() => {
			const  functions = getFunctions();
			if (location.hostname === 'localhost') {
				connectFunctionsEmulator(functions, '127.0.0.1', 5001);
			}
			return  functions;
		}),

		provideStorage(() => {
			const  storage = getStorage();
			if (location.hostname === 'localhost') {
				connectStorageEmulator(storage, '127.0.0.1', 5001);
			}
			return  storage;
		}),
		...
	]

The app is now configured to use local emulators, allowing testing and development to be done locally.

6. Adding Authentication

Now that emulators are set up for the app, we can add Authentication features to ensure that each user is signed in before they post messages.

To do so, we can import signin functions directly from AngularFire, and track your user's auth state with the authState function. Modify the login page functions so that the page checks for user auth state on load.

Injecting AngularFire Auth

In src/app/pages/login-page/login-page.component.ts, import Auth from @angular/fire/auth, and inject it into the LoginPageComponent. Authentication providers, such as Google, and functions such assignin, signout can also be directly imported from the same package, and used in the app.

import { Auth, GoogleAuthProvider, signInWithPopup, signOut, user } from  '@angular/fire/auth';

export  class  LoginPageComponent  implements  OnInit {
	private  auth: Auth = inject(Auth);
	private  provider = new  GoogleAuthProvider();
	user$ = user(this.auth);
	constructor() {}  

	ngOnInit(): void {} 

	login() {
		signInWithPopup(this.auth, this.provider).then((result) => {
			const  credential = GoogleAuthProvider.credentialFromResult(result);
			return  credential;
		})
	}

	logout() {
		signOut(this.auth).then(() => {
			console.log('signed out');}).catch((error) => {
				console.log('sign out error: ' + error);
		})
	}
}

Now the login page is functional! Try logging in, and check out the results in the Authentication Emulator.

7. Configuring Firestore

In this step, you'll add functionality to post and update travel blog posts stored in Firestore.

Similar to Authentication, Firestore functions come prepackaged from AngularFire. Each document belongs to a collection, and each document can also have nested collections. Knowing the path of the document in Firestore is required to create and update a travel blog post.

Implementing TravelService

Since many different pages will need to read and update Firestore documents in the web app, we can implement the functions in src/app/services/travel.service.ts, to refrain from repeatedly injecting the same AngularFire functions every page.

Begin with injecting Auth, similar to the previous step, as well as Firestore into our service. Defining an observable user$ object that listens to the current authentication status is also useful.

import { doc, docData, DocumentReference, Firestore, getDoc, setDoc, updateDoc, collection, addDoc, deleteDoc, collectionData, Timestamp } from  "@angular/fire/firestore";

export  class  TravelService {
	firestore: Firestore = inject(Firestore);
	auth: Auth = inject(Auth);
	user$ = authState(this.auth).pipe(filter(user  =>  user !== null), map(user  =>  user!));
	router: Router = inject(Router);

Adding a travel post

Travel posts will exist as documents that are stored in Firestore, and since documents must exist within collections, the collection that contains all travel posts will be named travels. Thus, the path of any travel post will be travels/

Using the addDoc function from AngularFire, an object can be inserted into a collection:

async  addEmptyTravel(userId: String) {
	...
	addDoc(collection(this.firestore, 'travels'), travelData).then((travelRef) => {
		collection(this.firestore, `travels/${travelRef.id}/stops`);
		setDoc(travelRef, {... travelData, id:  travelRef.id})
		this.router.navigate(['edit', `${travelRef.id}`]);
		return  travelRef;

	})
}

Updating and deleting data

Given the uid of any travel post, one can deduce the path of the document stored in Firestore, which can then be read, updated or deleted using AngularFire's updateFoc and deleteDoc functions:

async  updateData(path: string, data: Partial<Travel | Stop>) {
	await  updateDoc(doc(this.firestore, path), data)
}

async  deleteData(path: string) {
	const  ref = doc(this.firestore, path);
	await  deleteDoc(ref)
}

Reading data as an observable

Since travel posts and stops along the way can be modified after creation, it would be more useful to get document objects as observables, to subscribe to any changes that are made. This functionality is offered by the docData and collectionData functions from @angular/fire/firestore.

getDocData(path: string) {
	return  docData(doc(this.firestore, path), {idField:  'id'}) as  Observable<Travel | Stop>
}

  
getCollectionData(path: string) {
	return  collectionData(collection(this.firestore, path), {idField:  'id'}) as  Observable<Travel[] | Stop[]>
}

Adding stops to a travel post

Now that travel post operations are set up, it's time to consider stops, which will exist under a subcollection of a travel post like so: travels//stops/

This is almost identical to creating a travel post, so challenge yourself to implement it on your own, or check out the implementation below:

async  addStop(travelId: string) {
	...
	const  ref = await  addDoc(collection(this.firestore, `travels/${travelId}/stops`), stopData)
	setDoc(ref, {...stopData, id:  ref.id})
}

Nice! The Firestore functions have been implemented in the Travel service, so now you can see them in action.

Using Firestore functions in the app

Navigate to src/app/pages/my-travels/my-travels.component.ts and inject TravelService to use its functions.

travelService = inject(TravelService);
travelsData$: Observable<Travel[]>;
stopsList$!: Observable<Stop[]>;
constructor() {
	this.travelsData$ = this.travelService.getCollectionData(`travels`) as  Observable<Travel[]>
}

TravelService is called in the constructor to get an Observable array of all travels.

In the case where only the travels of the current user is needed, use the query function.

Other methods to ensure security include implementing security rules, or using Cloud Functions with Firestore as explored in optional steps below

Then, simply call the functions implemented in TravelService.

async  createTravel(userId: String) {
	this.travelService.addEmptyTravel(userId);
}

deleteTravel(travelId: String) {
	this.travelService.deleteData(`travels/${travelId}`)
}

Now the My Travels page should be functional! Check out what happens in your Firestore emulator when you create a new travel post.

Then, repeat for the update functions in /src/app/pages/edit-travels/edit-travels.component.ts :

travelService: TravelService = inject(TravelService)
travelId = this.activatedRoute.snapshot.paramMap.get('travelId');
travelData$: Observable<Travel>;
stopsData$: Observable<Stop[]>;

constructor() {
	this.travelData$ = this.travelService.getDocData(`travels/${this.travelId}`) as  Observable<Travel>
	this.stopsData$ = this.travelService.getCollectionData(`travels/${this.travelId}/stops`) as  Observable<Stop[]>
}

updateCurrentTravel(travel: Partial<Travel>) {
	this.travelService.updateData(`travels${this.travelId}`, travel)
}

  

updateCurrentStop(stop: Partial<Stop>) {
	stop.type = stop.type?.toString();
	this.travelService.updateData(`travels${this.travelId}/stops/${stop.id}`, stop)
}

  

addStop() {
	if (!this.travelId) return;
	this.travelService.addStop(this.travelId);
}

deleteStop(stopId: string) {
	if (!this.travelId || !stopId) {
		return;
	}
	this.travelService.deleteData(`travels${this.travelId}/stops/${stopId}`)
	this.stopsData$ = this.travelService.getCollectionData(`travels${this.travelId}/stops`) as  Observable<Stop[]>

}

8. Configuring Storage

You'll now implement Storage to store images and other types of media.

Cloud Firestore is best used to store structured data, such as JSON objects. Cloud Storage is designed to store files or blobs. In this app, you will use it to allow users to share their travel pictures.

Likewise with Firestore, storing and updating files with Storage requires a unique identifier for each file.

Let's implement the functions in TraveService:

Uploading a file

Navigate to src/app/services/travel.service.ts and inject Storage from AngularFire:

export  class  TravelService {
firestore: Firestore = inject(Firestore);
auth: Auth = inject(Auth);
storage: Storage = inject(Storage);

And implement the upload function:

async  uploadToStorage(path: string, input: HTMLInputElement, contentType: any) {
	if (!input.files) return  null
	const  files: FileList = input.files;
		for (let  i = 0; i  <  files.length; i++) {
			const  file = files.item(i);
			if (file) {
				const  imagePath = `${path}/${file.name}`
				const  storageRef = ref(this.storage, imagePath);
				await  uploadBytesResumable(storageRef, file, contentType);
				return  await  getDownloadURL(storageRef);
			}
		}
	return  null;
}

The primary difference between accessing documents from Firestore and files from Cloud Storage is that, although they both follow folder structured paths, the base url and path combination is obtained through the getDownloadURL, which can then be stored, and used in a file.

Using the function in app

Navigate to src/app/components/edit-stop/edit-stop.component.ts and call the upload function using:

	async  uploadFile(file: HTMLInputElement, stop: Partial<Stop>) {
	const  path = `/travels/${this.travelId}/stops/${stop.id}`
	const  url = await  this.travelService.uploadToStorage(path, file, {contentType:  'image/png'});
	stop.image = url ? url : '';
	this.travelService.updateData(path, stop);
}

When the image is uploaded, the media file itself will be uploaded to storage, and the url stored accordingly in the document in Firestore.

9. Deploying the application

Now we're ready to deploy the application!

Copy the firebase configs from src/environments/environment.ts to src/environments/environment.prod.ts and run:

firebase deploy

You should see something like this:

✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.

=== Deploying to 'friendly-travels-b6a4b'...

i  deploying storage, firestore, hosting
i  firebase.storage: checking storage.rules for compilation errors...
✔  firebase.storage: rules file storage.rules compiled successfully
i  firestore: reading indexes from firestore.indexes.json...
i  cloud.firestore: checking firestore.rules for compilation errors...
✔  cloud.firestore: rules file firestore.rules compiled successfully
i  storage: latest version of storage.rules already up to date, skipping upload...
i  firestore: deploying indexes...
i  firestore: latest version of firestore.rules already up to date, skipping upload...
✔  firestore: deployed indexes in firestore.indexes.json successfully for (default) database
i  hosting[friendly-travels-b6a4b]: beginning deploy...
i  hosting[friendly-travels-b6a4b]: found 6 files in .firebase/friendly-travels-b6a4b/hosting
✔  hosting[friendly-travels-b6a4b]: file upload complete
✔  storage: released rules storage.rules to firebase.storage
✔  firestore: released rules firestore.rules to cloud.firestore
i  hosting[friendly-travels-b6a4b]: finalizing version...
✔  hosting[friendly-travels-b6a4b]: version finalized
i  hosting[friendly-travels-b6a4b]: releasing new version...
✔  hosting[friendly-travels-b6a4b]: release complete

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/friendly-travels-b6a4b/overview
Hosting URL: https://friendly-travels-b6a4b.web.app

10. Congratulations!

Now your application should be complete and deployed to Firebase Hosting! All data and analytics will now be accessible in your Firebase Console.

For more features regarding AngularFire, Functions, security rules, don't forget to check out optional steps below, as well as other Firebase Codelabs !

11. Optional: AngularFire auth guards

Along with Firebase Authentication, AngularFire also offers authentication based guards on routes, so that users with insufficient access can be redirected. This helps protect the app from users accessing protected data.

In src/app/app-routing.module.ts, import

import {AuthGuard, redirectLoggedInTo, redirectUnauthorizedTo} from  '@angular/fire/auth-guard'

You can then define functions as to when, and where users should be redirected to on certain pages:

const  redirectUnauthorizedToLogin = () =>  redirectUnauthorizedTo(['signin']);
const  redirectLoggedInToTravels = () =>  redirectLoggedInTo(['my-travels']);

Then simply add them to your routes:

const  routes: Routes = [
	{path:  '', component:  LoginPageComponent, canActivate: [AuthGuard], data: {authGuardPipe:  redirectLoggedInToTravels}},
	{path:  'signin', component:  LoginPageComponent, canActivate: [AuthGuard], data: {authGuardPipe:  redirectLoggedInToTravels}},
	{path:  'my-travels', component:  MyTravelsComponent, canActivate: [AuthGuard], data: {authGuardPipe:  redirectUnauthorizedToLogin}},
	{path:  'edit/:travelId', component:  EditTravelsComponent, canActivate: [AuthGuard], data: {authGuardPipe:  redirectUnauthorizedToLogin}},
];

12. Optional: security rules

Both Firestore and Cloud Storage use security rules (firestore.rules and security.rules respectively) to enforce security and to validate data.

At the moment, the Firestore and Storage data has open access for reads and writes, but you don't want people to go about changing others' posts! You can use security rules to restrict access to your collections and documents.

Firestore rules

To only allow authenticated users to view travel posts, go to firestore.rules file and add:

rules_version  =  '2';
service  cloud.firestore  {
	match  /databases/{database}/travels  {
		allow  read:  if  request.auth.uid  !=  null;
		allow  write:
		if  request.auth.uid  ==  request.resource.data.userId;
	}
}

Security rules can also be used to validate data:

rules_version  =  '2';
service  cloud.firestore  {
	match  /databases/{database}/posts  {
		allow  read:  if  request.auth.uid  !=  null;
		allow  write:
		if  request.auth.uid  ==  request.resource.data.userId;
		&&  "author"  in  request.resource.data
		&&  "text"  in  request.resource.data
		&&  "timestamp"  in  request.resource.data;
	}
}

Storage rules

Similarly, we can use security rules to enforce access to storage databases in storage.rules. Note that we can also use functions for more complex checks:

rules_version  =  '2';

function  isImageBelowMaxSize(maxSizeMB)  {
	return  request.resource.size  <  maxSizeMB  *  1024  *  1024
		&&  request.resource.contentType.matches('image/.*');
}

 service  firebase.storage  {
	match  /b/{bucket}/o  {
		match  /{userId}/{postId}/{filename}  {
			allow  write:  if  request.auth  !=  null
			&&  request.auth.uid  ==  userId  &&  isImageBelowMaxSize(5);
			allow  read;
		}
	}
}