Five times the same app

Image from Pexels

Choosing a technology to create a simple web-application can be really tough. Many tools are at our disposal and each of them seems to promise us the Holy Grail. Let’s try some of them to get a better idea.


Through this article, we are going to overview different frameworks and libraries by creating the same To-Do List app five times. We are going to try Angular, Aurelia, Ember.js, React and Polymer 2. We are not going to focus on TDD or how to write tests. We are just going to create a really simple basic app to see how each of these tools work.


Angular

Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript.

Setting up a project

Installing Angular CLI

First, we need to install Angular CLI. We can do it like so:

npm install -g @angular/cli

Installing Anguar CLI with npm

Generating the project

We are now going to generate a new project:

ng new todo
cd todo

Generating the project

We now have to install the Node modules and run our project:

npm install
ng serve

Installing dependencies and running the server

Now our app is available at localhost:4200. We are going to create a class for our tasks and a service:

ng g class task
ng g service task

Creating our class and our service

Let’s code

For the sake of this example, we are also going to create mock data.

import { Task } from './task';export const TASKS: Task[] = [
{ id: 1, title: 'Take out the trash', status: false },
{ id: 2, title: 'Fix the roof', status: false },
{ id: 3, title: 'Clean the house', status: false }
];

mock-tasks.ts file

Now, let’s define first our Task class:

export class Task {
id: number;
title: string;
status: boolean;
constructor(id?: number, title?: string, status?: boolean) {
this.id = id;
this.title = title || '';
this.status = status || false;
}
}

task.ts file

Now we can create our service:

import { Injectable } from '@angular/core';import { Task } from './task';
import { TASKS } from './mock-tasks';
@Injectable()
export class TaskService {
tasks: Task[] = TASKS; constructor() { } getTasks(): Promise<Task[]> {
return Promise.resolve(this.tasks);
}
add(title: string): Promise<Task> {
return new Promise(() => {
const newTask = new Task(this.tasks.length + 1, title, false);
this.tasks.push(newTask);
return newTask;
}
);
}
delete(task: Task): Promise<number> {
return new Promise(() => {
const index = this.tasks.findIndex((obj => obj.id === task.id));
this.tasks.splice(index, 1);
return task.id;
}
);
}
changeStatus(task: Task): Promise<Task> {
return new Promise(() => {
task.status = true;
return task;
}
);
}
}

task.service.ts file

Now that our main class and our service are ready, we can set our app.component.ts like so:

import { Component, OnInit } from '@angular/core';import { Task } from './task';
import { TaskService } from './task.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [TaskService]
})
export class AppComponent implements OnInit { title = 'To-do List';
tasks: Task[];
constructor(private taskService: TaskService) { } ngOnInit(): void {
this.getTasks();
}
getTasks(): void {
this.taskService.getTasks().then(tasks => this.tasks = tasks);
}
addTask(title: string): void {
title = title.trim();
if (!title) { return; }
this.taskService.add(title)
.then(task => {
this.tasks.push(task);
});
}
deleteTask(task: Task): void {
this.taskService.delete(task)
.then(() => {
const index = this.findTaskIndex(task.id);
this.tasks.splice(index, 1);
});
}
changeStatus(task: Task): void {
this.taskService.changeStatus(task)
.then(updatedTask => {
const index = this.findTaskIndex(task.id);
this.tasks[index] = updatedTask;
});
}
findTaskIndex(id: number): number {
return this.tasks.findIndex((obj => obj.id === id));
}
}

app.component.ts.file

And finally, we can define our component’s template:

<h1>{{title}}</h1><div>
<label>New task:</label> <input #taskTitle />
<button (click)="addTask(taskTitle.value); taskTitle.value=''">
Add
</button>
</div>
<h2>Tasks</h2>
<ul class="tasks">
<li *ngFor="let task of tasks">
{{task.id}} - {{task.title}}
<div *ngIf="task.status; else notDoneBlock">Done</div>
<ng-template #notDoneBlock>
<p>Not done<br />
<button (click)="changeStatus(task)">Done!</button></p>
</ng-template>
<p><button (click)="deleteTask(task)">Delete</button></p>
</li>
</ul>

app.component.html

Checkpoint

If we go to localhost:4200, we can note that we have a really basic app where we can see some default tasks, create new ones and delete some others. We can argue that our code isn’t good enough, that there is no data persistence or that the tests are missing. But that is not the point.

With this simple project, we saw how we can initialize an Angular project and how it is structured. We can also see that with just a few lines of code written with TypeScript, we are able to generate something very quickly. We now have a better idea of how much time we need to set up a basic project and how we can go further.

