Small comparison of ionic2 and Aurelia + Framework7 for hybrid mobile applications

Update (2017-08-14): I finally published the part on Aurelia UX. See it here.

Updates (2017-01-24):

I spent some time recently to develop a mobile application with the ionic framework. This framework allows you to create mobile applications for Android and iOS with HTML5 and CSS3 and AngularJS. Which means you can develop native like applications with the ease of development of a web application: debug in a browser, use Javascript and a framework you probably already know and that's it! No java or Swift, just the power of the web. The application is then build for your phone with Apache Cordova.

Since they have a version 2 coming soon based on Angular2, I decided to use directly this version (currently in beta).

However, on a another project, I use Aurelia a JavaScript framework built with ES6 and around web standards aimed to create Single Page Applications easily. And I must say I love it. It embraces ES6 and the new possibilities offered by the language and its use of convention over configuration makes it easy to work with.

So I wanted to try to build a mobile application with it. There are plans for a commercial addon called Aurelia Interface. Sadly there is not much information about it available apart from that video. After a small discussion on Aurelia's gitter, Scalpal suggested to look at Framework7 an HTML/CSS (with small bits of JavaScript) framework to do pretty much the same thing as ionic without the dependency on a particular framework. That's what I did and it turned out to work well with Aurelia.

Let's test!

Application features

To compare the two possibilities, I wanted to do a basic todo list applications that can:

  • List all the saved todos (home page)
  • Edit an existing todo
  • Add a todo
  • Mark a todo as done on the home page. Done todos must have their title strike-through on the home page.

All todos must be stored in the permanent SqlStorage of the phone.

In additions to these features, both applications must work on a browser (for debug and development purposes), on an android emulator and on an android phone.

Here is how the home page looks like (Aurelia + Framework7 on the left, Ionic2 + Angular2 on the right):

main page of the application

And the todo page (Aurelia + Framework7 on the left, Ionic2 + Angular2 on the right):

Page to view and edit todo

The complete code of the applications is available on github under the MIT license. Normally, the READMEs are complete enough to let you try the applications. If you encounter any problem, leave a comment or open an issue!

Ionic2

I wrote this version in TypeScript a language made by Microsoft to ease the development of huge frontend applications and not ES6 since it is the recommended way for Angular2 applications (Angular2 is itself written in TypeScript).

To organize the application, I followed the standard structure proposed by ionic:

  • An app folder with the code. In this app folder:
    • each pages (JS, HTML and eventually SASS) are grouped in a folder within pages.
    • Global theme SASS files are in the theme folder.
    • Boostrap (app.ts) and utils (models.ts) are directly in app.
  • A build folder used for debugging and by Cordova to generate the final application. It contains the index.html generated by ionic and will contain all the built files.

First, let's talk of models.ts. It only contains one class and is used in the application as a type for the TypeScript compiler (and completion).

export class Todo {
    public id: number;
    public title: string;
    public description: string;
    public done: boolean;
}

In app.ts the application is bootstrapped. It is generated by ionic. I only changed the value of MyApp#rootPage so its value is ListTodosPage the JavaScript class that defines my root page.

That leads us to the first important concept of Ionic2: navigation doesn't work with URLs. As you'll see if you launch the app, the URL is never updated. To navigate between pages, you use a NavController object and pass the page on which you want to go, like that:

this.nav.push(TodoPage)

A page being a class decorated with @Page a decorator that comes from the ionic framework.

On the main page, we display the navbar and the list of all todos. To do that, you use in your template the ion-navbar element. This allows you to easily customize the navbar for each page. Ionic will also automatically add a Back button in this navbar when you navigate to another page.

The main content of the page must be inside the ion-content element. Almost all the elements you will put in it are defined by ionic to help you and starts with ion.

