Get the Newsletter

Case Study: Codeline and Aurelia

Posted by Giray Temel on February 10, 2019

Today we're please to introduce one of our Aurelia community members, Giray Temel, to share with you about Codeline's choice of and experience with Aurelia. Take it away Giray!

What is Codeline?

Codeline is an on demand software development platform. It offers an alternative to those who seek development services. Instead of turning to freelancing sites, posting jobs, and going through applications, you can bring your project to Codeline and we'll get experienced developers started on your tasks in no time.

Codeline is an enterprise grade software product that contains real-time project management, timers and budgeting tools, finance, invoicing and payroll modules, an integrated payment system, a hiring pipeline, and more. We have a PHP / Laravel backend and a Socket.io setup for real-time functionality. Our entire front-end is written in Aurelia.

Choosing Aurelia

To build Codeline, we needed to choose a modular, modern client-side framework that lends itself to robust single-page application development. Codeline is a data intensive, real-time software application. Therefore responsiveness, managing data flow, and handling complex routing were key concerns for us.

Being already familiar with Aurelia from a previous client project, we knew the framework's strengths, extensibility, and ease with which it can be customized.

We initially went for a Typescript + SystemJS setup. After the framework matured and the Aurelia CLI was released, we switched to that. Our build pipeline is now very simple and highly functional.

Less Constraints, More Customization

Most frameworks require you to write code in a certain format. Vue has .vue files, React has JSX. On the other hand, in the Aurelia framework the ViewController is just a simple class. You don't need to extend a parent component class. There are no contrived data() or methods() methods. Your logic and your view live in two different files for each module. If there's no particular logic, you can even leave out the ViewController and just write the template.

In our case, this flexibility allowed for a comprehensible architecture, enabled better separation of concerns, and resulted in a cleaner codebase.

Models and Role Based Authorization

Every client app needs to talk to the server and Codeline is no exception. These exchanges must be authenticated and authorized based on the active user.

In our case, the business rules around what actions can be taken on which models are very complex. Clients can create new tasks, but they can't run timers on them. A developer cannot delete a task, only a project manager is able to do that.

Codeline Project View

When you consider the size of Codeline and the dozens of models we have, it's obvious that we needed a better solution than if else'ing all over the codebase. Plus, we had no intention of maintaining two copies of the same logic both on the server and the client-side.

Our client-side strategy for authentication and authorization with Aurelia framework consists of these components:

1. Revive HTTP responses as Models

A model is nothing but a simple class. We simply take the response from the backend and instantiate the relevant Model class with this data.

One obvious advantage of this is being able to enrich the plain data with additional methods and getters. For instance, we have a Notification model and notifications come in levels. 1 is an info, 2 is a warning and 3 is an alert. In the UI, we display corresponding icons for each level. So we added a simple transformer to our Notification model that returns the CSS class of the icon dynamically.

    
  @transient()
  export class Notification extends Model {
    ...
  
    level: number;
  
    private levelIcons: any = {
      1: "info-circle",
      2: "warning",
      3: "alert-circle",
    };
  
    get levelIcon() {
      return this.levelIcons[this.level];
    }
    
    ...
  }
  
  

This way, we can simply do this in any of our views:

    
  <i class="${notification.levelIcon}"></i> ${notification.text}
  
  

2. Can Method on the Base Model

The more interesting upside is that our base model (the parent class for all models) presents a can(action: string): boolean method. Using this, we can check if an action is authorized on the given model anywhere in the codebase. To pull this off, we include an actions array in the data returned by the server. It looks something like this: ['start', 'edit', 'delete'] Then the can(action) method simply checks if this array contains the given string.

In our views, it looks something like this:

    
  <timer-start-button if.bind="timer.can('start')"></timer-start-button>
  
  

3. User Model and Role Checks

The authenticated user's data is also revived as a User model and this instance exposes a hasRole method, much similar to the can method we mentioned earlier. We also added shortcut methods that just call hasRole for us with specific combinations, such as isEmployee and isClient.

Customizing Aurelia

Codeline has unique needs when it comes to routing. The layout of the app is quite versatile and dynamic. Thankfully, Aurelia's powerful router went above and beyond our expectations when it came to customization.

First, let's take a look at Codeline's full layout:

    
  _______________ Top Bar ________________
  _______________ Nav Bar ________________
  
  | Left Menu   | Main View | Right Pane |
  ________________________________________
  
  

Here's where it gets tricky:

  • We want the left menu to be displayed only for certain routes. We couldn't include the left menu with each view as this would cause unnecessary loads or jumps in scroll position. It had to stay in place and must be toggled based on the active route.
  • The right pane, which we call the "viewbar", is also only displayed with certain routes. It's basically a little view that slides in from the right and is typically used for quickly creating or updating records. We already had a jQuery plugin for this but we needed to keep it open when the page is refreshed, or hide it dynamically when the user navigates away to another page.
  • Routes need to be authorized. We don't want our clients to access the administrative routes.

