Porting an Angular 2.0 App to Aurelia



http://blog.durandal.io/2015/05/20/porting-an-angular-2-0-app-to-aurelia/


Overview

Recently, Joe Eames had a nice PluralSight webinar demonstrating some of the features of Angular 2.0. We thought "Wouldn't it be cool to see the same app built with Aurelia?" It seemed like a good opportunity to show what we feel makes Aurelia so easy-to-use and efficient.

The Rules

In order to ensure that this is an even and accurate comparison, we set some ground rules:

  1. The code should be a direct port unless there is a blaring issue or error in the source.
  2. The code should use all of the same variables and names in order to avoid confusion.
  3. We'll use the standard Aurelia skeleton as our starting point.

Getting Started

Looking at Joe's original repo, the most logical place to start would be the "TodoApp" component. Let's see both versions side-by-side:

Angular 2.0

app.js
import {Component, View, bootstrap} from 'angular2/angular2';  
import {NewItem} from 'components/new-item';  
import {TodoList} from 'components/todo-list';

@Component({
    selector: 'todo-app'
})
@View({
    templateUrl: 'components/app.html',
    directives: [NewItem, TodoList]
})
export class TodoApp {}

bootstrap(TodoApp);  

Aurelia

app.js
export class TodoApp {}  

Angular 2.0

app.html
<div>  
    <todo-list></todo-list>
    <new-item></new-item>
</div>  

Aurelia

app.html
<template>  
  <require from="./todo-list"></require>
  <require from="./new-item"></require>

  <todo-list></todo-list>
  <new-item></new-item>
</template>  

Commentary

In order to port the JS code, all we had to do was delete code from the Angular version. This is a nice little example of how Aurelia's support of simple conventions drastically reduces the boiler plate code developers need to write.

If you look at the HTML, you'll see that the Aurelia version has a little more markup than the Angular 2.0 version. To understand why, take a look back at the Angular 2.0 JS code. You will see that developers must list all directives that their view will use there.

Aurelia makes a fundamentally different choice.

With Aurelia, we chose to support Separation of Concerns and believe strongly that details regarding a view's implementation should not be found inside of a Controller or ViewModel. Aurelia accomplishes this by allowing the view to declare its internal dependencies using require elements. There are a number of important positive side-effects from this choice:

  • Separation of Concerns - As already mentioned, the details of a view's implementation stay inside the view and aren't leaked into the Controller or View-Model.
  • Multiple Views Per Controller/View-Model- You can now more easily have different views for the same Controller/View-Model and they can be implemented drastically differently, with each one loading only the resources it needs.
  • Sharing a View Between Controllers/View-Models - You can more easily have multiple View-Models use the same view. There's no need to duplicate the view's resources in every JS file.
  • Improved Readability/Understandability - If you look at a view's source, you know exactly what behaviors are active in that view. You don't have to hunt down a JS file, open it and examine it's directive list and then correlate those back to the view.

Ultimately, this important design difference, placing view resource declaration in the view where it belongs, allows developers greater flexibility, reduction in code duplication and an overall improvement in maintainability.

Next we'll look at the "TodoList" component:

Angular 2.0

todo-list.js
import {Component, View, For, If, EventEmitter} from 'angular2/angular2';  
import {Inject, bind} from 'angular2/di';  
import {todoItems} from 'services/todoItems';

@Component({
    selector: 'todo-list',
  injectables: [
    bind('todoItems').toValue(todoItems)
  ]
})
@View({
    templateUrl: 'components/todo-list.html',
    directives: [For, If]
})
export class TodoList {  
  constructor(@Inject('todoItems') todoItems) {
        this.items = todoItems;
    }
    setCompleted(item, checked) { 
        item.completed = checked;
    }
    completeAll() {
        this.items.forEach((item) => {
            this.setCompleted(item, true); 
        });
    }
    removeItem(item) {
        this.items.splice(this.items.indexOf(item), 1);
    }
}

Aurelia

todo-list.js
import {TodoItems} from 'services/todo-items';

