Angular Signal Inputs

Angular Signal Inputs

Signal Inputs, released as developer preview in version 17.1.0, allows developers to use a Signal for inputs values. They can be used the same way as decorator based inputs @input(), but with the benefit of providing an easier, more reactive way to handle input changes.

How do I use signal inputs?

Angular supports optional and required inputs, similar to decorator inputs with the optional required property.
Signal inputs don’t use decorators anymore to define inputs. Inputs are assigned to an input function, which also accepts a default value as an optional parameter:

@Component({...})
export class MyComponentWithInputs {
  // required
  fooBar = input.required<string>();  // InputSignal<string>

  // optional
  foo = input<string>();              // InputSignal<string|undefined>
  bar = input<string>('bar');         // InputSignal<string>
}

Why should I use signal inputs over decorator based inputs?

As mentioned above, signal inputs are still in developer preview and should therefore be used carefully. But the angular team advises using signal inputs as soon as they are production ready in a future version of Angular.
Signal inputs do provide some advantages over decorator based inputs:

  • Values can be derived in a declarative way by using the computed function
  • Values can be observed by using the effect function instead of ngOnChanges or input setters
  • Type Safety: Required inputs do not need initial values or the non-null assertion operator (“!”) to satisfy TypeScript. Also Input transformations are type-checked
  • OnPush components will be marked as dirty when using signal inputs in the template

The biggest difference I personally experienced was that I could use the computed and effect functions, which can be used to respond to changes of a signal. This reduced my use of ngOnChanges and input setters in my projects to an absolute minimum. Input changes do not need to be handled in the input setter or be caught by ngOnChanges anymore, and the effect and computed functions allow a more declarative way to handle input changes.

For demonstration purposes, I set up a project with two components. One using traditional decorator based inputs and the other one signal inputs. Both accept a number input named count. The number of count gets multiplied by 2 and displayed. Here’s the difference between the two components:

Decorator Input Component
@Component({...})
export class NormalInputComponent implements OnChanges {
  @Input({ required: true }) count = 0;

  doubleCount = 0;

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['count']?.currentValue) {
      this.doubleCount = this.count * 2
    }
  }
}

As we can see, we have to check with the ngOnChanges lifecycle hook if a change has happened to count. If this is the case, we assign the new value to the public variable doubleCount.

Alternatively, we could also use input setters instead of ngOnChanges:

@Component({...})
export class NormalInputComponent {
  doubleCount = 0;

  @Input({required: true}) set count(c: number){
    this.doubleCount = c * 2;
  };
}

This reduces the amount of code needed quite a bit. We still need to declare and assign the doubleCount variables on two different lines in the code. In the solution above, the value of count is not accessible from outside the setter. If you need the value of count itself, you would need to write it to a variable as well or create a private variable and create a getter to return the private variable. This results in some more lines of code:

@Component({...})
export class NormalInputComponent {
  doubleCount = 0;

  private _count = 0;

  @Input({ required: true }) set count (c: number) {
    this._count = c;
    this.doubleCount = c * 2;
  }

  get count(): number {
    return this._count;
  }
}
Signal Input Component
@Component({...})
export class SignalInputComponent {
  count = input.required<number>();

  doubleCount = computed(() => this.count() * 2);
}

With signal inputs, we can assign doubleCount in a more declarative way directly by using the computed function, which responds to changes of the count input signal.

How can I test a component with signal inputs?

When testing components with decorator based inputs, we can assign values directly to the input variable. This is not possible with signal based inputs. The signal input type SignalInput is a non-writable signal, therefore we can not call component.mySignalInput.set(…) like we can with writable signals. The easiest way, in my opinion, to test signal inputs is to use the componentRef.setInput(inputName, inputValue) function.

In this example, I want to test if the doubleCount property is actually set based in the count input. Here’s how I use the componentRef.setInput function to test this behavior:

it('should double count input', () => {
    // arrange
    const count = 4;
    const expectedDoubledCount = 8;

    // act
    fixture.componentRef.setInput('count', count);

    // assert
    expect(component.doubleCount()).toEqual(expectedDoubledCount);
  });

As stated above, in most cases this is my preferred way to test signal inputs. There are other ways to test signal inputs, e.g. by using a Wrapper Component in your test to render your component and pass the inputs in the template. If you’re using the Angular Testing Library, you can also use the render function as described in this x-post.

Set default value for required inputs

When using required inputs, we need to set a default value for these inputs. Otherwise, your tests will fail, for example the default “should create” test:

beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [SignalInputComponent]
    })
    .compileComponents();

    fixture = TestBed.createComponent(SignalInputComponent);
    component = fixture.componentInstance;

    fixture.componentRef.setInput('count', 0);
    fixture.detectChanges();
});

A more realistic scenario

Imagine a very simple account management solution. We have a route /accounts where a list of accounts is displayed. When clicking on an account in the accounts list, the user gets redirected to the account detail page. So the user gets routed from /accounts to /accounts/{id}.
In the account detail page, the account data needs to be loaded from an API.

With Angular 16 a new feature was shipped that allows passing router data as component inputs. This feature can be enabled by using withComponentInputBinding in your app.config like so:

provideRouter(routes, withComponentInputBinding()),

Now the ID parameter in the route can be accessed by adding an input with the same name as the parameter in the routes file. Sadly, Angular currently does not provide an option to clearly declare an input as a router data input so they look the same as normal inputs.

With the id as signal input the account can be loaded with the toSignal and toObservable functions.

@Component({...})
export class AccountDetailComponent {
  private readonly accountsService = inject(AccountsService);

  id = input.required<string>()

  account = toSignal(
    toObservable(this.id).pipe(switchMap((id) => this.accountsService.getAccountDetail(id)))
  )
}

The ID input can be transformed to an observable and chain the HTTP request with a switchMap. Then the result from the HTTP request gets transformed back to a signal, which can be used in the template without the need to subscribe to it.

Conclusion

In this blog post, we have seen how we can react to input changes in a more declarative way by using signal inputs over decorator inputs. It is a long-awaited feature by the Angular Community to handle input changes more reactively. With signal inputs, they handed us the tools to do so.

You can find the code for the count example in my GitHub repository.

Thanks, Tim

Leave a Comment

Your email address will not be published. Required fields are marked *