
@Input() Options in Angular
Getting Started
Every day, we use the `@Input()` decorator to define properties that can be passed in from parent components. Here is a simple example that shows a typical use of `@Input()` in one of my applications. I use the exclamation mark (non-null assertion) to tell Typescript that `title` will be assigned a value by the component's parent. The `section` property is optional. Similarly, the `elevation` property is optional, but it defaults to zero (0).
1export const sectionNames = ['primary', 'secondary', 'aside'] as const;
2export type Section = typeof sectionNames[number];
3
4@Component({
5selector: 'hallpass-page-title',
6standalone: true,
7...
8})
9export class TitleHeading() {
10 @Input()
11 title!: string;
12
13 @Input()
14 section?: Section;
15
16 @Input()
17 elevation: number = 0;
18
19}
This pattern has served me well for years, but has some issues.
@Input() Options
title
is a known attribute...
By using title
as the property name, we are introducing a side-effect. Namely, your browser will treat the value as the element's title (i.e., show a tooltip with the value). This is not necessarily what we want. To combat that, we can just rename the property. Another solution is to give the property an alias so it is known to the outside world by one name and within the component by another.
1@Input('pageTitle')
2 title!: string;
3
4...
5
6<hallpass-page-title pageTitle="Welcome" />
Now, we use pageTitle
to pass in the title text and title
within the component's HTML to display the text.
Aliases have been around for a while, but did you know you can also declare them this way?
1@Input({ alias: 'pageTitle' })
2 title!: string;
If you haven't heard of this alternative way of defining an input alias, you might be asking, "Why would I use this? It is more typing!" Great question. Let's see.
pageTitle
is a non-null value!
We have marked pageTitle
with the non-null assertion (the exclamation point), we expect the parent to include a pageTitle
value in the element. But we can get away with something like this:
1<hallpass-page-title />
Angular (and Typescript) have no problem with this. Our intent is that pageTitle
must be included in the element. Enter the required @Input()
option.
1@Input({ alias: 'pageTitle', required: true })
2 title!: string;
Now, you will get an error in your IDE if you omit the pageTitle
property:
Required input 'pageTitle' from component TitleHeadingComponent must be specified.
Make section
more user friendly
Currently, the component allows for an optional section
. But we can expand this by allowing the section
property to be more flexible and use a transformation to convert the value into a Section. We will allow the value to be a Section string, sectionNames
index (i.e., a number) or an empty string. Have a look at these helper functions:
1function toSection(value?: "" | Section | number): Section | undefined {
2 if (typeof value === 'string') {
3 return value === ""
4 ? 'primary'
5 : isSection(value)
6 ? value
7 : undefined;
8 }
9 else if (typeof value === 'number' && value >= 0 && value < sectionNames.length) {
10 return sectionNames[value];
11 }
12 //else
13 return undefined;
14}
15function isSection(value: string): value is Section {
16 return sectionNames.includes(value as Section);
17}
The function toSection()
takes an argument of type Section, number or empty space ("") and attempts to transform the argument into a Section ...
1If (value is an empty string)
2 Return 'primary'
3If (value is a valid Section string)
4 Return value as a Section
5If (value is an index in the sectionNames array)
6 Return the Section associated with the index
7Otherwise
8 Return undefined. //silently fail
To use the transformation, we redefine section
:
1@Input({
2 transform: toSection,
3})
4section?: Section;
5
Now, we can indicate a section in a variety of ways!
1<!-- empty string => 'primary' -->
2<hallpass-page-title pageTitle="This is Primary" section="" />
3<hallpass-page-title pageTitle="Another Primary" section />
4
5<!-- Section string => 'secondary' -->
6<hallpass-page-title pageTitle="This is Secondary" section="secondary" />
7
8<!-- sectionNames index => 'aside' -->
9<hallpass-page-title pageTitle="This is Aside" [section]="2" />
Constrain `elevation`
We can make `elevation` more user-friendly by constraining it to an integer within a specific range and allowing the input to be a number or string.
First, let's consider why we might want to allow `elevation` to be either a number or a string. Currently, we can set th elevation
this way:
1<hallpass-page-title pageTitle="Using default elevation" />
2<hallpass-page-title pageTitle="Elevation equals 1" [elevation]="1" />
3
4<!-- this will throw an error: Type 'string' is not assignable to type 'number' -->
5<hallpass-page-title pageTitle="Will not work" elevation="2" />
To fix error caused by elevation="2"
, we can use the built-in @angular/core
transformation: numberAttribute
1@Input({
2 transform: numberAttribute,
3})
4elevation: number = 0;
Now, we can set an elevation either by assigning a number or string:
1<hallpass-page-title pageTitle="Elevation equals 1" [elevation]="1" />
2<hallpass-page-title pageTitle="Elevation equals 2" elevation="2" />
Next, let's see how to restrict `elevation` to within the range [0-4] such that if the value is outside the range, it is assigned the min/max of the range depending on whether it is above or below the range.
1function toElevation(value?: number): number {
2 return typeof value === 'number'
3 ? Math.max(0, Math.min(4, value)) // clamp to 0-4
4 : 0;
5}
6
7...
8
9@Input({
10 transform: toElevation
11})
12elevation: number = 0;
If we try an `elevation` below 0 or above 4, the component will use `0` or `4` respectively.
1<hallpass-page-title pageTitle="Elevation is 0" [elevation]="-1" />
2<hallpass-page-title pageTitle="Elevation is 4" [elevation]="5" />
Wait! We have lost the ability to write `elevation` as a string...
1<!-- ERROR: Type 'string' is not assignable to type 'number' --!>
2<hallpass-page-title pageTitle="Elevation equals 2" elevation="2" />
We can *pipe* the result of numberAttribute
to toElevation
to allow for both string and number input.
1@Input({
2 transform: (value?: string | number) =>
3 pipe(value, numberAttribute, toElevation),
4})
5elevation: number = 0;
You can use my pipe()
implementation], or you can just update toElevation()
to accept 'number' or 'string' and parse the value to a number if it is a string, then evaluate as you did before.
Note on using `@Input()` transformations
You will want to keep your transformations simple (and quick) with no side effects. While it might be tempting to perform some complex business validation here, it is better handled elsewhere.
Here are some examples of transformations that I have used - all involving arrays. Note - I prefer to put the transformation logic in helper functions (usually outside the component). But I have implemented them here as inline functions for convenience.
1// remove and falsey values (empty, null, or undefined) from the list of friends
2@Input({
3 transform: (values: string[]) => values.filter(Boolean)
4})
5friends: string[];
6
7// sort the list of friends in alphabetic order
8@Input({
9 transform: (values: string[]) => {
10 values.sort((a,b) => a.localeCompare(b));
11 return values;
12 }
13})
14friends: string[];
15
16//allow both a string array or comma-delineated string
17@Input({
18 transfrom: (value: string | string[]) => {
19 const array: string[] =
20 typeof(value) === 'string')
21 ? value.split(',')
22 : Array.isArray(value)
23 ? value
24 : [];
25 return array
26 .map(m => m.trim())
27 .filter(Boolean);
28 }
29 })
30friends: string[];
Finally
Now that you have learned all about @Input()
options, it is time for me to suggest that @Input()
might be obsolete and that you should consider using signal-based input()
. But we will save this for another post.