Creating a web application with Angular and Firebase - What I Learned

Just before the summer holidays, a member of staff at my school approached me looking for the best way of doing something that's usually performed on paper, digitally. It's a niche concept that can best be described as "self-allocation to classes in the timetable". In my Sixth Form, we have these sessions called Directed Studies - sessions throughout the day during which someone watches over us while we procrastinate study productively. In Year 13 the number you have to attend is predetermined, but you get to choose when you would like these study periods. It's a relatively simple system, however, in the past, it involved getting everyone's preferences on paper.

Now, you might say that building a website for this sole purpose is overkill - if it works, then why fix it? I agree. If you were to weigh up the value of the tool and development effort, it makes no sense to build something from scratch.

That's why I made a responsive and modern web application, from the ground up, to perform such a task.

Angular?

I started last Friday - the day after I got back from an internet free, three-week-long trip to Lesotho and South Africa (which was excellent, by the way). I thought I'd have to take some time to get used to the keyboard once again but nope, I was straight into it. The first thing I did was research the different frameworks that are currently used for creating web applications. The choice was between Angular and React. Most places on the internet recommend React since it's a smaller package and is fairly simple to understand, however, I decided to try out Angular because Typescript looked like a beautiful language to learn (that's right I called a programming language beautiful) and I wanted to challenge myself. That being said I already had a small amount of experience in Angular development from my time at Hildebrand which proved useful.

Even though the CLI is extremely powerful, tooling such as Angular Console and VS Code made it even easier to use the framework.

Firebase

From the start, I knew that I wanted to take advantage of Firebase's real-time database. Even though it would have been fun to create my own API, Firebase allowed me to worry about one codebase only, as a result cutting development time in half. I'll make sure my next web project uses a framework like Express.js!

Firebase also has very generous quota's for their free plan, important for me since I don't want to pay out my own pocket for something that the school is using. Authentication is free of charge and 50,000 document reads per day are probably enough for the Firestore database. I'll cover this in a bit more detail later on.

I used angularfire2 to make all of my dynamic requests and didn't find a single issue with the library. Not to mention the Firebase CLI that deploys application code and Firestore rules with a simple command.

Design

Another benefit to using Angular was the availability of Material Design components. I ended up using material.angular.io due to its relatively large library of components and data tables. The alternative was to implement Google's Material Web Components for Angular since these seemed more faithful to the Material spec, however, the lack of a data table meant that it did not meet my requirements. 

Branding of the Choose When logo is in Montserrat Alternates, and I have used this typeface for key UI elements, including the login page title.


Once signed in, I have opted for a simple Material Design card layout. The cards are dynamically generated depending on the school's timetable details, and CSS styles the selected time slots.


Generating the table

Below is the HTML Angular code that is used to generate the table, and the supporting code. Note the code with the ngFor directive. let weekIndex of school?.timetable.weekData.names.length | ngForNumberLoop I am using a custom pipe to iterate through the names array with a counter variable instead of each item. The code for that pipe can be found on Stack Overflow from Pardeep Jain. This gave me a lot more power to do what I wanted in the HTML code.


<ng-container *ngIf="sessions"> <div fxFlexOffset="20px" fxLayout="row" fxLayoutAlign="center" *ngFor="let weekIndex of school?.timetable.weekData.names.length | ngForNumberLoop"> <div fxFlex.lg="40%" fxFlex.md="60%" fxFlex.lt-md="95%"> <mat-card> <h2 class="mat-title">{{school?.timetable.weekData.names[weekIndex]}}</h2> <mat-grid-list [cols]="school?.timetable.dayData.names.length" rowHeight="{{isHandset ? '1:1' : '3:1' }}"> <!-- Headers --> <mat-grid-tile class="header" *ngFor="let dayName of school?.timetable.dayData.names"> <p class="mat-subheading-1">{{dayName}}</p> </mat-grid-tile> <!-- Sessions --> <div *ngFor="let sessionIndex of (school?.timetable.sessionData.count + school?.timetable.sessionData.breakAfter.length) | ngForNumberLoop"> <mat-grid-tile *ngFor="let dayIndex of school?.timetable.dayData.names.length | ngForNumberLoop" [class.break]="sessions[weekIndex][dayIndex][sessionIndex].break" [rowspan]="sessions[weekIndex][dayIndex][sessionIndex].break ? '1' : '2'"> <div fxFlexFill matRipple fxLayout="column wrap" fxLayoutAlign="center center" *ngIf="sessions[weekIndex][dayIndex][sessionIndex].events?.length; else noEvent" [class.event]="sessions[weekIndex][dayIndex][sessionIndex].events.length > 0" [class.selected]="isSelected(sessions[weekIndex][dayIndex][sessionIndex])" (click)="onSelect(sessions[weekIndex][dayIndex][sessionIndex].events)"> <ng-container *ngIf="sessions[weekIndex][dayIndex][sessionIndex].events.length === 1; else manyEvents"> <p class="mat-body-2"> {{sessions[weekIndex][dayIndex][sessionIndex].events[0].event.name}}</p> <p class="mat-body-1">{{sessions[weekIndex][dayIndex][sessionIndex].events[0].event.participants.length}} / {{sessions[weekIndex][dayIndex][sessionIndex].events[0].event.capacity}}</p> </ng-container> <ng-template #manyEvents> <p class="mat-body-2">{{sessions[weekIndex][dayIndex][sessionIndex].events?.length}} sessions</p> <p class="mat-caption" style="margin: 0px" *ngFor="let multipleSession of sessions[weekIndex][dayIndex][sessionIndex].events">{{multipleSession.event.participants.length}} / {{multipleSession.event.capacity}}</p> </ng-template> </div> <ng-template #noEvent> <ng-container *ngIf="!sessions[weekIndex][dayIndex][sessionIndex].break; else break"> <p class="mat-caption">No Event</p> </ng-container> <ng-template #break> <p class="mat-caption">break</p> </ng-template> </ng-template> </mat-grid-tile> </div> </mat-grid-list> </mat-card> </div> </div> </ng-container>

Quota

As mentioned earlier, Firebase has a free plan which offers a reasonable allowance. One of the key limits for Firestore is the number of 'Document Reads' and 'Document Writes' in a day. This is the number of reads and writes to objects in the database, documents. When querying a collection, you get charged n number of Document Reads, where n is the number of returned documents. This is something that I did not realise when I started, and therefore I did not plan the data structure well enough to keep the Document Read count as low as possible. It's key to note that duplication of data is definitely not frowned upon here, in fact, it's encouraged. This will result in slower writes but in the end, much faster reads.

For Choose When, I store details about an event's participants in two places. Firstly, in an array called participants in the event document - this lets school admins view a list of participants. Secondly, in an array called events in each user detail document - this means I can display a count of joined events without having to query for every event with this participant. When scaled up to 100 users, this should reduce the dramatically reduce the number of Document Reads consumed while managing users.


Security

Security was something that I didn't think much of until I got to work writing the Firestore security rules. Creating a serverless web app means that all the database interaction code is performed client-side, this is assuming that nobody else can access the database without going through your application. Clearly, this is not true - the easiest thing for a malicious person to do is to reverse engineer the app and find the calls to the database, hence there should be authentication and verification on both the server and client. Firebase released a pretty informative video about security rules a couple days ago on YouTube. This was not available when I was working on the site, and I spent a lot of time researching and looking for documentation when it is so well explained in that video - definitely watch it.

The security rules had actually ended up a lot more complex than I had imagined. Check out the file on GitHub to see what I mean.

Closing comments

As a developer of mainly Android applications, I found development with Angular very enjoyable - a lot more, in fact, than with the Android framework. Part of this was due to the beauty of a language that Typescript is, which has encouraged me to switch to Kotlin once I am done rewriting Monitor. The project is Open Source and available on Github here: https://github.com/daniel-stoneuk/timetable-scheduler

Comments