export class TodoList {  
  static inject = [TodoItems];
  constructor(todoitems) {
    this.items = todoitems.items;
  }
  completeAll() {
    this.items.forEach(item => item.completed = true);
  }
  removeItem(item) {
    this.items.splice(this.items.indexOf(item), 1);
  }
}


Angular 2.0

todo-list.html
<div style="margin-bottom:10px">  
    <h1>To Do</h1>
    <div style="padding:5px" *for="var item of items">
            <input type="checkbox" #chkbox [checked]="item.completed" (click)="setCompleted(item, chkbox.value)">
            {{item.text}} <a class="glyphicon glyphicon-remove" (click)="removeItem(item)"></a>
    </div>  
    <button *if="items.length > 1" class="btn btn-xs btn-warning" (click)="completeAll()">Complete All</button>
</div>  

Aurelia

todo-list.html
<template style="margin-bottom:10px">  
  <h1>To Do</h1>
<div style="padding:5px" repeat.for="item of items">  
      <input type="checkbox" checked.bind="item.completed" />
      ${item.text} <a class="glyphicon glyphicon-remove" click.trigger="$parent.removeItem(item)"></a>
  </div>
  <button if.bind="items.length" class="btn btn-xs btn-warning" click.trigger="completeAll()">Complete All</button>
</template>  

Commentary

Again, the primary change we made was to delete a bunch of code. Aurelia's conventions just don't require you to do all that work. We were also able to delete the setCompleted method, since Aurelia has Two-Way Databinding. The manual synchronization work required by Angular 2.0 can be automatically handled by Aurelia.

In this component, you also see the introduction of Dependency Injection. With Aurelia, you can simply create a static member named inject to declare your class's dependencies. If you prefer, you can also use an ES7 decorator to accomplish the same thing. In the Angular 2.0 version, for some reason, the DI has to be configured in two places. Once in the Component decorator and once on the constructor.

Looking at the HTML, we see two completely different approaches to syntax. The Angular 2.0 version relies on special characters: *, (), [] and #. The Aurelia version relies on binding commands, designated with the dot (.) character.

Again, this is an important design difference with serious ramifications.

While the special characters that Angular chooses for its templating language are technically valid in HTML, they are not valid in SVG nor can they be used directly with DOM APIs. The browser will not allow it. To get around this, you have to use a secondary syntax. So, all Angular 2.0 developers must learn two syntaxes for templating instead of one. On the other hand, Aurelia's templating syntax works in all scenarios without any issue: one, consistent syntax for everything. Additionally, using the dot as a separator allows us to make our binding language fully extensible.

There's another important side-effect of this design choice as well. If you've never seen Angular 2.0 before, you probably have no clue what those symbols mean. It's cryptic and requires even knowledgable developers to keep a constant mental map between the symbol and it's meaning. Aurelia, on the other hand, uses human-readable binding commands. Even developers who have never seen it before can often understand what it means. This results in improvements in learnability, readability and maintenance.

A few more quick notes:

  • Angular uses {{}} and Aurelia uses ${}. There's no real technical reason behind Aurelia's difference in choice here. It's more related to consistency. In ES 2015, JavaScript string interpolation is done with the ${} syntax. Throughout Aurelia, wherever possible, we've constantly tried to adopt the same syntax and concepts as the native platform, including bridging similar ideas into our templating language.
  • Aurelia's templating language can leverage Two-Way binding on the input element, allowing us to remove Angular's manual event wireup in the view as well as the previously mentioned function from JavaScript.
  • We used Aurelia's trigger command for events in order to match Angular 2.0 behavior as much as possible. However, particularly in the case of the repeater, we would normally use delegate to enable event delegation and reduce event handler wire-ups. I believe that Angular also supports this with the (^event) syntax, but that may have changed.

Now, let's port the last piece, the NewItem component:

Angular 2.0

new-item.js
import {Component, View} from 'angular2/angular2';  
import {Inject, bind} from 'angular2/di';  
import {todoItems} from 'services/todoItems';

