Angular and Command Pattern: Encapsulating Actions

In this tutorial, we will explore how to implement the Command Pattern in Angular to encapsulate actions and achieve separation of concerns. The Command Pattern is a behavioral design pattern that allows us to encapsulate a request as an object, thereby allowing us to parameterize clients with queues, requests, and operations. By using the Command Pattern, we can achieve reusability and easily implement undo/redo functionality in our Angular applications.

angular command pattern encapsulating actions

Introduction

What is Angular

Angular is a popular open-source framework for building web applications. It provides a structured and modular approach to developing front-end applications, making it easier to build scalable and maintainable code. Angular uses TypeScript as its primary programming language and follows the component-based architecture.

What is the Command Pattern

The Command Pattern is a behavioral design pattern that encapsulates a request as an object, thereby allowing us to parameterize clients with queues, requests, and operations. It decouples the sender of a request from its receiver, providing us with the flexibility to vary requests and their execution.

Benefits of Using the Command Pattern in Angular

Using the Command Pattern in Angular offers several benefits:

  • Separation of Concerns: The Command Pattern helps in separating the logic for executing a particular action from the components that trigger those actions. This separation allows us to have cleaner and more maintainable code.
  • Reusability: By encapsulating actions as commands, we can easily reuse the same commands across different components or even different applications. This reusability reduces duplication of code and promotes code modularity.
  • Undo/Redo Functionality: The Command Pattern makes it easier to implement undo/redo functionality in our Angular applications. By storing the commands executed in a stack, we can easily undo or redo actions as needed.

Implementing the Command Pattern in Angular

To implement the Command Pattern in Angular, we need to create command classes that encapsulate our actions. These command classes should implement a common interface or base class that defines a method for executing the command. We can then use these command classes in our Angular components to trigger the actions.

Creating Command Classes

To create command classes in Angular, we need to define a common interface or base class that defines the execute() method. This method will be responsible for executing the command. Let's create a Command interface that all our command classes will implement:

interface Command {
  execute(): void;
}

Now, let's create a concrete command class called AddTodoCommand that encapsulates the action of adding a todo item. This class should implement the Command interface and define the execute() method:

class AddTodoCommand implements Command {
  constructor(private todoService: TodoService, private todo: Todo) {}

  execute(): void {
    this.todoService.addTodo(this.todo);
  }
}

In this example, the AddTodoCommand takes a TodoService instance and a Todo object as parameters in its constructor. The execute() method then calls the addTodo() method of the TodoService, passing the Todo object.

Similarly, we can create other command classes like CompleteTodoCommand, DeleteTodoCommand, etc., that encapsulate different actions in our application.

Using Command Classes in Angular Components

Once we have our command classes defined, we can use them in our Angular components to trigger the actions. To do this, we need to inject the required command classes into our components and execute them when needed.

Let's consider an example where we have a component called TodoListComponent that displays a list of todo items. We can inject the AddTodoCommand and CompleteTodoCommand into this component and execute them when the user adds or completes a todo item.

@Component({
  selector: 'app-todo-list',
  template: `
    <ul>
      <li *ngFor="let todo of todos">
        {{ todo.title }}
        <button (click)="completeTodoCommand.execute(todo)">Complete</button>
      </li>
    </ul>
    <input [(ngModel)]="newTodoTitle" placeholder="New Todo" />
    <button (click)="addTodoCommand.execute(new Todo(newTodoTitle))">Add Todo</button>
  `,
})
export class TodoListComponent {
  todos: Todo[] = [];
  newTodoTitle: string = '';

  constructor(
    private addTodoCommand: AddTodoCommand,
    private completeTodoCommand: CompleteTodoCommand
  ) {}
}

In this example, we have injected the AddTodoCommand and CompleteTodoCommand into the TodoListComponent. The AddTodoCommand is executed when the user clicks the "Add Todo" button, and the CompleteTodoCommand is executed when the user clicks the "Complete" button for a todo item.

Executing Commands

To execute commands in our Angular application, we need to create an instance of the command class and call the execute() method on it. We can do this in the component where we want to trigger the action.

Let's consider an example where we have a component called TodoFormComponent that allows the user to add a new todo item. We can inject the AddTodoCommand into this component and execute it when the user submits the form.

@Component({
  selector: 'app-todo-form',
  template: `
    <form (ngSubmit)="addTodoCommand.execute(new Todo(newTodoTitle))">
      <input [(ngModel)]="newTodoTitle" placeholder="New Todo" />
      <button type="submit">Add Todo</button>
    </form>
  `,
})
export class TodoFormComponent {
  newTodoTitle: string = '';

  constructor(private addTodoCommand: AddTodoCommand) {}
}

In this example, we have injected the AddTodoCommand into the TodoFormComponent. The AddTodoCommand is executed when the user submits the form by calling the execute() method on it and passing a new Todo object.

Undo/Redo Implementation

One of the key benefits of using the Command Pattern in Angular is the ability to implement undo/redo functionality easily. To achieve this, we can store the executed commands in a stack and provide methods to undo or redo the actions.

Let's consider an example where we have a TodoService that maintains a stack of executed commands. The TodoService should provide methods to undo or redo the actions by popping commands from the stack and calling their execute() or undo() methods.

class TodoService {
  private executedCommands: Command[] = [];
  private undoneCommands: Command[] = [];

  addTodo(todo: Todo): void {
    const command = new AddTodoCommand(this, todo);
    command.execute();
    this.executedCommands.push(command);
  }

