Skip to content
This repository has been archived by the owner on Dec 20, 2019. It is now read-only.

Timeflies Angular 2+ front-end.

License

Notifications You must be signed in to change notification settings

stoneLeaf/timeflies-angular

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

timeflies-angular

Work in progress Build Status

Timeflies is a time-tracking SaaS built upon a full JavaScript stack. This repository contains an Angular front-end client meant to work with Timeflies's Node.js back-end.

Live demo on GitHub Pages

Please note that the demo's back-end is hosted on a free Heroku dyno. Therefore, a few seconds are necessary before it fully wakes up and handles the first request.

Table of contents

Setup

This project requires Node.js, npm and an instance of Timeflies backend API. With a global install of the Angular CLI, you can then follow these steps:

  • clone the repository
  • npm install to set up the required dependencies
  • edit src/environments/environment.ts to set the base URL of the backend API (local or remote)
  • ng serve to run a dev server

Development journal

In the same fashion as with my coding journey and the back-end development, I will try to lay out my chain of thoughts and progress.

  1. Tour of Cats
  2. Minesweeper
  3. Reporting for duty
  4. Router entanglement
  5. First request
  6. Authentication
  7. The matcher trick
  8. Fixing redirect from guard
  9. Taking shape
  10. Hitting REC
  11. Off the charts
  12. Back and forth
  13. Continuous deployment

Tour of Cats

To get a first glance at the Angular framework, I started with the official Tour of Heroes tutorial, and made it about cats instead. I was finally going to see what all the fuss was about with component-based design. The component starter kit provided by the Angular CLI for each of them is a class, a template, a private stylesheet and a spec. The idea is that these components are reusable throughout the application and should be as isolated as possible.

Something that struck me right away was the easy implementation of two-way data-binding. With a special syntax, [()], also called banana-box, wrapped around the ngModel directive of FormsModule, the value of a text input syncs back instantly to the model. And it was also the first time I encountered TypeScript. It brought back familiar notions such as instances and decorators, and mixed them with the ones I had newly encountered in JavaScript.

For some reason, the notion of reusable components made me think of the tiles of a Minesweeper game. Without further ado, I booted up a fresh Angular project with the CLI and put my mind to it. After some work on a settings screen, I had a field with fixed dimensions from which I intended to dynamically generate a grid of tile components. The solution I came up with was using two ngFor directives which would iterate through both dimensions of the field array.

As anticipated, I also had to design the layout with CSS, or more specifically Sass which adds a few handy features like mixins and inheritance. I must admit I had some apprehension as it brought back distant memories of tedious work. This time I decided to use Flexbox which now has good browser support. With the help of a good guide, I built the layout with a lot more ease that I would have ever imagined and felt a bit reconciled with the matter.

Each tile component encapsulating a tile object, my goal was then to bind properties of the latter to CSS classes of the former. For instance, when a tile was flagged by the user, the flagged property of the object was set to true and the .flagged class had to be applied to the component. I achieved this by using the ngClass directive which adds classes depending on evaluated conditions. The result for the aforementioned example was [ngClass]="{'flagged': tile.flagged}". This system turned out to work perfectly and I was quite impressed with the tidiness of this solution.

On a last note, I tried to find a use for RxJS as I wanted to get a glimpse of reactive programming. I ended up using BehaviorSubject observables to hold some values and made components react to changes emitted. In the end, I went a lot further than anticipated on this small intermediate project, and now feel a lot more prepared and eager for what's next.

Reporting for duty

The first order of business was to design a draft of the logo. I felt like I needed that and a color scheme before anything else to help me get mental pictures of the layout. While I am not illustrator, I still managed to make something workable. As intended all along, I used a play on the word flies to get a basis for the logo.

Regarding the general layout, which I had already sketched on plain old paper, I planned to have a full width navigation bar at the top, a content section with fixed width and a minimalist footer. While I intended to make it responsive and more elaborate at some point, the primary focus was to produce a basic and workable layout.

Router entanglement

Faithful to my long habit of adding complexity at early stages, I decided it was imperative that the layout for what I called the lobby area was different from the one for the logged in user interface. While I knew it would require some more advanced structural work, I figured it would simply translate in creating several sub modules with dedicated companion routers. The implementation, of course, turned out to be a lot more time consuming that I had anticipated.