@Component({
    selector: 'new-item',
  injectables: [
    bind('todoItems').toValue(todoItems)
  ]
})
@View({
    templateUrl: 'components/new-item.html'
})
export class NewItem {  
    constructor(@Inject('todoItems') todoItemList) {
    this.items = todoItemList
    }
    keyPressed($event, input) {
        if($event.which === 13) {
            this.addItem(input);
        }
    }
    addItem(input) {
        this.items.push({
            text: input.value,
            completed: false
        })
        input.value = '';
    }
}

Aurelia

new-item.js
import {TodoItems} from 'services/todo-items';

export class NewItem {  
  static inject = [TodoItems];
  constructor(todoitems) {
    this.items = todoitems.items;
  }
  keyPressed($event) {
    if($event.which === 13) {
      this.addItem(this.value);
    }
  }
  addItem(input) {
    this.items.push({
      text: this.value,
      completed: false
    })
    this.value = '';
  }
}


Angular 2.0

new-item.html
<div class="form-inline">  
    <div class="form-group">
        <label for="description">New Item</label>
        <input id="description" class="form-control" #desc (keyup)="keyPressed($event, desc)">
    </div>
    <button class="btn btn-primary" type="button" (click)="addItem(desc)">Add Item</button>
</div>  

Aurelia

new-item.html
<template>  
  <div class="form-inline">
    <div class="form-group">
      <label for="description">New Item</label>
      <input id="description" class="form-control" value.bind="value" keyup.trigger="keyPressed($event)">
    </div>
    <button class="btn btn-primary" type="button" click.trigger="addItem()">Add Item</button>
  </div>
</template>  

Commentary

As in the previous examples, we begin the porting by deleting a lot of Angular 2.0 code which is just not needed. Dependency Injection takes on the same form here as previously with a single declaration in Aurelia vs. two for Angular.

There's a slightly different sort of interplay here between the ways the two versions interact with their views. In the Angular version, there's no Two-Way Databinding, so again the HTML must be manually wired up to shuffle data in both directions. In this case, it creates a direct coupling between the view and view-model in the Angular version because the lack of Two-Way Databinding forces the developer to pass the HTMLInputElement instance to the view-model in order to retrieve it's value. In the Aurelia version, we just bind the value. This results in more re-usable code, but also a much easier to test interface, as no faking/mocking of HTML elements is required for the Aurelia version.

Conclusion

Porting the Angular 2.0 app to Aurelia was pretty straight forward. Aurelia Core Team Member, Patrick Walters, who helped to put this article together, said it took him about 8 minutes to do the port. As you can see, in every case we deleted great swathes of JavaScript code. If you look back over the Aurelia versions, you'll also notice something I think is very important. There's not a single reference to the Aurelia framework in any of the JavaScript. It's all just plain ES2015. No framework intrusion. That's a big contrast to the Angular version.

Hopefully you can also see how Aurelia's templating language ports in a pretty straight forward manner, but that doing so improves the readability and platform compatibility of the markup. Even though this example uses very little in terms of forms input, you can start to get the feel for how modern Two-Way Databinding can also reduce code and markup and simplify the implementation process.

We hope this has been informative. We didn't fabricate any examples for this post, but instead started with a recent Angular 2.0 sample app. We then ported it to see what the process was like, hoping to show you the elegance of Aurelia as well as the rationale and side-effects of some of the different design decisions we've made along the way.

Footnote

We don't intend to constantly do these sorts of Angular comparison posts. It's not fun for us and we don't want to have a reputation for doing this all the time. If you are wondering why we have done this, it's very simple. There's a huge demand for it. We receive a constant stream of requests through email, at conferences and in person to talk about how Aurelia differs from Angular 2.0. We get asked to come to conferences and user groups to speak about this, asked to write blog posts and asked to prepare private internal presentations for various organizations. This post is an attempt to provide some simple answers to a large group of people who has a critical interest in this topic.



posted @ 2016-04-16 11:07  张同光  阅读(86)  评论(0编辑  收藏  举报