Building for production

We can build our project for production like so:

ng build -prod --base-href ./

Building for production

This last command will generate a bundle for us.

Some numbers

GitHub

Angular: 481 contributors; 144 releases; 8147 commits; 1551 issues

Angular CLI: 270 contributors; 100 releases; 1698 commits; 419 issues

Stack Overflow

Angular: 61654 tagged questions


Aurelia

Aurelia is a JavaScript client framework for mobile, desktop and web leveraging simple conventions.

Setting up a project

Installing Aurelia CLI

First, we need to install Aurelia CLI. We can do it like so:

npm install aurelia-cli -g

Installing Aurelia CLI

Generating the project

We are now going to generate a new project:

au new todo
cd todo

Generating the project

A list of questions will be presented to us. For the sake of this example, we will select default values. We can now run the project that will be available at localhost:9000 with the following command:

au run --watch

Running the server

Let’s code

For the sake of this example, we are also going to create mock data.

export const TASKS = [
{ id: 1, title: 'Take out the trash', status: false },
{ id: 2, title: 'Fix the roof', status: false },
{ id: 3, title: 'Clean the house', status: false }
];

mock-tasks.js file

Now, we are going to create a task.js file:

export class Task {
constructor(id, title) {
this.id = id;
this.title = title;
this.status = false;
}
}

task.js file

We also need to create a service:

import { Task } from './task';
import { TASKS } from './mock-tasks';
export class TaskService { tasks = TASKS; constructor() { } getTasks() {
return Promise.resolve(this.tasks);
}
add(title) {
return new Promise(() => {
const newTask = new Task(this.tasks.length + 1, title, false);
this.tasks.push(newTask);
return newTask;
}
);
}
delete(task) {
return new Promise(() => {
const index = this.tasks.findIndex((obj => obj.id === task.id));
this.tasks.splice(index, 1);
return task.id;
}
);
}
changeStatus(task) {
return new Promise(() => {
task.status = true;
return task;
}
);
}
}

task-service.js file

Now we can fill our app.js file:

import { Task } from './task';
import { TaskService } from './task-service';
import { inject } from 'aurelia-framework';
@inject(TaskService)
export class App {
constructor(taskService) {
this.taskService = taskService;
this.title = 'To-do List';
this.tasks = [];
this.taskTitle = '';
}
created() {
this.taskService.getTasks().then(tasks => this.tasks = tasks);
}
addTask() {
const title = this.taskTitle.trim();
if (!title) { return; }
this.taskService.add(title)
.then(task => {
this.tasks.push(task);
this.taskTitle = '';
});
}
deleteTask(task) {
this.taskService.delete(task)
.then(() => {
const index = this.findTaskIndex(task.id);
this.tasks.splice(index, 1);
});
}
changeStatus(task) {
this.taskService.changeStatus(task)
.then(updatedTask => {
const index = this.findTaskIndex(task.id);
this.tasks[index] = updatedTask;
});
}
findTaskIndex(id) {
return this.tasks.findIndex((obj => obj.id === id));
}
}

app.js file

Finally, we have to edit our template app.html like so:

<template>
<h1>${title}</h1>
<form submit.trigger="addTask()">
<input type="text" value.bind="taskTitle">
<button type="submit">Add</button>
</form>
<ul>
<li repeat.for="task of tasks">
${task.id} - ${task.title}
<div show.bind="!task.status">
<p>Not done<br />
<button click.trigger="changeStatus(task)">Done!</button></p>
</div>
<div show.bind="task.status">
<p><button click.trigger="deleteTask(task)">Delete</button></p>
</div>
</li>
</ul>
</template>

app.html file

Checkpoint

If we go to localhost:9000, we can note that we have a really basic app where we can see some default tasks, create new ones and delete some others. In fact, our app is pretty similar to the one we created with Angular.

With this simple project, we saw how we can initialize an Aurelia project and how it is structured. We now have a better idea of how much time we need to set up a basic project and how we can go further. In fact, it was pretty easy because, even here if we don’t use TypeScript but only JavaScript, our code is quite similar to our previous app.

Building for production

We can build our project for production like so:

au build --env prod

Building for production

This last command will generate a bundle for us.

Some numbers

GitHub

Aurelia: 86 contributors; 85 releases; 572 commits; 54 issues

Aurelia CLI: 53 contributors; 38 releases; 834 commits; 93 issues

Stack Overflow

Aurelia: 2370 tagged questions


Ember.js

Ember.js is an open-source JavaScript web framework, based on the Model–view–viewmodel (MVVM) pattern. Although primarily considered a framework for the web, it is also possible to build desktop and mobile applications in Ember.