  completeTodo(todo: Todo): void {
    const command = new CompleteTodoCommand(this, todo);
    command.execute();
    this.executedCommands.push(command);
  }

  undo(): void {
    const command = this.executedCommands.pop();
    command?.undo();
    this.undoneCommands.push(command);
  }

  redo(): void {
    const command = this.undoneCommands.pop();
    command?.execute();
    this.executedCommands.push(command);
  }
}

In this example, the TodoService maintains two stacks: executedCommands and undoneCommands. When a command is executed, it is added to the executedCommands stack. When the undo action is triggered, the last executed command is popped from the stack and its undo() method is called. The undone command is then added to the undoneCommands stack for redo functionality. Similarly, when the redo action is triggered, the last undone command is popped from the stack and its execute() method is called. The executed command is then added back to the executedCommands stack.

Real-World Example: Building a Todo App

Now that we understand how to implement the Command Pattern in Angular, let's build a real-world example: a Todo app. This app will allow users to add, complete, and undo/redo todo items.

Creating the Todo Service

First, let's create a TodoService that will be responsible for managing the todo items. We will use the Command Pattern to encapsulate the actions of adding and completing todo items.

@Injectable()
export class TodoService {
  private todos: Todo[] = [];
  private executedCommands: Command[] = [];
  private undoneCommands: Command[] = [];

  getTodos(): Todo[] {
    return this.todos;
  }

  addTodo(todo: Todo): void {
    const command = new AddTodoCommand(this, todo);
    command.execute();
    this.executedCommands.push(command);
  }

  completeTodo(todo: Todo): void {
    const command = new CompleteTodoCommand(this, todo);
    command.execute();
    this.executedCommands.push(command);
  }

  undo(): void {
    const command = this.executedCommands.pop();
    command?.undo();
    this.undoneCommands.push(command);
  }

  redo(): void {
    const command = this.undoneCommands.pop();
    command?.execute();
    this.executedCommands.push(command);
  }
}

In this example, the TodoService maintains a list of todo items and two stacks: executedCommands and undoneCommands. The addTodo() and completeTodo() methods create instances of the respective command classes, execute them, and push them to the executedCommands stack. The undo() method pops the last executed command from the stack, calls its undo() method, and pushes it to the undoneCommands stack. The redo() method pops the last undone command from the stack, calls its execute() method, and pushes it back to the executedCommands stack.

Implementing AddTodoCommand

Next, let's implement the AddTodoCommand class that encapsulates the action of adding a todo item.

class AddTodoCommand implements Command {
  constructor(private todoService: TodoService, private todo: Todo) {}

  execute(): void {
    this.todoService.addTodo(this.todo);
  }

  undo(): void {
    this.todoService.deleteTodo(this.todo);
  }
}

In this example, the AddTodoCommand takes a TodoService instance and a Todo object as parameters in its constructor. The execute() method calls the addTodo() method of the TodoService, passing the Todo object. The undo() method calls the deleteTodo() method of the TodoService, passing the same Todo object.

Implementing CompleteTodoCommand

Similarly, let's implement the CompleteTodoCommand class that encapsulates the action of completing a todo item.

class CompleteTodoCommand implements Command {
  constructor(private todoService: TodoService, private todo: Todo) {}

  execute(): void {
    this.todoService.completeTodo(this.todo);
  }

  undo(): void {
    this.todoService.uncompleteTodo(this.todo);
  }
}

In this example, the CompleteTodoCommand takes a TodoService instance and a Todo object as parameters in its constructor. The execute() method calls the completeTodo() method of the TodoService, passing the Todo object. The undo() method calls the uncompleteTodo() method of the TodoService, passing the same Todo object.

Implementing Undo/Redo Functionality

To implement undo/redo functionality in our Todo app, we can add buttons for undo and redo actions in our component templates. When the user clicks the undo button, we call the undo() method of the TodoService. When the user clicks the redo button, we call the redo() method of the TodoService.

@Component({
  selector: 'app-todo-list',
  template: `
    <ul>
      <li *ngFor="let todo of todos">
        <span [class.completed]="todo.completed">{{ todo.title }}</span>
        <button (click)="completeTodoCommand.execute(todo)">Complete</button>
      </li>
    </ul>
    <input [(ngModel)]="newTodoTitle" placeholder="New Todo" />
    <button (click)="addTodoCommand.execute(new Todo(newTodoTitle))">Add Todo</button>
    <button (click)="todoService.undo()">Undo</button>
    <button (click)="todoService.redo()">Redo</button>
  `,
})
export class TodoListComponent implements OnInit {
  todos: Todo[] = [];
  newTodoTitle: string = '';

  constructor(
    private todoService: TodoService,
    private addTodoCommand: AddTodoCommand,
    private completeTodoCommand: CompleteTodoCommand
  ) {}

  ngOnInit(): void {
    this.todos = this.todoService.getTodos();
  }
}

In this example, we have added two buttons for undo and redo actions in the TodoListComponent template. When the user clicks the undo button, we call the undo() method of the TodoService. When the user clicks the redo button, we call the redo() method of the TodoService.

Conclusion

In this tutorial, we explored how to implement the Command Pattern in Angular to encapsulate actions and achieve separation of concerns. We learned about the benefits of using the Command Pattern, such as reusability and undo/redo functionality. We saw how to create command classes, use them in Angular components, and execute the commands. We also implemented a real-world example of a Todo app using the Command Pattern. By applying the Command Pattern in our Angular applications, we can improve code maintainability and achieve a more modular and scalable architecture.