You can also add Angular2 markup:

  • if the tag starts by a star (*), it's a template, like *ngFor="#todo of todos, #index = index". Note that the syntax is particular: to create a variable in a template, you must put a pound sing (#) before the name of the variable.
  • if the tag is between parenthesis it is bound to an event like (click)="viewTodo(todo)". The value of the tag will be executed when the event occurs.
  • if the tag is between bracket like [class]="getClass(todo)" it means its value is one way bound to the expression you pass.
<ion-navbar *navbar>
       <ion-title>TODO list</ion-title>
</ion-navbar>

<ion-content padding>
       <ion-list>
           <ion-item *ngFor="#todo of todos, #index = index">
               <ion-label [class]="getClass(todo)" (click)="viewTodo(index)">{{ todo.title }}</ion-label>
               <button item-right (click)="markTodoDone(todo)">Toggle done</button>
           </ion-item>
       </ion-list>

       <button full round (click)="addTodo()">Add</button>
</ion-content>

On the TypeScript side, we have a class ListTodosPage decorated by @Page. This decorator is only used here to defined the template used for the page but it is also used to define which pipes (filters in Angular2 terminology) and directives are available in the template.

In the constructor, we create the SQL database if it doesn't exist and retrieve the todos:

this.storage.query('CREATE TABLE IF NOT EXISTS todos(id integer primary key unique, title VARCHAR(10), description TEXT, done BOOLEAN)')
  .then(() => this.storage.query('SELECT * FROM todos'))
  .then(results => {
      for (let result of results.res.rows) {
          console.log(result)
          this.todos.push(result);
      }
  })
  .catch(err => console.log(err));

To view a particular todo, we use the navigation controller with navigation parameters:

this.nav.push(TodoPage, {
    todo: this.todos[index],
    index: index,
});

To add a todo, we just omit the navigation parameter.

We can then retrieve the todo on the TodoPage like this:

constructor(private nav: NavController, navParams: NavParams) {
     if (navParams.data.todo) {
         this.todo = navParams.data.todo;
     } else {
         this.todo = new Todo();
     }
}

Once we have our todo (the one we want to edit or a new one), we can create the form and bind its default value to the existing todo. This way, if we have an existing todo, the from will be prefilled with the correct values.

I must say that forms in Angular2 are a huge beast and I am not sure to fully see the interest of such a complex system (at least in most cases). The programming API and the template tags are powerful and allow you to create fine grained validators to suit your needs but they are hard to understand at first. You have a simple syntax which suffers from lack of functionality and a powerful and verbose one (which I use here). Let's look at the code:

let group: {[key: string]: Control} = {
    title: new Control(this.todo.title, Validators.compose([Validators.required])),
    description: new Control(this.todo.description),
    done: new Control(this.todo.done),
};

this.todoForm = new ControlGroup(group);
this.title = this.todoForm.controls['title'];
this.description = this.todoForm.controls['description'];
this.done = this.todoForm.controls['done'];

First we defined the group of input we want. We give them a default value and eventually the validator use to determine if the value is correct or not. Then we create the form and extract from it the controls that will be used in the template for validation.

Now we can look at the template. I won't dig into it. If you want to learn more about forms in Angular2, visit this page.

Just see how we use the controls to see if a field is correct or not and note that we use the [ngFormModel] tag to bind the form to the proper form object.

<form [ngFormModel]="todoForm" (ngSubmit)="save(todoForm.value)">
     <ion-item [class.error]="!title.valid && title.touched">
         <ion-label floating>Title</ion-label>
         <ion-input type="text" value="" [ngFormControl]="title"></ion-input>
     </ion-item>
     <div *ngIf="title.hasError('required') && title.touched"
         class="error-box">
             * Title is required!
     </div>
     <ion-item [class.error]="!description.valid && description.touched">
         <ion-label floating>Description</ion-label>
         <ion-textarea type="text" value="" [ngFormControl]="description"></ion-textarea>
     </ion-item>
     <ion-item [class.error]="!done.valid && done.touched">
         <ion-label>Done</ion-label>
         <ion-checkbox [ngFormControl]="done"></ion-checkbox>
     </ion-item>

     <button full round danger type="reset" (click)="cancel()">Cancel</button>
     <button full round type="submit" [disabled]="!todoForm.valid">Save</button>
 </form>

