Unleashing the Power of the @watch Decorator in Aurelia 2

In Aurelia 2 the @watch decorator allows you to react effectively to data changes in your application, from simple properties to complex expressions. Think of it as computedFrom (if you’re coming from Aurelia 1) but on steroids.

Basics of @watch

The @watch decorator in Aurelia 2 lets you define a function that will execute whenever a specified expression changes. This expression can be a simple property on your view model, a custom attribute, or a more complex expression involving the view-models properties.

Here’s a basic example of using @watch to observe a property:

import { watch } from '@aurelia/runtime-html';

class MyApp {
  name = '';

  @watch('name')
  onNameChange(newName, oldName) {
    console.log(\`Name changed from ${oldName} to ${newName}\`);
  }
}

In this example, the onNameChange method is triggered whenever the name property changes.

Observing Single Values with String Expressions

For observing single properties, you can use simple string expressions. This approach is straightforward and valuable for simpler use cases.

class MyApp {
  packages = ['package1', 'package2'];

  @watch('packages.length')
  onPackagesChange(newLength, oldLength) {
    console.log(\`Packages array length changed from ${oldLength} to ${newLength}\`);
  }
}

In this case, onPackagesChange is triggered whenever an item is added to or removed from the packages array.

Watching Complex Expressions with Function Expressions

Function expressions come into play when you need to observe more complex expressions. They allow you to construct intricate expressions involving property access, array methods, and more.

Here’s an example where we watch the full name of a runner:

class MyApp {
  runner = { first: 'John', last: 'Doe' };

  @watch((app: MyApp) => \`${app.runner.first} ${app.runner.last}\`)
  onRunnerChange(newFullName, oldFullName) {
    console.log(\`Runner's full name changed from ${oldFullName} to ${newFullName}\`);
  }
}

In this example, onRunnerChange is triggered whenever the first or last property of the runner object changes.

Observing Array Methods: .map, .filter, and .find

The @watch decorator is robust enough to observe transformations of arrays using methods like .map, .filter, and .find.

Consider an array of numbers. We can watch the array transformed by the .map method:

class MyApp {
  numbers = [1, 2, 3, 4, 5];

  @watch((app: MyApp) => app.numbers.map(n => n \* 2))
  onDoubledNumbersChange(newNumbers, oldNumbers) {
    console.log(\`Doubled numbers changed from ${oldNumbers} to ${newNumbers}\`);
  }
}

Here, onDoubledNumbersChange is triggered whenever the numbers array changes. The newNumbers parameter represents the numbers array mapped to its doubled values after the change. The oldNumbers parameter represents the numbers array mapped to its doubled values before the change.

Similarly, we can observe the result of the .filter method:

class MyApp {
  numbers = [1, 2, 3, 4, 5];

  @watch((app: MyApp) => app.numbers.filter(n => n % 2 === 0))
  onEvenNumbersChange(newNumbers, oldNumbers) {
    console.log(\`Even numbers changed from ${oldNumbers} to ${newNumbers}\`);
  }
}

Here, onEvenNumbersChange is triggered whenever the array of even numbers changes.

The .find method can also be used in an expression:

class MyApp {
  runners = [
    { name: 'Alice', distance: 5 },
    { name: 'Bob', distance: 10 },
    { name: 'Charlie', distance: 15 },
  ];

  @watch((app: MyApp) => app.runners.find(r => r.distance > 10))
  onFastestRunnerChange(newFastestRunner, oldFastestRunner) {
    console.log(\`Fastest runner changed from ${oldFastestRunner.name} to ${newFastestRunner.name}\`);
  }
}

In this example, onFastestRunnerChange is triggered whenever the runner who has run more than ten units changes.

Observing Nested Arrays

The @watch decorator also supports watching nested arrays or objects. This enables you to observe changes in complex data structures.

class MyApp {
  teams = [
    { name: 'Team 1', members: ['Alice', 'Bob'] },
    { name: 'Team 2', members: ['Charlie', 'David'] },
  ];

  @watch((app: MyApp) => app.teams.map(t => t.members.length).reduce((a, b) => a + b, 0))
  onTotalMembersChange(newTotal, oldTotal) {
    console.log(\`Total members changed from ${oldTotal} to ${newTotal}\`);
  }
}

In this example, onTotalMembersChange is triggered whenever a member is added to or removed from any team. The newTotal and oldTotal parameters represent the total number of team members after and before the change.

Conclusion

The @watch decorator in Aurelia 2 is a powerful tool for maintaining reactivity in your applications. Its flexibility lets you observe changes in simple properties, complex expressions, and computed values. From watching single values with string expressions to observing complex expressions with array methods and function expressions, @watch provides a simple and intuitive way to handle changes in your data.

Remember, Aurelia’s philosophy is convention over configuration, so it’s always a good idea to keep your code as simple and readable as possible. Utilize the @watch decorator wisely to keep your UI up-to-date and your users happy.