Introduction
One of the great things about Aurelia is how it simply gets out of your way and allows you to focus on your own code. Another thing that's great about Aurelia is that it makes it easy to write your very own Todo list application. And let's be honest, Todo list applications are about 90% of the reason that all modern JavaScript frameworks exist.
I kid.
As many who have met me at various conferences will know, I have extensive experience as a .Net developer. This means I've spent a ton of time learning .Net concepts. One of the common things in the .Net space when introducing new concepts is doing it within a console application. A console application allows the reader to focus only on what is being taught, without needing to think about how things work within an ASP.Net MVC or Web API context. This gave me an idea to create a Todo list application in NodeJS. I'm a core team member for a JavaScript framework, so I have to create a new Todo list application at least every few weeks or I start to get a bit of a nervous twitch.
Getting started
I decided to use TypeScript for this application simply because I've been wanting to use it lately. I use Visual Studio Code as my preferred editor, and the tooling provided in VS Code definitely makes TypeScript development very pleasant. I also decided I would simply use ts-node
to run the application I would build, rather than set up a full build process. Since I'm not planning to distribute my Todo list console application, this just makes my life simpler.
So let's get started building our application. First, I'm going to define a Todo
class to represent each Todo item. Todos will have a description
and a done
property stating whether or not the item has been completed. I'll use a TypeScript language feature to define these properties directly in the constructor for the class. Also, I'll default done
to be false:
todo.ts
export class Todo {
constructor(public description: string, public done: boolean = false) {
}
}
That's simple enough. Now all we need to do is create a class that represents the all-important Todo list. It will have a todos
array property that contains all the Todo
items. It will also have a addTodo
and removeTodo
functions to manipulate the list of todos. Here's the code for TodoList
:
todo-list.ts
import { Todo } from './todo';
export class TodoList {
public todos: Todo[] = [];
addTodo(description: string) {
const newTodo = new Todo(description);
this.todos.push(newTodo);
}
removeTodo(todo: Todo) {
const index = this.todos.indexOf(todo);
if (index >= 0) {
this.todos.splice(index, 1);
}
}
}
That's it for the business logic of our application. We can add and remove todos. If we need to mark a todo as done/not done, we simply modify the done
property of the Todo
instance in questions.
Writing the Console Application
It turns out that writing a console Todo list application in NodeJS is a bit involved, but that's no big deal. First, we will instantiate a TodoList
object.
const todoList = new TodoList();
Next, we'll present our user with a list of actions that can be performed. It'll look something like this:
What would you like to do?
1. Add todo
2. Remove todo
3. Toggle todo done
:
Then the app can enter any of the three modes named. If the user gives an invalid input, we simply ignore the input and ask again. If the user decides to add a todo item, we will ask the user for the todo description, then we'll call todoList.addTodo
passing it the description. The UI will look like this:
Please give me a description and I'll add a todo to the list.
:
If the user decides to remove a todo item, we'll present the user with the list of todos and ask which one to remove. It should look something like this:
0. Desc: Do laundry, Done: No
1. Desc: Do dishes, Done: No
Please select an item to remove.
:
Finally, if the user decides to toggle a todo's done
property, we'll present the user with a list of todos and ask which one to toggle. It should look something like this:
0. Desc: Do laundry, Done: No
1. Desc: Do dishes, Done: No
Please select an item to toggle being done.
:
Once the user completes an action, they are returned to the "main menu," if you will. This means we need to create a loop. In C#, I would probably use a do/while
loop to do this. But Node's asynchronous nature requires a slightly different approach to this. We use the callback model to our advantage to set this up. First, we need to use the createInterface
method from the readline
module that's included with Node. We'll call createInterface
and tell it to use the standard input for its input and standard output for its output.
We'll do this by creating a file called index.ts
and putting the following code in it:
import { createInterface } from 'readline';
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
Now we'll set up callbacks to for the line
and close
callbacks. The line
callback function receives a single string parameter that is the line of input. The close
callback allows us to say goodbye to the user and clean up anything we need to cleanup and then exit the program.
rl.on('line', function (line: string) {
const input = line.trim();
handleInput(input);
prompt();
}).on('close', function () {
console.log('Have a great day!');
process.exit(0);
});
Now, you see the the handleInput
and prompt
functions being called in the line
callback. Those are helper functions I wrote to help abstract away the dirty work of actually handling the application. Let's look at them.
function handleInput(input: string) {
if (!input) {
console.log(`Unrecognized command`);
return;
}
switch (selectedPromptMode) {
case PromptMode.Main:
handleMainQuestion(input);
printTodos();
return;
case PromptMode.AddTodo:
handleAddTodo(input);
break;
case PromptMode.RemoveTodo:
handleRemoveTodo(input);
break;
case PromptMode.ToggleTodoDone:
handleToggleTodoDone(input);
break;
}
printTodos();
selectedPromptMode = PromptMode.Main;
}
function prompt() {
const prompt = `${questions[selectedPromptMode]}\n: `;
rl.setPrompt(prompt);
rl.prompt();
}
Now we're starting to get in to the meat of my code. You can tell that I have created a TypeScript enum to handle what "mode" the application is currently in. Based on the mode, I'll call a function to actually handle the input. Once the input has been handled, I'll print the list of todos and return to "Main" mode, except for when I was in "Main" mode to begin with. In that case, I simply print the todo list and return. This isn't really a scalable solution, but for this simple application, it is good enough.
I've also created an array of prompts to show the user that is ordered the same as the enum is ordered. I use this to set the prompt for the user. I exploit the ordering of the enum again to help handle user input.
Next, let's look at the remaining functions in this application.
function handleMainQuestion(input: string) {
switch (input) {
case '1':
selectedPromptMode = PromptMode.AddTodo;
break;
case '2':
selectedPromptMode = PromptMode.RemoveTodo;
break;
case '3':
selectedPromptMode = PromptMode.ToggleTodoDone;
break;
default:
console.log(`Unrecognized command`);
return;
}
printTodos();
}
function printTodos() {
let counter = 0;
console.log(``);
for (const todo of todoList.todos) {
console.log(`${counter++}. Desc: ${todo.description}, Done: ${todo.done ? 'Yes' : 'No'}`);
}
console.log(``);
}
handleMainQuestion
will check the user's input and set the prompt mode. As long as it is a valid input, it will then print the list of todos, since all of the modes that can be set in this function require displaying the list of todos. printTodos
is a helper functions to print the list of todos.
function handleAddTodo(input: string) {
if (!input) {
console.log(`You must enter a description.`);
return;
}
todoList.addTodo(input);
console.log(`Todo item added. Here's your todo list`);
}
function handleRemoveTodo(input: string) {
const selectedTodoIndex = Number(input);
if (Number.isNaN(selectedTodoIndex) || selectedTodoIndex > todoList.todos.length - 1) {
console.log(`Unrecognized command`);
return;
}
todoList.removeTodo(todoList.todos[selectedTodoIndex])
console.log(`Todo item removed. Here's your todo list\n`);
}
function handleToggleTodoDone(input: string) {
const selectedTodoIndex = Number(input);
if (Number.isNaN(selectedTodoIndex) || selectedTodoIndex > todoList.todos.length - 1) {
console.log(`Unrecognized command`);
return;
}
const selectedTodo = todoList.todos[selectedTodoIndex];
selectedTodo.done = !selectedTodo.done;
console.log(`Todo item marked ${selectedTodo.done ? 'done' : 'not done'}. Here's your todo list\n`);
}
These three functions are fairly simple. The simply add or remove todo items use the TodoList
class API, or directly toggle an individual todo list item's done
property.
So now we've created a todo list console application. Let's see how it looks when running the app:
What would you like to do?
1. Add todo
2. Remove todo
3. Toggle todo done
: 1
Please give me a description and I'll add a todo to the list.
: Do the dishes
Todo item added. Here's your todo list
0. Desc: Do the dishes, Done: No
What would you like to do?
1. Add todo
2. Remove todo
3. Toggle todo done
: 1
0. Desc: Do the dishes, Done: No
Please give me a description and I'll add a todo to the list.
: Walk the dog
Todo item added. Here's your todo list
0. Desc: Do the dishes, Done: No
1. Desc: Walk the dog, Done: No
What would you like to do?
1. Add todo
2. Remove todo
3. Toggle todo done
: 3
0. Desc: Do the dishes, Done: No
1. Desc: Walk the dog, Done: No
Please select an item to toggle being done.
: 1
Todo item marked done. Here's your todo list
0. Desc: Do the dishes, Done: No
1. Desc: Walk the dog, Done: Yes
What would you like to do?
1. Add todo
2. Remove todo
3. Toggle todo done
: 2
0. Desc: Do the dishes, Done: No
1. Desc: Walk the dog, Done: Yes
Please select an item to remove.
: 0
Todo item removed. Here's your todo list
0. Desc: Walk the dog, Done: Yes
What would you like to do?
1. Add todo
2. Remove todo
3. Toggle todo done
: Have a great day!
We had to write a bit of code to create the console app, but that's to be expected. The nature of a simple console app leads to this type of coding. It wasn't too bad at all, really. But what if I wanted to take my Todo list application and port it to Aurelia so I can fulfill my weekly quota of writing Todo list applications?
Port our app to Aurelia
First, we're going to need to create an Aurelia app. I'll use the Aurelia CLI to make this simple. I'll run au new
from my command line. If I didn't have the Aurelia CLI already installed, I could do this simply by running npm install -g aurelia-cli
. I'll accept the default app name (aurelia-app
), choose the "Default TypeScript" setup, and then create the project. Finally I'll have the CLI run npm install
for me. It'll look this:
$ au new
_ _ ____ _ ___
__ _ _ _ _ __ ___| (_) __ _ / ___| | |_ _|
/ _` | | | | '__/ _ \ | |/ _` | | | | | | |
| (_| | |_| | | | __/ | | (_| | | |___| |___ | |
\__,_|\__,_|_| \___|_|_|\__,_| \____|_____|___|
Please enter a name for your new project below.
[aurelia-app]>
Would you like to use the default setup or customize your choices?
1. Default ESNext (Default)
A basic web-oriented setup with Babel for modern JavaScript development.
2. Default TypeScript
A basic web-oriented setup with TypeScript for modern JavaScript development.
3. Custom
Select transpilers, CSS pre-processors and more.
[Default ESNext]> 2
Project Configuration
Name: aurelia-app
Platform: Web
Transpiler: TypeScript
CSS Processor: None
Unit Test Runner: Karma
Editor: Visual Studio Code
Would you like to create this project?
1. Yes (Default)
Creates the project structure based on your selections.
2. Restart
Restarts the wizard, allowing you to make different selections.
3. Abort
Aborts the new project wizard.
[Yes]> 1
Project structure created and configured.
Would you like to install the project dependencies?
1. Yes (Default)
Installs all server, client and tooling dependencies needed to build the project.
2. No
Completes the new project wizard without installing dependencies.
[Yes]> 1
This will create a aurelia-app
folder. Inside the folder is a src
folder where all of the code for our Aurelia application needs to go. I'll go ahead and copy over the todo-list.ts
and todo.ts
files to that folder. I'm going to delete app.ts
from the src
folder and rename todo-list.ts
to app.ts
. I'll then edit app.html
to look like this:
<template>
<form submit.delegate="addTodo(description)">
<input type="text" value.bind="description" />
<button type="submit">Add todo</button>
</form>
<ul>
<li repeat.for="todo of todos"
css="text-decoration: ${todo.done ? 'line-through' : 'none'}">
<input type="checkbox" checked.bind="todo.done" />
${todo.description}
<button click.delegate="removeTodo(todo)">
Remove
</button>
</li>
</ul>
</template>
Here, I'm using basic Aurelia data-binding commands to bind to the TodoList
. This HTML is all I need for my Aurelia application. You'll notice that I've created a description
property that only lives in the view. There's no need to declare it our viewmodel.
To run the app, I just cd aurelia-app
to enter the directory for the application. Then I run au run
and browse to http://localhost:9000
. Let's see what our app looks like:
Conclusions
The really neat thing to see in porting the app from Node to Aurelia is how we're able to take our business logic over completely untouched. We wrote simple classes that don't use any Node-specific modules, and they work without any changes in Aurelia. Also, Aurelia's templating and binding system made building our UI very simple. Aurelia helps you focus on your application's code, which is the most important code!