Tuesday, November 22, 2016

Creating a Todo web app using Angular2 and Aurelia for Comparison

 

Introduction

YouTube Video Link:

In this article, I’ll show how to create a TodoMVC application using Angular2 and Aurelia to compare these frameworks. The article is in a table format with 2 columns named “Angular2” and Aurelia, which contains the steps to create a TodoMVC application in both these frameworks. The projects are created using CLI (Command Line Interface) tools that are part of these frameworks. The source code for these projects are available in my github page:

Angular2 Source Code: https://github.com/rajajhansi/ng2-todo-app

Aurelia Source Code: https://github.com/rajajhansi/au-todo-app

 

Steps to create a Todo MVC Application in Angular2 and Aurelia

The table below lists the steps to create a Todo MVC Application in Angular2 and Aurelia.

Angular2

Aurelia

Installing the ng CLI
npm install angular-cli -g
Installing the au CLI
npm install aurelia-cli -g
Creating a new application
ng new ng2-todo-app
Creating a new application
au new au-todo-app
Running the application
cd ng2-todo-app
ng serve
Running the application
cd au-todo-app
au run --watch
Creating a Todo class
ng g class Todo
Creating a Todo class
You can either run the command:
au generate Todo
and move src/resources/Todo.ts into src folder and delete src/resources/elements/Todo.html
      OR
Create a new file named Todo.ts in src folder, which is easier.
todo.ts
export class Todo {  
	id: number;  
	description: string = '';  
	done: boolean = false;
  	constructor(values: Object = {}) {   
 		Object.assign(this, values);  
	}
}
todo.ts
export class Todo {  
	id: number;  
	description: string = '';  
	done: boolean = false;
  	constructor(values: Object = {}) {   
 		Object.assign(this, values);  
	}
}

Creating unit test for Todo class using jasmine - Angular2
todo.spec.ts

/* tslint:disable:no-unused-variable */

import { TestBed, async } from '@angular/core/testing';
import {Todo} from './todo';

describe('Todo', () => {

  it('should create an instance', () => {
    expect(new Todo()).toBeTruthy();
  });

  it('should accept values in the constructor', () => {
    let todo = new Todo({
      description: 'hello',
      done: true
    });
    expect(todo.description).toEqual('hello');
    expect(todo.done).toEqual(true);
  });

  it('should ignore values sent in wrong properties in the constructor', () => {
    let todo = new Todo({
      title: 'hello',
      done: true
    });
    expect(todo.description).toEqual('');
    expect(todo.done).toEqual(true);
  });

});

Creating unit test for Todo class using jasmine - Aurelia
todo.spec.ts

/* tslint:disable:no-unused-variable */
import {Todo} from '../../src/todo';

describe('Todo', () => {

  it('should create an instance', () => {
    expect(new Todo()).toBeTruthy();
  });

  it('should accept values in the constructor', () => {
    let todo = new Todo({
      description: 'hello',
      done: true
    });
    expect(todo.description).toEqual('hello');
    expect(todo.done).toEqual(true);
  });

  it('should ignore values sent in wrong properties in the constructor', () => {
    let todo = new Todo({
      title: 'hello',
      done: true
    });
    expect(todo.description).toEqual('');
    expect(todo.done).toEqual(true);
  });

});


Todo Service Class - Angular2
todo.service.ts
import { Injectable } from '@angular/core';
import { Todo } from './todo';

@Injectable()
export class TodoService {

  lastId: number = 0;

  todos: Todo[] = [];

  constructor() { }

  addTodo(todo: Todo): TodoService {
    if(!todo.id) {
      todo.id = ++this.lastId;
    }
    this.todos.push(todo);
    return this;
  }

  deleteTodo(id: number): TodoService {
    this.todos = this.todos.filter(todo => todo.id !== id);
    return this;
  }

  updateTodo(id: number, values: Object = {}): Todo {
    let todo = this.getTodo(id);
    if (!todo) {
      return null;
    }
    Object.assign(todo, values);
    return todo;
  }

  getAllTodos(): Todo[] {
    return this.todos;
  }

  getTodo(id: number): Todo {
    return this.todos.filter(todo => todo.id === id).pop();
  }