We can then save the todo with an SQL query and parameters replacement:

this.storage.query(
    'UPDATE todos SET title = ?, description = ?, done = ? WHERE id = ?',
    [todoValues.title, todoValues.description, todoValues.done, this.todo.id]
)

Aurelia and Framework7

I decided to use webpack (an official skeleton is available) instead of the default SystemJS. Two reasons for that:

  1. It's a good way to test webpack support
  2. During my initial research I found that people had problems with SystemJS and cordova. Since webpack is used by ionic2, I expected it to work well.

I followed the same structure than for ionic: each pages in app/pages/<pagename>, services in app/services and bootstrap (main.js and app.js directly in the app folder.

The HTML code in index.html and app.html is almost a copy/paste from the getting started of Framework7. There is just one point in app.html that is different: in the getting started, the code is design for iOS. To use the material design stylesheet (like I did), replace <div class="pages navbar-through toolbar-through"> by <div class="pages navbar-fixed toolbar-through">. Otherwise, the content of the page will partially be in the navbar (due to a missing padding-top: 46px).

main.js contains nothing particular, it's just the standard main provided by the skeleton.

Since navigation in the app will rely on the Aurelia router, we have to defines our route in app.js:

configureRouter(config, router) {
     config.title = 'Aurelia';
     config.map([
         {
             route: ['', 'todos'],
             name: 'todos',
             moduleId: 'pages/list-todos/list-todos',
             nav: true,
             title: 'todos'
         }, {
             route: ['todo', 'todo/:id'],
             name: 'todo',
             moduleId: 'pages/todo/todo',
             nav: true,
             title: 'View Todo',
         }
     ]);

     this.router = router;
 }

And to navigate, I use either:

this.router.navigateToRoute('todo', {id: todo.id});

To view/edit a precise todo. This allows me to get the todo with the given id from storage. To add a new todo, I use:

this.router.navigate('/todo');

To navigate to the route without the parameter.

Concerning the templates, nothing fancy. Just think to put your code in a div with either the list-block or the content-block class depending on what you want to do:

<ul>
    <li class="item-content" repeat.for="todo of todos" click.delegate="viewTodo(todo)">
       <div class="item-inner ${todo.done === true || todo.done === 'true' ? 'done' : ''}">
           ${todo.title}
       </div>
       <p><a href="#" class="button button-fill" click.delegate="toggleDone(todo)">Toggle Done</a></p>
    </li>
</ul>

We use Aurelia's binding mechanism to make the link with the JavaScript:

  • We repeat the element with repeat.for="todo of todos"
  • Bind a click event to a function with click.delegate
  • Use string interpolation to display the values with ${todo.title} or to update the value of a tag with class="item-inner ${todo.done === true || todo.done === 'true' ? 'done' : ''}". The part from ${ to } will be replaced by the value of the expression computed by Aurelia.

The main difference with the ionic app is that I used localForage in a custom class to store the todos. localForage is a small library by Mozilla that uses localStorage if neither IndexedDB nor WebSQL is supported with an API close the localStorage. With ionic, you have a simple way to use SQLite on the phone and WebSQL on your browser with nice capabilities for SQL queries but that's a ionic thing. Since behind the scenes, it relies on Cordova, it should be possible to use it with Aurelia and Framework7 but it felt overkill for my small test.

On the page to edit todos, nothing fancy. I create a nice looking form with the good classes. The structure is a little bit complicated but is necessary to have the proper theme. The rest is just classic html (here the code for one field):

<li>
    <div class="item-content">
        <div class="item-inner">
            <div class="item-title label">Title</div>
            <div class="item-input">
                <input type="text" placeholder="Title" value.bind="todo.title" required>
            </div>
        </div>
    </div>
</li>

Just note that in forms, Aurelia will do two way data bindings with value.bind whereas everywhere else it will do one way data bindings by default for performance reasons.

In the JavaScript, we just have to load the proper todo:

activate(params) {
     this.todo = {};
     if (params.id) {
         this.storage.getTodo(params.id)
             .then(todo => this.todo = todo);
     }
 }

To allow the todo to be saved, I do a basic check on the title (it must not be empty). No message saying that the field is required if the user touches it is displayed. That's the other import difference with ionic. It is due to a bug in aurelia-validation about how translations are loaded with webpack. This is clearly behind what I did in ionic but I'll update the code and this article once it is fixed.

Update: I created a new post about validation and Aurelia. The code samples used in it are based on this application You can see it here.

get canSave() {
     return this.todo.title !== undefined && this.todo.title.length > 0;
 }

Bonus

It's not useful for my application, but I wanted to show how to use Framework7's JavaScript should you need it. In a javascript file (you can name it f7.js) put the code below:

// Load JS for webpack
import 'framework7';


// Create F7 object
export const F7 = new Framework7({
    material: true,
});

You can then use it as you would expect:

import { F7 } from 'f7'
import {inject} from 'aurelia-framework';

@inject(F7)
export class ListTodosPage {

  constructor(f7) {
     this.f7 = f7;
  }
}

And in your HTML:

<a click.delegate="f7.openPanel('left')" href="#" class="button button-big button-fill">Open Left Panel</a>

To see a usage, go here for my code and there for the associated template.

Conclusion

  • Both application have roughly the same amount of code: 164 lines of JS and 40 lines of HTML for ionic and 143 lines of JS and 103 lines of HTML for Aurelia/Framework7. Since my storage service is about 40 lines that are handled by directly by ionic, I have about 100 lines of JS code for Aurelia. There is more HTML but it's mostly the bootstrap required for Framework7.
  • I find Aurelia lighter and easier to use. Even if typescript has some nice features like type error detection and a better auto-completion (since it can be based on the type of the object), it looks Javaish with type declarations and interfaces. In addition to that, the syntax for the template is weird (and it breaks some HTML validators like the one integrated in NetBeans IDE). I've used it for some days now, so I begin to get use to it but with my fist experience with Aurelia I didn't have this incomfortable moments at first: templating is plain and simple tag.bind (default) or tag.two-way or tag.one-way depending on what you want to do, string interpolation works everywhere and is just plain ES6 template strings.
  • ionic uses a lot of custom elements and tags. I guess the goal is to make the code less verbose, but sometime it is confusing. For instance, to make a text input field, you use <ion-input type="text" />. So logically to make a checkbox, I tried <ion-input type="checkbox" /> (which doesn't work) instead of <ion-checkbox [ngFormControl]="done" />. On the contrary, Framework7 is just plain HTML. You just have to respect a certain structure (well detailed on the documentation) and put the proper classes on your element. So it can integrate with pretty much everything and it's quite easy to learn.
  • ionic has some good bindings with Corodova. For instance its ability to use WebSQL on your browser but SQLite on your phone. There's no easy way (yet?) to do that with Aurelia/Framework7. Maybe Aurelia-Interface will have that too.
  • the startup time of the application was quite slow (several seconds) with both solutions. Maybe there is a way to improve that, I admit I haven't dug into it yet.
  • webpack is awesome ;-)

In the end, ionic has a good cli, documentation and some bindings for Cordova which make it complete and interesting. But in my opinion, the Aurelia/Framework7 feels more natural mostly because of the weird design choices of Angular2 and ionic has lots of custom element over HTML.

Maybe I should start something (skeleton and command line tool) to work with Aurelia, Framework7 and Cordova. If there's no news of Aurelia-Interface soon, I think it's highly probable that I'll try. I'll update this post if I do (or decide not to).


blogroll

social

>