This project is an update of the code found in the video series Angular 2 & Firebase App by Brad Traversy using Angular 5, AngularFire2 (v5), and the Cloud Firestore Database. His original code repo may be found here. Instructions on how to deploy this code to Firebase are also included.
- Angular CLI v1.6.3
- Angular v5.1.2
- Angular Fire 2 v5.0.0-rc.4
- Firebase CLI v3.16.0
- Firebase v4.8.0
The original project by Brad Traversy was made around March 2017 using Angular 2 and Firebase's Realtime Database. Since that time, there have been numerous changes to the software ecosystem with the release of Cloud Firestore in early October 2017 as well as Angular 5 in early November 2017. Hopefully, this repository will help others implement the Firebase Property Listings app using these updated technologies. The below explanations are introduced by category and are not inclusive of all changes to the original code.
The important functionality of this project has been extracted into 3 services, and each service will be covered in detail:
- Firebase Authentication
fireauth.service.ts
- Cloud Firestore
firestore.service.ts
- Cloud Storage
firestorage.service.ts
The authentication service uses AngularFire2 to authenticate users using Google Sign-In. The login and logout functions are shown below. The angular2-flash-messages
package was implemented as promises so that they would display to the user after the completion of the login and logout functions.
// navbar.component.ts and home.component.ts
login() {
this.fireAuth.afLogin();
}
// fireauth.service.ts
afLogin() {
this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider())
.then(() => {
this.flashMessage.show('You are logged in', { cssClass: 'alert-success', timeout: 3000 });
});
}
// navbar.component.ts
logout() {
this.fireAuth.afLogout();
}
// fireauth.service.ts
afLogout() {
firebase.auth().signOut()
.then(() => {
this.flashMessage.show('You are logged out', { cssClass: 'alert-danger', timeout: 3000 });
});
}
Cloud Firestore is a flexible, scalable NoSQL cloud database that is used in this project, replacing the older Realtime Database used in the original video series. The main purpose of using this database is 1. to display all listings and 2. to implement the ability to create, read, update, and delete an individual listing, also known as CRUD functionality. The cloud firestore service uses AngularFire2 to perform these tasks. The image uploading functionality is handled separately by the cloud storage service.
// listings.component.ts
ngOnInit() {
this.fireStore.getListings().subscribe((listings: Listing[]) => {
this.listings = listings;
});
// firestore.service.ts
constructor(private afStore: AngularFirestore) {
this.listingsCollection = afStore.collection<Listing>('listings');
}
getListings(): Observable<Listing[]> {
this.listings = this.listingsCollection.snapshotChanges().map(changes => {
return changes.map(a => {
const data = a.payload.doc.data() as Listing;
data.id = a.payload.doc.id;
return data;
});
});
return this.listings;
// add-listing.component.ts
onAddSubmit() {
this.fireStore.addListing(this.listing);
this.router.navigate(['/listings']);
}
// firestore.service.ts
addListing(listing: Listing) {
this.listingsCollection.add(listing);
}
// listing.component.ts
ngOnInit() {
this.id = this.route.snapshot.params['id'];
this.fireStore.getListingDetails(this.id).subscribe((listing: Listing) => {
this.listing = listing;
});
}
// firestore.service.ts
getListingDetails(id: string): Observable<Listing> {
this.listingDoc = this.afStore.doc<Listing>(`listings/${id}`);
this.listing = this.listingDoc.valueChanges();
return this.listing;
}
// edit-listing.component.ts
onEditSubmit(listing: Listing) {
this.fireStore.updateListing(this.id, listing);
this.router.navigate(['/listings']);
}
// firestore.service.ts
updateListing(id: string, listing: Listing) {
this.listingDoc = this.afStore.doc(`listings/${id}`);
this.listingDoc.update(listing);
}
// listing.component.ts
onDeleteClick() {
this.fireStore.deleteListing(this.id);
this.fireStorage.deleteUpload(this.listing); // handled by the cloud storage service
}
// firestore.service.ts
deleteListing(id: string) {
this.listingDoc = this.afStore.doc(`listings/${id}`);
this.listingDoc.delete();
}
Cloud Storage for Firebase used to store and serve user-generated files such as documents, images, and videos. Unfortunately, this feature has not yet been modularized by AngularFire2 so it has to be implemented using the original Firebase code found in their documentation. The code from Angular Firestarter by codediodeio was also helpful to create the upload function. The difficulty with using Cloud Storage with Cloud Firestore is that both of these features are separate, and therefore the code is de-coupled. Because of this, it was challenging to use both services in unison to create a listing with both text properties (stored in Cloud Firestore) and an associated image (stored in Cloud Storage). The solution I came up with using setTimeout()
is far from perfect, but hopefully can be improved upon in the future. In this project, an image can be uploaded, viewed, and deleted for each listing.
<!-- add-listing.component.html -->
<label>Image<sup class="red">*</sup></label><br>
<input type="file" (change)="detectFiles($event)">
<a class="btn btn-primary" routerLink="/add-listing" (click)="onUploadImage()">Upload</a>
// add-listing.component.ts
detectFiles($event: Event) {
this.selectedFiles = ($event.target as HTMLInputElement).files;
}
onUploadImage() {
const file = this.selectedFiles;
if (file && file.length === 1) {
this.currentUpload = new Upload(file.item(0));
this.fireStorage.pushUpload(this.currentUpload); // File uploaded to cloud storage here
this.errorSwitch = false;
// upload.name and upload.url is defined asynchronously only after upload is completed
// listing object will define image_name and image_url correctly if upload time < 2 seconds
setTimeout(() => {
this.listing.image_name = this.currentUpload.name;
this.listing.image_path = this.currentUpload.url;
this.submitSwitch = true;
}, 2000);
} else {
this.errorSwitch = true;
}
}
// firestorage.service.ts
// Executes the file uploading to firebase storage at https://firebase.google.com/docs/storage/web/[basePath]
pushUpload(upload: Upload) {
const storageRef = firebase.storage().ref();
const uploadTask = storageRef.child(`${this.basePath}/${upload.file.name}`).put(upload.file);
uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED,
(snapshot: firebase.storage.UploadTaskSnapshot) => {
// upload in progress
const snap = snapshot;
upload.progress = (snap.bytesTransferred / snap.totalBytes) * 100;
},
(error) => {
// upload failed
console.log(error);
},
() => {
// upload success
if (uploadTask.snapshot.downloadURL) {
upload.url = uploadTask.snapshot.downloadURL;
upload.name = upload.file.name;
return;
} else {
console.error('No download URL!');
}
},
);
}
<!-- listing.component.html -->
<div class="col-md-4">
<img style="width: 100%" class="img-thumbnail" src="{{ listing?.image_path }}">
</div>
// listing.component.ts
onDeleteClick() {
this.fireStore.deleteListing(this.id); // handled by the cloud firestore service
this.fireStorage.deleteUpload(this.listing);
}
// firestorage.service.ts
deleteUpload(listing: Listing) {
const storageRef = firebase.storage().ref();
const uploadTask = storageRef.child(`${this.basePath}/${listing.image_name}`).delete();
}
To begin working with this project, perform the following tasks:
-
Clone this repo:
git clone https://github.com/Stanza987/angular-firebase-proplistings.git
-
cd
into the folder of the cloned repo -
Run
yarn install
to install dependencies -
Add your Firebase configuration to
environment.ts
andenvironment.prod.ts
export const environment = { production: false, //change to true for environment.prod.ts firebase: { apiKey: '<your-key>', authDomain: '<your-project-authdomain>', databaseURL: '<your-database-URL>', projectId: '<your-project-id>', storageBucket: '<your-storage-bucket>', messagingSenderId: '<your-messaging-sender-id>' } };
- Run
ng build --prod
- Run
firebase init
and chooseHosting
, follow the on-screen prompts. - Delete the
public
directory automatically generated by the Firebase CLI - Change
firebase.json
to{ "hosting": { "public": "dist", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ], "rewrites": [ { "source": "**", "destination": "/index.html" } ] } }
- Run
firebase deploy