  toggleTodoDone(todo: Todo): Todo {
    let updatedTodo = this.updateTodo(todo.id, { done: !todo.done });
    return updatedTodo;
  }

  filterTodo(filterCriteria: string): Todo[] {
    switch (filterCriteria) {
      case 'active':
        return this.todos.filter((t: Todo) => !t.done);
      case 'completed':
        return this.todos.filter((t: Todo) => t.done);
      case 'all':
      default:
        return this.todos;
    }
  }
  completeAllTodos() {
    this.todos.forEach((t: Todo) => t.done = true);
  }

  removeAllTodos() {
    this.todos.splice(0);
  }

  removeDoneTodos() {
    this.todos = this.todos.filter((todo: Todo) => !todo.done);
  }
}

Todo Service Class - Aurelia
todo.service.ts
import { Todo } from './todo';

export class TodoService {

  lastId: number = 0;

  todos: Todo[] = [];

  constructor() { }

  addTodo(todo: Todo) : TodoService {
    if(!todo.id) {
      todo.id = ++this.lastId;
    }
    this.todos.push(todo);
    return this;
  }

  deleteTodo(id: number) : TodoService {
    this.todos = this.todos.filter(todo => todo.id !== id);
    return this;
  }

  updateTodo(id: number, values: Object = {}): Todo {
    let todo = this.getTodo(id);
    if(!todo) {
      return null;
    }
    Object.assign(todo, values);
    return todo;
  }

  getAllTodos(): Todo[] {
    return this.todos;
  }

  getTodo(id: number): Todo {
    return this.todos.filter(todo => todo.id === id).pop();
  }

  toggleTodoDone(todo: Todo): Todo {
    let updatedTodo = this.updateTodo(todo.id, { done: !todo.done });
    return updatedTodo;
  }

  filterTodo(filterCriteria: string) : Todo[] {
    switch(filterCriteria) {
      case "active":
        return this.todos.filter((t: Todo) => !t.done);
      case "completed":
        return this.todos.filter((t: Todo) => t.done);
      case "all":
      default:
        return this.todos;
    }     
  }

  completeAllTodos() {
    this.todos.forEach((t: Todo) => t.done = true);
  }

  removeAllTodos() {
    this.todos.splice(0);
  }
  
  removeDoneTodos() {
    this.todos = this.todos.filter((todo: Todo) => !todo.done);
  }
}

Running unit tests
ng test
Running unit tests
au build
au test
Creating a Service class for Todo
ng g service Todo
Creating a Service class for Todo
create a new folder named “src\services” and a file named todo.service.ts
Installing the TodoMVC Stylesheets
npm install todomvc-app-css todomvc-common --save

Adding custom style for toolbar

Installing the TodoMVC Stylesheets
npm install todomvc-app-css todomvc-common --save

Adding custom style for toolbar

styles.css - Angular2
/* You can add global styles to this file, and also import other style files */

@import url('../node_modules/todomvc-common/base.css');
@import url('../node_modules/todomvc-app-css/index.css');

.app-root-loader {
	text-align: center;
	padding: 30px;
}

.toolbar {
	color: #777;
	/*background-color: lightslategray;*/
	padding: 5px 10px;
	height: 20px;
	text-align: center;
	border-top: 
	1px solid #e6e6e6;
	float: right;
}


styles.css - Aurelia
/* You can add global styles to this file, and also import other style files */

@import url('../node_modules/todomvc-common/base.css');
@import url('../node_modules/todomvc-app-css/index.css');

.app-root-loader {
	text-align: center;
	padding: 30px;
}

.toolbar {
	color: #777;
	/*background-color: lightslategray;*/
	padding: 5px 10px;
	height: 20px;
	text-align: center;
	border-top: 
	1px solid #e6e6e6;
	float: right;
}

Adding a new Todo Item in text box after entering the description and pressing enter key
Angular2 has (keyup.enter) event to trigger a JavaScript function upon pressing enter key after entering the description for a new Todo item.
Adding a new Todo Item in text box after entering the description and pressing enter key