Setting up a project

Installing Ember CLI

First, we need to install Ember CLI. We can do it like so:

npm install -g ember-cli

Installing Ember CLI

Generating the project

We are now going to generate a new project and run it:

ember new todo
cd todo
ember serve

Generating the project and running the server

Our app will be available at localhost:4200. Now we are going to generate what we need:

ember g model task title:string status:boolean
ember g route tasks --path '/'
ember g route tasks/index
ember g http-mock tasks
ember g adapter application
ember generate serializer application
ember g controller tasks

Generating model, routes, adapter, serializer and controller

Let’s code

Let’s start by creating some fake data. We are going to place what we need into the file server/mocks/tasks.js:

...
tasksRouter.get('/', function(req, res) {
res.send({
'tasks': [
{ taskId: 1, title: 'Take out the trash', status: false },
{ taskId: 2, title: 'Fix the roof', status: false },
{ taskId: 3, title: 'Clean the house', status: false }
]
});
});
...

server/mocks/tasks.js files

Our Task model should look like so:

import DS from 'ember-data';export default DS.Model.extend({
title: DS.attr('string'),
isCompleted: DS.attr('boolean')
});

models/tasks.js file

In our controllers/tasks.js file, we are going to put the following code:

import Ember from 'ember';export default Ember.Controller.extend({  actions: {    addTask() {
const title = this.get('newTitle');
if (!title.trim()) { return; }
const id = this.get('model.length') + 1 + new Date().getTime();
const task = this.store.createRecord('task', {
id,
title,
status: false
});
this.set('newTitle', '');
task.save();
},
deleteTask(id) {
this.store.findRecord('task', id).then((task) => task.destroyRecord());
},
changeStatus(id){
this.store.findRecord('task', id).then((task) => task.set('status', true));
}
}

});

controllers/tasks.js file

Now we have to define our Routes. First, in our routes/tasks.js file, let’s place the following code:

import Ember from 'ember';export default Ember.Route.extend({    model: function() {
return this.store.findAll('task');
}
});

routes/tasks.js file

Our router.js file should also look like this:

import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
this.route('tasks', { path: '/' }, function() {});
});
export default Router;

router.js file

Now, we have to define our Adapter and our Serializer. Let’s do it like so:

import DS from 'ember-data';export default DS.RESTAdapter.extend({
contentType: 'application/json',
dataType: 'json',
namespace: 'api'
});

adapters/application.js file

import DS from 'ember-data';export default DS.RESTSerializer.extend({
primaryKey: 'taskId'
});

serializers/application.js file

We are almost done. We just have to set our templates.

{{outlet}}

templates/application.hbs