In the end it took me a few hours of frustration and failed attempts to achieve the end goal. I could not get my head around nested router outlets. It felt logic to have a global router outlet in my root component routed to a component from a sub module, which would contain another router outlet filled in turn by a sub router. In the end the solution was the children property of Route, of which I was aware from the beginning but that I had prematurely discarded due to a misconception. Thankfully, I had gained a better understanding of Angular routing in the process and was finally satisfied with the result.

A significant benefit of that structure is that it allows lazy loading. Basically, modules can be loaded on a as-needed basis. For instance, a visitor could be able to swiftly load the lobby area without fetching right away the supposed heavy application interface. That feat was accomplished at the root router level, with the loadChildren property. At that stage it was obviously a bit early but I was glad I had encountered such an important feature.

First request

I decided that the first step of the application implementation had to be the user signup. To this end, I needed, at the very least, a working form and a service making the HTTP requests to the backend. I had already stumbled upon the ngModel directive and planned to use it. I ended up creating an empty user model at component creation, binding its properties to the form inputs and POSTing the result on submission. Of course, the first request I made didn't work as my backend was not allowing CORS. After a quick commit, I finally had the first successful request to the API.

The back-end running locally, the requests were too fast to let me see style changes such as button deactivation on submission. I decided to add an HttpInterceptor which would delay all requests in order to simulate network latency. But I just couldn't make it work at first. I was adding it to the AppModule providers array on the condition that environment.production was false. And it turned out to be true, while isDevMode() was also true, and I couldn't get my head around it. In the end, I found out that I had misused the auto-import feature of VS Code and imported environment.prod instead of environment, a difference which was quite hard to spot at the end of the import line. Ironically, I ended up discarding the idea of an interceptor made for that purpose and used the more suited network throttling capability of the Chrome DevTools.

Many questions arose as I started to write the code handling the API responses. Some of them about error handling. It was obvious to me that it had to be done at the service level and never in the components. For that purpose, I created a specific handler for HTTP errors which was piped in with catchError. One of the ideas was to create a ValidationError that would be passed to the components in case of a 422 status and be processed to give feedback to the user. I thought about network issues too and planned to create a toast notification service later on to display short messages.

Immediately after, the HTTP error handling had to be switched from a misused service to an HttpInterceptor. In case the server response was 401 (unauthorized), I wanted the router to navigate to the login page. For that purpose, I simply added the Router to the constructor to have it injected, but its reference remained undefined. Then I remembered that I wasn't directly calling the error handler's singleton instance method but instead passing it as a function object to the catchError operator. With the interceptor, I was finally able to make the router call I needed and as a bonus my services became drier.

Authentication

Having implemented a JWT authentication scheme on the back-end, I needed to write the client-side part. To that purpose, the first step was getting the token from the login request and storing it in local storage. The token payload containing the user profile, it had to be extracted, stored in the authentication service and made available. In case of a browser refresh, or any application reloading, the token would be read again and if not expired, the profile extracted. That process was triggered by a route guard mediating navigation to all routes requiring a logged in user.

In addition, the token had to be sent along every back-end API requests. Came to the rescue another HttpInterceptor which would add the authentication header on the fly to all requests. I was aware that angular2-jwt, a package I was already using for token decoding, came with a fully-featured interceptor. But I thought I would make my own as I wanted to comprehend all authentication aspects of the application. Of course I added a simple filter to prevent token leaks to other domains than the back-end's.

One feature I chose to implement was a very basic user role system. For a single guard to suffice, I just fed it arguments using the data option of Route, in specific an array of Role, a custom enum. For instance, the dashboard area required a logged in user and therefore Role.User was passed to the guard. On the other hand, the lobby area was only to be accessible to unregistered users and Role.Unregistered was passed. For a route to be accessible to several user roles, they just had to be passed in an array. Those rules were probably going to change later but it was interesting to put in place some basic logic and experiment.

The matcher trick

Early in the application design, I had in mind a structure in which a different module would be lazy loaded depending on wether the user was logged in or not. Furthermore, I didn't want to have the user interface available under a global sub-path. For instance, I pictured that the root path of the application for a logged in user would be the dashboard from the user interface module, and for the unregistered visitor, the introduction page from the lobby module. I thought of it as a pretty common architecture and chose to implement it later as I was not yet comfortable enough with the router.

Then came the time to implement and things were unexpectedly trickier. The first obvious thing I tried was using a guard. But what I had not realized at first is that when a guard returns false, as in this route should not be activated, the navigation is definitively canceled. It's up to the guard to trigger some actions such as a router navigation to a fallback path. And what I needed instead is the router to look for another match in the Routes array, specifically to another module.

After looking at all the properties available for a Route, I came up with an idea. The matcher option took a function that could be set up as a custom path matching strategy. In my case, even though I didn't care much for the path itself, I thought I could maybe divert it from its original purpose and check instead if the user was logged in or not. The issue which came first when implementing was that they were only functions and as such did not have access to Angular's dependency injection, thus preventing any calls to the authentication service. I wondered what things of interest could be accessed from this scope and then it hit me, localStorage. I just had to check whether the token property was set or not.

To my great satisfaction, this solution turned out to work well and exactly as intended. After some more research I found out that another developer had come out with a similar solution. Moreover, he found a way to access the DI from the matcher functions. But I liked the simplicity of my implementation and did not think it was at all necessary in my case to risk playing with Angular's internals.

Fixing redirect from guard

With the 7.1 Angular update came a new feature for guards. Instead of handling manually redirections and then returning false, it became possible for them to return a UrlTree to be directly processed by the router as a redirection. The goal was to let the former properly handle priority in case multiple guards were trying to redirect at the same time. But as I was implementing this feature, something did not work as expected and it led to a few hours spent debugging Angular's router.

To be specific, in case the returned UrlTree was the root url / and it was the first navigation request (as in the application is started on the guarded path), it would not redirect at all after the NavigationCancel event. The culprit was router.navigated, a boolean set to true when 'at least one navigation happened' as stated internally. For some reason, in the piped catchError block handling the case where a guard returned a UrlTree to be used as a redirection, that boolean was invariably set to true. Then, when the router would process the new navigation, in that case the redirection to the root path, it would discard it as that path was considered already navigated to.

I learned a lot in the process about both VS Code debugging tools and Angular's router inner workings. This led to my first pull request on a project that size. Before committing the patch, I also had to set up the right environment for a local run of the lengthy full Angular test suite. I ended up spending a lot more time than anticipated as I had to switch versions for some packages, install missing dependencies and use a whole new tool to me, Bazel.

Taking shape

At that point, I had enough in place to be able to finally focus on component creation. It felt great to see my application getting its features implemented one-by-one. A good amount of work went into the layout design. I wanted the UI to look good and be well structured. My daily practice of CSS was really paying off as I was less and less prone to the trial and error approach and was able to write styles with more confidence.

One key aspect of component design was finding a way to properly handle the flow of data between components. Apart from the use of services, one way to get data to a child component was the Input decorator, and to the parent component, an EventEmitter. Another key aspect was the proper handling of observables. For instance, to prevent memory leaks, it is good practice to unsubscribe on component destruction. That can become quite cumbersome when you have multiple subscriptions in a single component. With that in mind, I chose to make use of Angular's async pipe as much as possible as it handles it automatically.

Another feature I wanted to implement was a toast notification service. It was important to give feedback to user actions, and that was one way to do it. The service was just a single point of entry for broadcasting messages. Most of the logic came in a shared component which subscribed to the service stream. I chose to implement a queue system to ensure each message had a controllable lifetime and also added the option to make them either auto-dismissible or requiring a user action depending on the significance of the message.

Hitting REC

Came the time to implement the heart of the application, the handling of time activities. It was more complex, as I had experienced in the back-end, because it meant handling dates and durations. Early on in this project, the idea came up of a button in the navigation bar that would transform to a timer when activated. I had detailed sketches of it and was eager to start the implementation.

After setting up the activity model and its dedicated service, I started the creation of the aforementioned component. When there was no running activity it displayed a Record button. On click, it showed a dropdown menu filled with the asynchronously loaded project list. Finally, when the recording started, it gracefully faded into a boxed timer along with a Stop button. I was quite happy with the end result, it really served its purpose as a quick and eye-catching way to handle the activities start and stop.

Working with time intervals and durations, I also had to find a way to make them human readable in my templates. That was my excuse to experiment custom pipes. I must say it was really straightforward to implement: a simple class featuring a transform method with the input value and optional arguments, and returning the transformed value. I ended up creating three pipes for that purpose:

  • one for displaying the timer value in the navigation bar,
  • another one to put in words the time elapsed since a date (e.g. 2 days ago),
  • and the last simply to format durations.

Off the charts

The application was finally sporting the bare minimum logic it was advertising. But it became suddenly obvious something major was missing: statistics. To that point, I only had a bleak list of activities on each project page. One way to do it could have been to fetch all the activities in the desired interval and compute it into statistics. But it could have been potentially very expensive, especially since I didn't have any caching implemented. For instance, to get the total time spent on a specific project, I would have had to fetch all activities and make a sum. Imagine that for 10 different projects on a single page. That was not acceptable.

Instead, I did some extensive work on the back-end. Firstly, I added a total counter to the project model that was incremented/decremented on activity creation/deletion. Furthermore, I created a whole new endpoint dedicated to project statistics. Given a start day and an end day, it delivered an array of daily statistics either for a specific project or for all of them as a whole. The underlying logic proved to be quite challenging at some points and I ended up using moment-js range add-on to simplify the algorithm.

With that data, I could finally add dynamic charts to my layout. After some research and considering at first ng2-charts, I ended up implementing ngx-charts. To my delight, I was quickly able to configure its directive and easily shaped my data to fit its input. The application dashboard front page was now featuring a nice chart of the past 7 days activities. And the project page was displaying a chart of its past 30 days. Theses additions were really a game changer in terms of both usefulness and appearance.

Back and forth

I encountered an interesting challenge while refactoring components. The project creation and project edition components both contained a very similar form in which only the submit button label differed. That was an obvious invitation to create a shared component so that if I were, for instance, to add a field to the project model, I would only have to add it in one place.

The extraction of the form and logic was really easy. As inputs, I firstly had the project, either blank for creation or filled for edition, which would be queried for initial values. I also had the submit button label, a simple string. As output, after passing client-side validation, the component would emit the new project to its parent component through an EventEmitter.

But that was not enough, as I also wanted the form to handle the case of a potential server-side error. That meant I had to figure out a way to transmit back asynchronously a potential message to the child form component and instruct it to allow a new submission. The solution I came up with was to pass, as a third input, an observable the child component would subscribe to in order to get the potential message and switch back from its waiting state.

To sum up the data flow:

  • the model goes down from parent to child as an Input,
  • it goes back up as an event when submitted by the user and cleared by client-side validation ;
  • and then the parent can potentially ask the child for a new submission if a server-side error came up, through an asynchronous stream (observable).

It could have been done in other ways, but I thought this one was satisfying and furthered my practice of reactive programming.

Continuous deployment

After deploying a back-end instance on Heroku, the next step was to find a host for this Angular client. Without much hesitation, I chose GitHub Pages. This required a few build adjustments as Pages for a repository are necessarily accessible under a sub path. Specifically, the base href has to be set accordingly in order for the router to work and URLs to be properly resolved. But as I soon discovered, like others, that was far from enough. After a few unsuccessful attempts, and various tweaks, I finally managed to have all the assets accessible.

But I found it cumbersome and somewhat arbitrary to manually build and push to the GitHub Pages branch every time I deemed necessary. There were great tools available to automatize that step but I wanted to go further. To that prospect, Travis CI came to mind as I had already set it up on the back-end for continuous testing. But using it for build and deployment meant a more advanced configuration.

The challenge which quickly arose was about the storage and retrieval of the production back-end API URL. It was not an option to hard-code it in an Angular's environment file as it certainly did not belong to the code repository. For a back-end, it is quite simple as the application can access environment variables. But for a front-end client being executed in the browser, it had to be done differently. After some research, I found an elegant solution in an Angular issue thread.

Quite simply, before building the client, the API URL is retrieved from the build server environment variables, and injected in the Angular project environment file using replace. As a result, the back-end API URL is only stored in Travis CI's environment variables. That was exactly what I had in mind and was quite pleased with the outcome. As I pushed to master, the code was, in that order, checked by the linter, tested in Headless Chrome, built specifically for GitHub Pages and then finally deployed.

License

The social icons are from Simple Icons and under CC0 1.0.

This project's codebase is under the MIT license and its graphics under CC BY 4.0.

About

Timeflies Angular 2+ front-end.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published