Aurelia recommends to use <form> and wire the submit.trigger to trigger a JavaScript function upon pressing enter key. It also requires a <button type=”submit”> as well. To attach enter keypress event to any element, we can easily do that with Aurelia Custom attribute.


au generate attribute keyup-enter

keyup-enter.ts - Aurelia
import {autoinject} from 'aurelia-framework';

@autoinject()
export class KeyupEnterCustomAttribute {
    element: Element;
    value: Function;
    enterPressed: (e: KeyboardEvent) => void;

    constructor(element: Element) {
        this.element = element;

        this.enterPressed = e => {
            let key = e.which || e.keyCode;
            if (key === 13) {
                this.value();//'this' won't be changed so you have access to your VM properties in 'called' method
            }
        };
    }

    attached() {
        this.element.addEventListener('keypress', this.enterPressed);
    }

    detached() {
        this.element.removeEventListener('keypress', this.enterPressed);
    }
}

Creating the application component
ng g component TodoApp
Creating the application component
au generate element todo-app

Aurelia has the concept of global resources and by adding a resource into global resources, you can use them in any views without having to do <require from=”path of the resource”></require>
src/resources/index.ts

import {FrameworkConfiguration} from 'aurelia-framework';

export function configure(config: FrameworkConfiguration) {
  config.globalResources([
    "./elements/todo-app",
    "./attributes/keyup-enter"
  ]);
}

Wiring Todo Item functionalities into the App component - Angular2
todo-app.component.ts
import { Component, OnInit } from '@angular/core';
import { Todo } from '../todo';
import { TodoService } from '../todo.service';

@Component({
  selector: 'todo-app',
  templateUrl: './todo-app.component.html',
  styleUrls: ['./todo-app.component.css'],
  providers: [TodoService]
})
export class TodoAppComponent implements OnInit {
  newTodo: Todo = new Todo();
  filter: string = 'all';
  filteredTodos: Todo[] = [];

  constructor(private todoService: TodoService) { }

  ngOnInit() {
  }

  addTodo() {
    this.todoService.addTodo(this.newTodo);
    this.newTodo = new Todo();
    this.filterTodo(this.filter);
  }

  toggleTodoDone(todo) {
    this.todoService.toggleTodoDone(todo);
    this.filterTodo(this.filter);
  }

  removeTodo(todo) {
    this.todoService.deleteTodo(todo.id);
    this.filterTodo(this.filter);
  }

  filterTodo(filterCriteria: string) {
    this.filter = filterCriteria;
    this.filteredTodos = this.todoService.filterTodo(filterCriteria);
  }

  completeAllTodos() {
    this.todoService.completeAllTodos();
    // this.checkIfAllTodosAreCompleted();
    this.filterTodo(this.filter);
  }

  removeAllTodos() {
    this.todoService.removeAllTodos();
    this.filterTodo(this.filter);
  }

  removeDoneTodos() {
    this.todoService.removeDoneTodos();
    this.filterTodo(this.filter);
  }
}

Wiring Todo Item functionalities into the App component - Aurelia
todo-app.ts
import {inject} from 'aurelia-framework';
import {Todo} from '../../todo';
import {TodoService} from '../../todo.service';

@inject(TodoService)
export class TodoApp {
  newTodo: Todo = new Todo();
  filter: string = "all";
  filteredTodos: Todo[] = [];
  constructor(private todoService: TodoService) { }

  addTodo() {
    this.todoService.addTodo(this.newTodo);
    this.newTodo = new Todo();
    this.filterTodo(this.filter);
  }

  toggleTodoDone(todo) {
    this.todoService.toggleTodoDone(todo);
    this.filterTodo(this.filter);
  }

  removeTodo(todo) {
    this.todoService.deleteTodo(todo.id);
    this.filterTodo(this.filter);
  }

  filterTodo(filterCriteria: string) {
    this.filter = filterCriteria;
    this.filteredTodos = this.todoService.filterTodo(filterCriteria);
  }

  completeAllTodos() {
    this.todoService.completeAllTodos();
    //this.checkIfAllTodosAreCompleted();
    this.filterTodo(this.filter);
  }

