Coding on a computer screen

Converting a controller function into a class

Last time we refactored an AngularJS template and controller into a component. Today, we’re going to continue our adventure in refactoring by turning the controller into a class.

Here’s what we’ve got so far:

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);
    });
  };
}

Replacing each forEach

First, we don’t need to use the angular.forEach function anymore, now that modern JavaScript has it’s own forEach method for arrays.

So instead of angular.forEach:

angular.forEach(todoList.todos, function (todo) {
  count += todo.done ? 0 : 1;
});

…we can use the forEach method directly on the todoList.todos array:

todoList.todos.forEach( function(todo) {
  count += todo.done ? 0 : 1;
});

Not much has changed but now we’re not using AngularJS for small utility functions anymore, just for providing a framework for the app.

Converting functions to arrow functions

The next step is to convert methods to arrow functions. There are good arguments I’ve heard for not converting every function to an arrow function, but in this case we’re going to want to be able to bind the controller’s methods and properties to the this keyword.

So this…

todoList.remaining = function () {
  var count = 0;
  todoList.todos.forEach( function (todo) {
    count += todo.done ? 0 : 1;
  });
  return count;
};

becomes this…

todoList.remaining = () => {
  var count = 0;
  todoList.todos.forEach( (todo) => {
    count += todo.done ? 0 : 1;
  });
  return count;
};

Using this more explicitly

One of the confusing things about the function shown above is that it’s not clear what’s bound to the this keyword. It’s very easy to miss unless you catch it at the beginning of the controller function:

function controller() {
  var todoList = this;
  ...

Now that we’ve converted functions to arrow functions, we should be able to use the this keyword directly instead of relying on a reference.

function controller() {
  this.todos = [
    { text: "learn AngularJS", done: true },
    { text: "build an AngularJS app", done: false },
  ];

  this.addTodo = () => {
    this.todos.push({ text: this.todoText, done: false });
    this.todoText = "";
  };

So in the template, $ctrl.todos is data bound to this.todos, and so forth for the other properties and methods bound to this.

Converting to a class

Converting to a class isn’t necessary for refactoring an AngularJS controller, but I’ve found it helpful in that it cleans up the code and makes it easier to distinguish between the controller’s properties and methods.

Here’s our controller now, converted to a class:

class Controller {
  constructor() {
    this.todos = [
      { text: "learn AngularJS", done: true },
      { text: "build an AngularJS app", done: false },
    ];
  }

  addTodo() {
    this.todos.push({ text: this.todoText, done: false });
    this.todoText = "";
  }

  remaining() {
    var count = 0;
    this.todos.forEach((todo) => {
      count += todo.done ? 0 : 1;
    });
    return count;
  }

  archive() {
    var oldTodos = this.todos;
    this.todos = [];
    oldTodos.forEach((todo) => {
      if (!todo.done) this.todos.push(todo);
    });
  }
}

First, you might notice that the methods look differently as they are now class methods and not arrow functions anymore. Also, instead of having the properties listed with the methods, they’re inside the constructor which makes it a little easier to read.

From the front end, this app runs exactly the same as before. But it’s been refactored to make it easier to read, maintain, and hopefully translate into another JavaScript framework or perhaps none at all.

There’s still more refactoring we can do, such as extracting logic from the controller’s methods and separating them into individual functions.