With all of these custom requirements in mind, we devised the following interface:

    
  export interface RouteConfig {
    name: string;
    title?: string;
    icon?: string; // Icon displayed in the navbar
    nav?: boolean; // Should the route appear in the navbar?
    viewbar?: string; // Path to the viewbar module. Null hides it.
    hasLeftMenu?: boolean; // Show/hide the left menu. Hidden by default.
    roles?: Array<string>; // List of roles to access route. Null allows everyone.
    login?: boolean; // Null allows everyone. True: users only, False: guests only.
  }
  
  

Route Authorization

When adding routes, we make sure that the settings property of the route complies with the above interface.

To authorize the routes, we created a pipeline step called AuthorizeRoutes and added it to our configureRouter in app.ts like this:

    
  import { AuthorizeRoutes } from "./core/auth/auth-middleware";
  ...
  class App {
  	configureRouter() {
  		config.addPipelineStep("authorize", AuthorizeRoutes);
  	}
  }
  
  

And auth-middleware.ts looks like this:

    
  import {inject} from "aurelia-dependency-injection";
  import {NavigationInstruction, Redirect} from "aurelia-router";
  
  import {Auth} from "./auth";
  
  @inject(Auth)
  export class AuthorizeRoutes {
    constructor(private auth: Auth) { }
  
    run(navigationInstruction: NavigationInstruction, next: any) {
      // Check if the route has an "auth" key
      // The reason for using `getAllInstructions()` is to check all the child routes
      const allInstructions = navigationInstruction.getAllInstructions();
  
      if (allInstructions.some(i => this.requiresAuthMode(i.config.settings.login))) {
        if (!this.auth.check()) {
          // User needs to login
          // Redirect to login...
        }
  
        let roles = navigationInstruction.config.settings.roles;
  
        if (roles.length && !this.auth.user().hasRole(roles.join('|'))) {
          // Not authorized to access
          // Redirect to home page
        }
      }
  
      if (allInstructions.some(i => this.requiresGuestMode(i.config.settings.login))) {
          // Must be a guest to view...
          // Redirect to home page
      }
  
      return next();
    }
  
    private requiresAuthMode(login) {
      return login === true;
    }
  
    private requiresGuestMode(login) {
      return login === false;
    }    
  }
  
  

Toggling the Left Menu Based on Active Route

In our app.html we check if the left menu should be visible based on the current route:

    
  <div class="page scrollable" id="mainPageContainer">
    <div class="page-aside" if.bind="router.currentInstruction.config.hasLeftMenu">
      <compose view-model="modules/left-menu/left-menu"></compose>
    </div>
    <div class="page-main">
      <div class="page-content">
        <router-view swap-order="after"></router-view>
      </div>
    </div>
  </div>
  
  

Using Aurelia's Enhance to Integrate jQuery With Aurelia

Our right pane implementation had to work with the existing jQuery plugin as it already had the CSS animations we wanted. One problem with it was we needed to load Aurelia views using the plugin, not just static HTML. Aurelia had a solution ready for this, too. As Aurelia is a modular framework, you are able to leverage the functionality in each module individually to create any custom implementation you can imagine.

Our solution works in two steps:

  1. Listening to router:navigation:success to capture page changes.
  2. If the activated route has the viewbar setting defined, trigger the jQuery plugin and load the relevant module inside the right pane.

The first part is done through the event aggregator package that ships with Aurelia. The router package will emit useful events that let you hook into the router lifecycle. Here is a very useful article that lists all of these events.

    
  this.eventAggregator.subscribe(
  	'router:navigation:success',
  	this.onNavigationSuccess
  )
  
  

The second step is to get the router instruction from the event and handle it:

    
  onNavigationSuccess($event) {
  	const {viewbar} = $event.instruction
      .getAllInstructions()
      .pop()
      .config.settings
     
  	$.slidePanel.show({
  		content: '<compose view-model="' + viewbar + '" containerless></compose>'
  	}, {
  		afterLoad: () => this.enhance()
  	});
  }
  
  enhance() {
  	// We get the main DIV to be enhanced,
  	// Binding context is basically the view model (data) exposed to your view.
  	this.viewbarView = this.templating.enhance({
  		element: $(".slidePanel").get(0),
  		bindingContext: {
  			...
  		},
  	})
  	// As lifecycle methods are not called after manually enhancing,
  	// Call the attached method yourself.
  	if ("attached" in this.viewbarView) {
  		this.viewbarView.attached()
  	}
  }
  
  

Conclusion

There are loads of other awesome features in Aurelia framework that made the process of developing Codeline really fun: value converters, custom elements and attributes, embedded event system and cutting edge templating features, to name a few. Aurelia is different from other frameworks in how customizable and versatile it is. You feel closer to the metal, as you simply write regular JavaScript classes. You don't have to jump through any hoops to satisfy any arbitrary syntax or structure determined by the framework. Aurelia packs great power inside its well-written and tested modules to satisfy your expectations out of a modern JavaScript framework. As Codeline, we are a proud supporter of Aurelia and we are happy to see the framework get better and better.