  removeAllTodos() {
    this.todoService.removeAllTodos();
    this.filterTodo(this.filter);
  }

  removeDoneTodos() {
    this.todoService.removeDoneTodos();
    this.filterTodo(this.filter);
  }
}

Wiring Todo Item functionalities into the App View - Angular2
todo-app.component.html
<section class="todoapp">
  <header class="header">
    <h1>Todos</h1>
    <div class="toolbar">
    <a href="#" (click)="removeAllTodos()">
        Remove All
      </a> |
      <a href="#"  (click)="removeDoneTodos()">
        
        Remove Completed
      </a> |
      <a href="#"  (click)="completeAllTodos()">
        Complete All 
      </a>
    </div>
    <br/>
    <input type="text" class="new-todo" placeholder="what needs to be done?" autofocus="" 
      [(ngModel)]="newTodo.description" (keyup.enter)="addTodo()">
  </header>

  <section class="main" *ngIf="filteredTodos.length > 0">
    <ul class="todo-list">
      <li *ngFor="let todo of filteredTodos" [class.completed]="todo.done">
        <div class="view">
          <input type="checkbox" class="toggle" (click)="toggleTodoDone(todo)" [checked]="todo.done">
          <label>{{todo.description}}</label>
          <button class="destroy" (click)="removeTodo(todo)"></button>
        </div>
      </li>
    </ul>
  </section>

  <footer class="footer" >
    <span class="todo-count"><strong>{{filteredTodos.length}}</strong> {{filteredTodos.length === 1 ? 'item': 'items'}} left</span>
    <ul class="filters">
      <li>
        <a class="{{filter == 'all' ? 'selected' : ''}}" href="#" (click)="filterTodo('all')">All</a>
      </li>
      <li>
        <a class="{{filter == 'active' ? 'selected' : ''}}" href="#" (click)="filterTodo('active')">Active</a>
      </li>
      <li>
        <a class="{{filter == 'completed' ? 'selected' : ''}}" href="#" (click)="filterTodo('completed')">Completed</a>
      </li>
    </ul>
  </footer>
</section>

Wiring Todo Item functionalities into the App View - Aurelia
todo-app.html
<template>  
  <section class="todoapp">
  <header class="header">
    <h1>Todos</h1>
    <div class="toolbar">
      <a href="#" click.trigger="removeAllTodos()">
        Remove All
      </a> |
      <a href="#"  click.trigger="removeDoneTodos()">
        
        Remove Completed
      </a> |
      <a href="#"  click.trigger="completeAllTodos()">
        Complete All 
      </a>
    </div>
    <br/>
    <input type="text" class="new-todo" placeholder="what needs to be done?" autofocus="" 
      value.bind="newTodo.description" keyup-enter.call="addTodo()">
  </header>

  <section class="main" show.bind="filteredTodos.length > 0">
    <ul class="todo-list">
      <li repeat.for="todo of filteredTodos" class="${todo.done ? 'completed' : ''}">
        <div class="view">
          <input type="checkbox" class="toggle" checked.bind="todo.done">
          <label>${todo.description}</label>
          <button class="destroy" click.trigger="removeTodo(todo)"></button>
        </div>
      </li>
    </ul>
  </section>

  <footer class="footer">
    <span class="todo-count"><strong>${filteredTodos.length}</strong>${filteredTodos.length === 1 ? ' item ': ' items '} left</span>
    <ul class="filters">
      <li>
        <a class="${filter == 'all' ? 'selected' : ''}" href="" click.trigger="filterTodo('all')">All</a>
      </li>
      <li>
        <a class="${filter == 'active' ? 'selected' : ''}" href="" click.trigger="filterTodo('active')">Active</a>
      </li>
      <li>
        <a class="${filter == 'completed' ? 'selected' : ''}" href="" click.trigger="filterTodo('completed')">Completed</a>
      </li>
    </ul>
  </footer>
</section>

</template>

Using custom element in app.html - Angular2
app.html

<todo-app></todo-app>

Using custom element in app.html - Aurelia
app.html

<template>
<require from="./styles.css"></require>
  <todo-app></todo-app>
</template>

 

Comparison of Angular2 and Aurelia

 

 

Angular2

Aurelia

I see the framework code explicitly in my application code because Angular2 uses explicit declarations via decorators
I don’t see the framework polluting my application code because Aurelia uses conventions
Hard to switch to another framework because my application has too much of framework code.
Easy to switch to another framework because my application code is reusable JavaScript code
Uses non-standard syntax like [(ngModel)], *ngFor, *ngIf, (event), {{ }} for interpolation etc., in the HTML markup that are not part of the standards. Follows web standards like web components, ECMAScript 6 string interpolation etc.
Angular2 has great support from 3rd party vendors like Telerik who are writing the Angular2 directives. Aurelia has great support from Syncfusion(http://aureliajq.syncfusion.com/). Aurelia UX bridge project is a community run project that has created components for Telerik KendoUI and Materialize.
Angular2 is more popular because it is from google and people hear about it everywhere be it a conference or other software companies’ web sites like Microsoft ASP.NET Core or MSDN or Channel 9 etc. Aurelia is gaining momentum because its creators are presenting in popular conferences like NDC and sites like Channel 9 etc. Aurelia is also supported for .NET Core officially and talked about in MSDN magazine and on Channel 9. More Channel 9 content is coming soon.
Angular2 has been in development for a very long time. Angular2 documentation has gotten much better compared to Angular 1 documentation. Aurelia has a very aggressive release cycle and they have frequent releases. The Aurelia.io hub documentation has gotten much better, very detailed oriented, technical and useful for anyone who wants to learn the framework.
Plugins are available for IntelliJ, VSCode and other popular editors out there. Plunkr supports Angular2 templates for quick online application development. Plugins are available for IntelliJ, VSCode and other popular editors out there. Aurelia team built GistRun (https://gist.run/) using Aurelia itself that can run your gists and is very handy for online application development. Aurelia has the plunker resources (https://github.com/jdanyow/aurelia-plunker) created by core team member Jeremey Dayow for use as well.
There are a lot of books out there (started using the early releases of Angular2 but are constantly updated) already for Angular2. There are only 2 books available out there for Aurelia (both are still under development) now. There are more books and video trainings coming soon.
There are many learning resources like video tutorials out there from Pluralsight, Wintellect, egghead etc. besides a lot of other tutorials on YouTube. There are so many enthusiasts who blog regularly on Angular. There are so many enthusiasts who regularly blog about Aurelia besides Aurelia’s core team members. There aren’t that many learning resources out there but the ones that are available are really top quality resources. Rob Eisenberg’s official 3 part Aurelia Virtual Training videos (https://www.picatic.com/modern-javascript-and-aurelia) on Vimeo are awesome.
It is a great move from the Angular2 team to use TypeScript, which is a typed superset of JavaScript instead of their proprietary Dart language. Aurelia itself is written using ES6 & 7 and has great starter kits (aka their skeleton navigation projects) in both ES6 and TypeScript. Some of the new plugins are built using TypeScript.
When it comes to  hiring front end developers to work in Angular2 projects, you need to look for skills like TypeScript, understanding of module systems like Webpack or SystemJs, and give an in-depth training on Angular2 fundamentals because it has a steep learning curve. When it comes to hiring front end developers to work in Aurelia projects, all you need to look for is skills like JavaScript (ES6 or TypeScript is a plus), understanding of module systems like Webpack or SystemJs or RequireJs and give some training on Aurelia fundamentals because it has a simple learning curve.
Although you’ll find many front end developers who might have heard about Angular2, knowledge of Angular 1 is not required at all because the framework has changed so much from version 1. You’ll not find that many front end developers who might have heard about Aurelia, which shouldn’t matter because the framework itself is about 1.5 years old. Since it is a conventions based framework, it should be very easy to train any front end developer on Aurelia.

 

Conclusion

Both Angular2 and Aurelia are very modern JavaScript frameworks with  very good open source community backing them up.

If I were to start a project today, I would pick a modern, modular and standards based framework like Aurelia. While others may have a different opinion which is perfectly fine, my suggestion to the readers of this blog post would be to pick a framework that works for you, your team and your company.

Happy Coding!

No comments:

Post a Comment