When creating a new component, we often build it around specific inputs and, in some cases, that component has no reason to exist without those inputs.
Fortunately, Angular offers several ways to ensure that an input has been provided, let's review some of them!
Table of Contents
Using NgIf
One of the most common ways to display the template of a component only when an input has a value is to check it in the NgIf
directive:
@Component({
selector: 'app-with-required',
standalone: true,
imports: [NgIf],
template: '<div *ngIf="isDefined">Value is: {{ value }}</div>'
})
export class WithNgIfComponent {
@Input() value!: number;
get isDefined(): boolean {
return typeof this.value === 'number';
}
}
Using this approach, the template will not be displayed if the value
has not been provided.
This somewhat makes the @Input
mandatory for the view to be shown but it might not be optimal for us.
Firstly, not passing a value will not throw any kind of error. If someone else would like to use our component, having our component explicitly tell what it needs for it to work would be better than just guessing what is required and what is not.
Secondly, since passing a value is not enforced, value
can be undefined
and any logic we might have in our component would require us to write defensive code anywhere we are using this value.
Using lifecycle hooks
To tackle the first issue, we can update our component to implement the OnInit
lifecycle hook and programmatically check if the value has been provided:
@Component({
selector: 'app-with-ngoninit',
standalone: true,
template: '<div>Value is: {{ value }}</div>'
})
export class WithNgOnInitComponent implements OnInit {
@Input() value!: number;
ngOnInit(): void {
if (this.value === undefined) {
throw new Error('`value` is required');
}
}
}
This is a bit better since we can now see an error in the console if we do not provide the value:
That's some progress! However, this still is not optimal: the error, even if there is one this time, it is only thrown at runtime. This makes our component a bit harder to work with since its incorrect behavior is not visible until invoked.
Using the selector
It would certainly be better if we could see that error earlier.
One way to achieve this is to enforce this is to take advantage of the Angular Language Service and to directly add the input in the selector of the component:
@Component({
selector: 'app-with-selector[value]',
standalone: true,
template: '<div>Value is: {{ value }}</div>'
})
export class WithSelectorComponent {
@Input() value!: number;
}
By doing so, using the component without passing the input would result in Angular not knowing what component we are trying to use:
Similarly, passing the input to the component will work just fine.
However, with this approach, the error message is not very explicit since it is telling us about an unknown component instead of the missing input.
It is also important to note that Angular is not aware that the input in your selector is the same as the
@Input
you defined. It means that if you were to rename it, you would have to be sure to manually make the change in the selector too
Using the required
option
All solutions that were presented are about tradeoffs:
- Using
NgIf
we are silencing the error but saving ourselves the code to check the input - Using
OnInit
we can ensure that the value has been provided but also have to write extra code for that - Using the selector, we can enforce the usage of the @Input and get an error message at compile time. However, the error message is not very specific, as it tells us about an unknown component instead of the missing input
Fortunately, the Angular dev team has noticed this issue as well and is addressing it in the upcoming version 16 of the framework:
feat(compiler): add support for compile-time required inputs #49468
This is a re-submit of #49453.
Adds support for marking a directive input as required. During template type checking, the compiler will verify that all required inputs have been specified and will raise a diagnostic if one or more are missing. Some specifics:
- Inputs are marked as required by passing an object literal with a
required: true
property to theInput
decorator or into theinputs
array. - Required inputs imply that the directive can't work without them. This is why there's a new check that enforces that all required inputs of a host directive are exposed on the host.
- Required input diagnostics are reported through the
OutOfBandDiagnosticRecorder
, rather than generating a new structure in the TCB, because it allows us to provide a better error message. - Currently required inputs are only supported during AOT compilation, because knowing which bindings are present during JIT can be tricky and may lead to increased bundle sizes.
Fixes #37706.
The suggestion is to provide an object literal to the @Input
to indicate whether this input is mandatory or not:
@Component({
selector: 'app-with-required',
standalone: true,
template: '<div>Value is: {{ value }}</div>'
})
export class WithRequiredComponent {
@Input({ required: true }) value!: number;
}
If not provided, Angular will throw a compilation error:
You can notice that this time the error message is both during the compilation and very explicit as it tells us what property is missing, on which component and on which line.
An advantage of using the object literal to enhance the current @Input
behavior is that it is backward compatible: previous solutions will still work without it and, once the migration is done to Angular 16, you can take advantage of it too in your code base with little to no breaking changes.
In this article we saw four different ways of ensuring that an input has been provided to our component:
- Checking in the template using
NgIf
- Checking during the component initialization using
OnInit
- Taking advantage of the selector syntax to bring the Angular Language Service to the rescue
- Using the new
required
flag of Angular 16 in the@Input
options
I personally prefer the required
flag since I think this is the most explicit and straightforward way to conveys the agreement I aim to establish between the component and potential consumers.
If you would like to check the resulting code, you can head on to the associated GitHub repository where you can find all the components and their usage.
I hope that you learnt something useful there!
Top comments (4)
Good writeup!
Ng 16 looks to be a very potent release.
Signals are very very cool
Yes ,😀 but let's just don't stop there ...
I would love to see Angular to fully adapting JSON Schema validation.
And with adapting I mean fully builtin 🤩
Unfortunately, there is no compiler error in my case... Is there any config?