From the moment I started working with Angular (and I don’t mean AngularJS) I loved the framework. I experienced the same thing with Reactive Programming using RxJS. The cool thing is that Angular and RxJS go very well together. So, as you might expect from a big Reactive Programming fan: my Angular programs are chock full of observable streams 😉
One of those places where I started using them is to respond to changes of @Input
properties of components and directives. Let’s explore a few different ways how this is usually done in Angular and then work to a reactive solution.
Responding to input changes with ngOnChanges
Perhaps the most commonly used method to be notified of the fact that one of the inputs has changed, is to have the component or directive implement the OnChanges
interface. This means you need to add a function called ngOnChanges
which will be invoked every time one of the input properties receives a new value.
Optionally you can make use of the SimpleChanges
object which is passed as an argument to this function to find out which properties were changed and what the old and new values are.
An example of responding to input changes with ngOnChanges
is shown below:
@Component({ template: '{{ multiplier }} x {{ base }} = {{ product }}' }) class MyComponent implements OnChanges { @Input() public base: number; @Input() public multiplier: number; public product: number; public ngOnChanges(): void { this.product = this.base * this.multiplier; } }
Obviously, this is just a simple example where the use of ngOnChanges
is a bit overkill. The product could also have been computed directly in the template, thereby eliminating the need for the product
property and ngOnChanges
function altogether. However, you can imagine that for a more complex computation this is a more desirable approach as we want to keep our template expressions as simple as possible.
Personally, I only use this method when there are multiple inputs that are all used together in a synchronous computation. If there are multiple inputs that are independently used, then the use of ngOnChanges
requires you to explicitly check which properties were changed, since it is invoked for every input property change.
This quickly makes the use of the ngOnChanges
method unergonomic.
Of course you can also choose to ignore checking which properties were changed and simply recompute every derived value whenever ngOnChanges
is called. That is less efficient, but if the computations are simple it might not incur a significant performance penalty.
Responding to input changes with property accessor functions
Another method to detect input changes is to make use of property accessor functions that were introduced with ECMAScript 5. The @Input
decorator can also be applied to getter and setter functions in classes, as shown in the following example:
@Component({ template: '<ul><li *ngFor="let part of parts">{{ part }}</li></ul>' }) class MyComponent { public parts: string[] = []; @Input() public set name(value: string) { this.parts = value.split(' '); } public get name(): string { return this.parts.join(' '); } }
Note that this can be made less verbose by omitting the getter function. This is possible because you are often only interested in the derived value that is computed in the setter function rather than the original input value.
The property accessor function approach works best if you have multiple input properties which are not tied together to compute a derived value from these inputs.
Responding to input changes using a reactive approach
Both methods shown so far have one drawback: they only work well for synchronous computations. As soon as the computation is asynchronous, e.g. if you need to fetch data from a REST API, then things become uncomfortable. In that case it would be nicer to model your input as an Observable
, where each next event represents a change in value of the input property.
There are two ways to achieve this, either by simply accepting an Observable
as input or by using a getter function to ‘wrap’ the input. An example of the first method is shown below:
@Component({ template: '<div>Hi there {{ (customer$ | async)?.name }}!</div>' }) class MyComponent implements OnInit { // input is provided as Observable: @Input() public customerId$: Observable<string>; public customer$: Observable<Customer>; constructor (private readonly customerService: CustomerService) {} public ngOnInit(): void { this.customer$ = this.customerId$.pipe( switchMap((id) => this.customerService.getCustomer(id)) ); } }
The first method is obviously the simplest of the two but it requires the parent component to have the input available as an Observable
, which is not always the case. Personally I therefore prefer the second method, which is illustrated in the next example.
@Component({ template: '<div>Hi there {{ (customer$ | async)?.name }}!</div>' }) class MyComponent implements OnInit { // input is provided as scalar value: @Input() public set customerId(value: string) { if (value !== this.customerId) { this.customerIdSubject.next(value); } } public get customerId(): string { return this.customerIdSubject.getValue(); } public customer$: Observable<Customer>; private customerIdSubject = new BehaviorSubject<string>(undefined); constructor (private readonly customerService: CustomerService) {} public ngOnInit(): void { this.customer$ = this.customerIdSubject.pipe( switchMap((id) => this.customerService.getCustomer(id)) ); } }
Okay, to be honest, because of its verbosity I don’t like this too much either.
We had to introduce a BehaviorSubject
and property accessor functions, which make it harder to understand how things are tied together. Nevertheless, I do like the fact that input can be provided as a scalar value and we are still able to use Reactive Programming to handle changes of the input value.
This got me thinking about how to simplify this approach. In essence what we are trying to achieve is to observe the changes of a property of an object and wrap that in an observable stream. So I crafted a little utility function that does exactly that:
function observeProperty<T, K extends keyof T> (target: T, key: K): Observable<T[K]> { const subject = new BehaviorSubject<T[K]>(target[key]); Object.defineProperty(target, key, { get(): T[K] { return subject.getValue(); }, set(newValue: T[K]): void { if (newValue !== subject.getValue()) { subject.next(newValue); } } }); return subject.asObservable(); }
This function takes an object as an argument and overrides the property descriptor of a property called key
in the specified target object. By overriding the property descriptor it is possible to install getter and setter functions. The setter function is then used to detect when a new value is written to the property.
Any change in value is pushed to a BehaviorSubject
. Finally the BehaviorSubject
is returned as an Observable
, so the caller of this utility function will receive an observable stream of the property values.
Thanks to some Typescript generics the return type of this function is equal to an Observable
of the type of the specified property. This makes the function completely typesafe. Unfortunately we do need to use a type assertion for the this
variable otherwise Typescript expands the result type to Observable<this["customerId"]>
instead of Observable<string>
.
By using the observeProperty
utility function the earlier reactive version can be written much simpler:
@Component({ template: '<div>Hi there {{ (customer$ | async)?.name }}!</div>' }) class MyComponent implements OnInit { @Input() public customerId: string; public customerId$ = observeProperty(this as MyComponent, 'customerId'); public customer$: Observable<Customer>; constructor (private readonly customerService: CustomerService) {} public ngOnInit(): void { this.customer$ = this.customerId$.pipe( switchMap((id) => this.customerService.getCustomer(id)) ); } }
As you can see the usage is pretty straightforward and it makes the code much easier to read than the previous version. This can even be written shorter by leaving out the customerId$
field, which is possible because it is only used in the ngOnInit
function. In many cases that will probably suffice, so the code can be made even less verbose.
Summary
The two most common approaches to respond to input property changes of Angular directives and components are:
- Implement the
OnChanges
interface. - Define property accessor functions for the input properties.
Which approach is most suitable depends on the context and there is not always a clear winner. However, if you like to model your application using Reactive Programming principles then property accessor functions provide a nice method to bridge a scalar input property to an observable stream of the values of that property.
The only downside of that approach is that it is quite verbose to setup yourself. Fortunately with the help of a little utility function to create an observable stream of the values of a property that verbosity can be reduced to almost nothing. A simple implementation of such a utility function has been presented in this article.
For a complete working example, you can check out the following StackBlitz project: angular-reactive-component-input-properties
This is a beautiful idea! I’m new to Angular, and have been looking for a way of working as much with observable streams as possible, just like the “Reactive UI” .NET library.
This is a really similar idea to what the “Reactive UI” provides to .NET developers.
Hi, I like this approach a lot!
I’m using a modified version that avoids needing to use a type assertion for the this and I find it a little more flexible:
function observeProperty(target: T, key: K, existingValue: V): Observable {
const subject = new BehaviorSubject(existingValue);
Object.defineProperty(target, key, {
get(): V { return subject.getValue(); },
set(newValue: V): void {
subject.next(newValue);
}
});
return subject.pipe(distinctUntilChanged());
}
Hi John,
Glad to hear you like this idea. The type assertion for `this` also annoys me a bit, so thanks for your suggestion.
FYI: the `observeProperty` in this article is a very basic / limited implementation. It won’t work for properties that are defined using getter/setter accessor functions. You can find a more complete implementation at:
https://gist.github.com/dscheerens/9fb7de6b7c47acae98cb9dd6501df6c5
Great solution! Although I think creating a decorator for this would be perfect ! It would be way less code and would be placed right above the “Input” decorator. I wrote an article about creating decorators for angular components here, check it out ?
https://medium.com/@abdelelmedny/angular-input-decorators-5d38089070aa