Ancient Greek temple architecture with columns

How to refactor ancient AngularJS code

So you’ve got a project that still runs on AngularJS? No, not that Angular. I’m talking about the AngularJS that is no longer supported.

Does anyone even use AngularJS anymore? Apparently, yes. According to npm trends, almost half a million download AngularJS per week.

Now if something works, why replace the framework? Because you don’t want your app running on something so old that nobody remembers how to even maintain it.

But isn’t upgrading to Angular difficult?

Yes it is. In fact, I tried to upgrade from AngularJS to Angular for an e-commerce app, and I couldn’t get it to work. I must have read through and tried to follow those instructions a dozen times. So I went a different route.

For the last three years I’ve been working on migrating a WordPress e-commerce plugin from AngularJS to React. Why React and not the newer Angular?

  1. I found the upgrade path from AngularJS to Angular so difficult that it would require rewriting most of code anyway.
  2. React ships with WordPress as part of the newer block editor and site editor.

Refactoring a todo app

So let’s take a very simple todo list app from the AngularJS homepage and see what it looks like. In the index.html, we see this:

<div ng-controller="TodoListController as todoList">
  <span>{{todoList.remaining()}} of {{todoList.todos.length}} remaining</span>
  [ <a href="" ng-click="todoList.archive()">archive</a> ]
  <ul class="unstyled">
    <li ng-repeat="todo in todoList.todos">
      <label class="checkbox">
        <input type="checkbox" ng-model="todo.done" />
        <span class="done-{{todo.done}}">{{todo.text}}</span>
      </label>
    </li>
  </ul>
  <form ng-submit="todoList.addTodo()">
    <input
      type="text"
      ng-model="todoList.todoText"
      size="30"
      placeholder="add new todo here"
    />
    <input class="btn-primary" type="submit" value="add" />
  </form>
</div>

When I started working in AngularJS back in 2013 (good grief it’s been that long), it was common for apps to have a template plopped onto an HTML page that was bound to a controller, in this case, the TodoListController. The actual function for the controller is here:

angular.module("todoApp", []).controller("TodoListController", function () {
  var todoList = this;
  todoList.todos = [
    { text: "learn AngularJS", done: true },
    { text: "build an AngularJS app", done: false },
  ];
  todoList.addTodo = function () {
    todoList.todos.push({ text: todoList.todoText, done: false });
    todoList.todoText = "";
  };
  todoList.remaining = function () {
    var count = 0;
    angular.forEach(todoList.todos, function (todo) {
      count += todo.done ? 0 : 1;
    });
    return count;
  };
  todoList.archive = function () {
    var oldTodos = todoList.todos;
    todoList.todos = [];
    angular.forEach(oldTodos, function (todo) {
      if (!todo.done) todoList.todos.push(todo);
    });
  };
});

Let me explain what this does. The controller has a todos property with two todos, and has an addTodo method for adding a new todo, a remaining method which returns the number of incomplete todos, and an archive method which filters only incomplete todos.

Converting the controller and template combination to a component

This practice of using a combination of controllers and templates gave way to directives and then to components. Components are common in the new Angular and in more modern frameworks like React and Vue. So let’s convert this to a component.

Moving the template to a new file

First, I’m going to move most of that template to new separate HTML file. If you go to my GitHub project, you can see the changes.

<span>{{$ctrl.remaining()}} of {{$ctrl.todos.length}} remaining</span>
[ <a href="" ng-click="$ctrl.archive()">archive</a> ]
<ul class="unstyled">
  <li ng-repeat="todo in $ctrl.todos">
    <label class="checkbox">
      <input type="checkbox" ng-model="todo.done" />
      <span class="done-{{todo.done}}">{{todo.text}}</span>
    </label>
  </li>
</ul>
<form ng-submit="$ctrl.addTodo()">
  <input
    type="text"
    ng-model="$ctrl.todoText"
    size="30"
    placeholder="add new todo here"
  />
  <input class="btn-primary" type="submit" value="add" />
</form>

What’s changed:

  1. The template only includes what was sandwiched between <div ng-controller=“...
  2. The instances of todoList have been renamed $ctrl. $ctrl is the default name used for binding the controller to a template, and I prefer to use it as it makes it easier to see in the code what’s bound to this in the controller.

So back on the index.html page, I can replace everything between <div ng-controller=“... with <todo-list></todo-list>, essentially creating a custom HTML element.

Converting a controller to a component

Lastly I need to convert the controller to a component:

const TodoList = {
  controller,
  templateUrl: "./todo.template.html",
};
function controller() {
  var todoList = this;
  todoList.todos = [
    { text: "learn AngularJS", done: true },
    { text: "build an AngularJS app", done: false },
  ];
  todoList.addTodo = function () {
    todoList.todos.push({ text: todoList.todoText, done: false });
    todoList.todoText = "";
  };
  todoList.remaining = function () {
    var count = 0;
    angular.forEach(todoList.todos, function (todo) {
      count += todo.done ? 0 : 1;
    });
    return count;
  };
  todoList.archive = function () {
    var oldTodos = todoList.todos;
    todoList.todos = [];
    angular.forEach(oldTodos, function (todo) {
      if (!todo.done) todoList.todos.push(todo);
    });
  };
}
angular.module("todoApp", []).component("todoList", TodoList);

If you look at the commit, not much has changed besides:

  1. A new object called TodoList has been created, which includes a controller function and templateUrl property referencing the new HTML file.
  2. The controller used for TodoList is exactly the same as what was declared for the todoApp module.
  3. Instead of declaring a TodoListController, we’re declaring a todoList component. BTW, the reason for the component name being “todoList” is because that’s how AngularJS will look for <todo-list> in the HTML. This is different from React components where the component would likely be named <TodoList />.

Next time will be shorter, and I’ll demonstrate how to convert the controller into a JavaScript class.