<h1>To-do List</h1><div>
<label>New task:</label> {{input type="text" value=newTitle}}
<button {{action "addTask"}}>Add</button>
</div>
<h2>Tasks</h2>
<ul class="tasks">
{{#each model as |task|}}
<li>
{{task.id}} - {{task.title}}
{{#if task.status}}
<p><button {{action "deleteTask" task.id}}>Delete</button></p>
{{else}}
<p>Not done<br />
<button {{action "changeStatus" task.id}}>Done!</button></p>
{{/if}}
</li>
{{/each}}
</ul>

templates/tasks.hbs

Checkpoint

If we go to localhost:4200, we can note that we have a really basic app where we can see some default tasks, create new ones and delete some others.

With this simple project, we saw how we can initialize an Ember.js project and how it is structured. We now have a better idea of how much time we need to set up a basic project and how we can go further. We can also see that, even if it has common concepts with the other frameworks, Ember.js is significantly different and we need to take another approach.

Building for production

We can build our project for production like so:

ember build

Building for production

This last command will generate a bundle for us.

Some numbers

GitHub

Ember.js: 673 contributors; 255 releases; 14762 commits; 242 issues

Ember CLI: 369 contributors; 140 releases; 7546 commits; 151 issues

Stack Overflow

Ember.js: 21528 tagged questions


React

React is a JavaScript library for building user interfaces. React allows us to create large web-applications that use data that can change over time, without reloading the page. It aims primarily to provide speed, simplicity and scalability. React processes only user interfaces in applications. This corresponds to View in the Model-View-Controller (MVC) template, and can be used in combination with other JavaScript libraries or frameworks in MVC.

Setting up a project

Installing Create React App

To start our project, we need to install Create React App. We can do it like so:

npm install -g create-react-app

Installing Create React App

Generating the project

We are now going to generate a new project and run it:

create-react-app todo
cd todo
npm start

Generating the project and running the server

Our project is now available at localhost:3000.

Let’s code

For the sake of this example, we are also going to create mock data. Let’s create a file mock/Tasks.js:

export const TASKS = [
{ id: 1, title: 'Take out the trash', status: false },
{ id: 2, title: 'Fix the roof', status: false },
{ id: 3, title: 'Clean the house', status: false }
];
export default TASKS;

mock/Tasks.js file

Now we have to define a model for Task and an Interface like so:

function guidGenerator() {
function S4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return S4()+S4()+'-'+S4()+'-'+S4()+'-'+S4()+'-'+S4()+S4()+S4();
}
export default class Task { constructor(title, status, id) {
this.title = title || '';
this.status = status || false;
this.id = id || guidGenerator();
}

}

lib/Task.js file

import Task from './Task';
import TASKS from '../mock/Tasks'
import { findIndex } from 'lodash';export default class TaskDataInterface {
constructor() {
this.tasks = TASKS;
}
addTask(title) {
const newTask = new Task(title);
this.tasks.push(newTask);
return newTask;
}
changeStatus(taskId) {
const taskIndex = findIndex(this.tasks, (task) => task.id === taskId);
if (taskIndex > -1) {
this.tasks[taskIndex].status = !this.tasks[taskIndex].status;
}
}
deleteTask(taskId) {
const taskIndex = findIndex(this.tasks, (task) => task.id === taskId);
if (taskIndex > -1) {
this.tasks.splice(taskIndex, 1);
}
}
getAllTasks() {
return this.tasks.map(task => task);
}
}

lib/TaskDataInterface.js file

We can now create two components: one for the list itself and another for each item.

import React from 'react';
import SingleTask from './SingleTask'
export default class TasksList extends React.Component {
render() {
return (
<div>
<ul>
{this.props.tasks.map(
(task) =>
<SingleTask
key={task.id}
taskId={task.id}
title={task.title}
status={task.status}
changeStatus={this.props.changeStatus}
deleteTask={this.props.deleteTask}
/>
)}
</ul>

</div>
);
}
}

components/TasksList.js file

import React from ‘react’;

export default class TaskTask extends React.Component {
render() {
return (
<li>
{this.props.taskId} - {this.props.title} {this.props.status ? ( <p><button
onClick={() => this.props.deleteTask(this.props.taskId)}>
Delete
</button></p>
) : ( <p>Not done<br />
<button
onClick={() => this.props.changeStatus(this.props.taskId)}>
Done!
</button></p>
)}

</li>
);
}
}

components/SingleTask.js file

We can now edit the App.js and index.js files like so:

import React, { Component } from 'react';
import './App.css';
import TasksList from './components/TasksList';class App extends Component { constructor(props) {
super(props);
this.state = {
tasks: this.props.dataInterface.getAllTasks()
};
}
addTask = () => {
if (this._taskInputField.value) {
this.props.dataInterface.addTask(this._taskInputField.value);
this.setState({tasks: this.props.dataInterface.getAllTasks()});
this._taskInputField.value = '';
}
}
changeStatus = taskId => {
this.props.dataInterface.changeStatus(taskId);
this.setState({tasks: this.props.dataInterface.getAllTasks()});
}
deleteTask = taskId => {
this.props.dataInterface.deleteTask(taskId);
this.setState({tasks: this.props.dataInterface.getAllTasks()});
}
render() { return (
<div>
<h1>To-do List</h1>
<div>
<label>New task: </label>
<input
type="text"
ref={(c => this._taskInputField = c)}
/>
<button onClick={this.addTask}>Add</button>
</div>
<h2>Tasks</h2>
<TasksList
tasks={this.state.tasks}
changeStatus={this.changeStatus}
deleteTask={this.deleteTask}
/>
</div>
);
}
}export default App;

App.js file

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import TaskDataInterface from './lib/TaskDataInterface';
import App from './App';
import registerServiceWorker from './registerServiceWorker';const taskDataInterface = new TaskDataInterface();
ReactDOM.render(<App dataInterface={taskDataInterface} />, document.getElementById('root'));
registerServiceWorker();

index.js file

Checkpoint

If we go to localhost:3000, we can note that we have a really basic app where we can see some default tasks, create new ones and delete some others.

With this simple project, we saw how we can initialize a React project and how it is structured. We now have a better idea of how much time we need to set up a basic project and how we can go further. We can also see, even if it has common concepts with the other frameworks, how React differs and how, with just a few lines of code, we can quickly achieve something.

Building for production

We can build our project for production like so:

npm run build

Building for production

This last command will generate a bundle for us.

Some numbers

GitHub

React: 1039 contributors; 63 releases; 8811 commits; 580 issues

Create React App: 346 contributors; 127 releases; 1143 commits; 216 issues

Stack Overflow

React: 51656 tagged questions


Polymer 2

Polymer is an open-source JavaScript library for building web applications using web components. Polymer lets us build encapsulated, reusable elements that work just like standard HTML elements, to use in building web applications.

Setting up a project

Installing Polymer CLI

To start our project, we need to install Polymer CLI. We can do it like so:

npm install -g polymer-cli

Installing Polymer CLI

Generating the project

We are now going to generate a new project and run it:

cd todo
polymer init
polymer serve

Generating the project and running the server

During the process, we are going to be asked a few questions. In particular, we have to choose a template. We are going to select polymer-2-application. If everything is alright, our project will be available at localhost:8081.

Let’s code

For the sake of this example, we are also going to create mock data. Let’s create a file mock/tasks.js:

const TASKS = [
{ id: 1, title: 'Take out the trash', status: false },
{ id: 2, title: 'Fix the roof', status: false },
{ id: 3, title: 'Clean the house', status: false }
];

mock/tasks.js file

Now we are going to fill our todo-app.html file like so:

<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<script src="mock/tasks.js"></script><dom-module id="todo-app">
<template>
<style>
:host {
display: block;
}
</style>
<h1>[[title]]</h1>
<div>
<label>New task:</label> <input type="text" id="newTask" value="{{term::input}}" />
<button on-click="addTask">
Add
</button>
</div>
<h2>Tasks</h2> <ul>
<template id="list" is="dom-repeat" items="[[tasks]]">
<li>
{{item.id}} - {{item.title}}
<template is="dom-if" if="{{item.status}}">
<p><button on-click="deleteTask">Delete</button></p>
</template>
<template is="dom-if" if="{{!item.status}}">
<p>Not done<br />
<button on-click="changeStatus">Done!</button></p>
</template>

</li>
</template>
</ul>
</template> <script> class TodoApp extends Polymer.Element { static get is() { return 'todo-app'; } static get properties() { return { title: {
type: String,
value: 'To-do List'
},
term: {
type: String,
value: ''
},
tasks: {
type: Array,
value: []
}
}; } ready() {
super.ready();
this.set('tasks', TASKS);
}
addTask() {
this.term = this.term.trim();
if (!this.term) { return; }
this.push('tasks', {id: this.guidGenerator(), title: this.term, status: false});
this.term = '';
}
deleteTask(e) {
const index = this.findTaskIndex(e.model.item.id);
this.splice('tasks', index, 1);
}
changeStatus(e) {
const index = this.findTaskIndex(e.model.item.id);
const oldTask = this.tasks[index];
this.set(`tasks.${index}.status`, true);
}
findTaskIndex(id) {
return this.tasks.findIndex((obj => obj.id === id));
}
guidGenerator() {
function S4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return S4()+S4()+'-'+S4()+'-'+S4()+'-'+S4()+'-'+S4()+S4()+S4();
}
} window.customElements.define(TodoApp.is, TodoApp);
</script>
</dom-module>

todo-app.html file

Checkpoint

If we go to localhost:8081, we can note that we have a really basic app where we can see some default tasks, create new ones and delete some others.

With this simple project, we saw how we can initialize a Polymer project and how it is structured. We now have a better idea of how much time we need to set up a basic project and how we can go further. We can also see that, even if it has common concepts with the other frameworks, Polymer has a different philosophy.

Building for production

We can build our project for production like so:

polymer build

Building for production

This last command will generate a bundle for us. Most optimizations are disabled by default. To make sure the correct build enhancements are always used, we have to provide a set of build configurations via the “builds” field of our polymer.json file:

"builds": [{
"bundle": true,
"js": {"minify": true},
"css": {"minify": true},
"html": {"minify": true}
}]

polymer.json file

Some numbers

GitHub

Polymer: 119 contributors; 115 releases; 5433 commits; 668 issues

Polymer CLI: 34 contributors; 46 releases; 576 commits; 164 issues;

Stack Overflow

Polymer: 7105 tagged questions

Conclusion

How can we conclude this article and can we really do it? That is a good question. Through our different tryouts we played with different tools that share same concepts, but have a different philosophy. Unfortunately for us, there is no magic formula that will tell us which one to pick.

It is sad, but true. We have to try the tools that we think could fit to our needs, then compare them on various points. Aside of performance and ease of use, looking at the community that may have grown around a specific tool can be a relevant point.

Mátyás Lancelot Bors

Written by

WebDeveloper / Writer / Musician / https://www.mlbors.com / https://medium.com/@mlbors

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade