Revolutionizing Angular: Introducing the New Signal Input API
Angular’s development team has achieved a remarkable milestone with the successful integration of the Signal Input API, a transformative addition to the framework. This integration, which already available in v17.1.0, heralds a new era of coding efficiency and clarity. Here’s a sneak peek into what this groundbreaking update entails.
Required Input Signal
Angular allows for a required input signal, ensuring mandatory data provision. If not supplied, it throws a compile and runtime error. This feature offers a more declarative syntax than the traditional @Input({ required: true })
, enhancing readability:
@Component({
selector: 'user-profile',
standalone: true,
template: `{{ email() }}`,
})
export class UserProfile {
email = input.required<string>();
}
Optional Input Signal without Initial Value
This variation caters to optional input signals where an initial value isn’t set:
@Component({
selector: 'user-profile',
standalone: true,
template: `{{ lastName() }}`,
})
export class UserProfile {
lastName = input<string>();
}
It returns InputSignal<string|undefined, string|undefined>
, allowing components to handle undefined
inputs gracefully.
Input with Default Value
Setting a default value for an input signal is straightforward. In this case, the input firstName
defaults to "John" if no other value is provided.
@Component({
selector: 'user-profile',
standalone: true,
template: `{{ firstName() }}`,
})
export class UserProfile {
firstName = input('John');
}
Input Aliasing
Input aliasing, as demonstrated with suspended
, allows inputs to be referred to by alternate names. This provides flexibility in naming conventions without altering the underlying component logic.
@Component({
selector: 'user-profile',
standalone: true,
template: `{{ suspended() }}`,
})
export class UserProfile {
suspended = input<boolean>(false, { alias: 'disabled' });
}
Computed Properties Based on Signal Input
This update eliminates the need for the ngOnChanges
lifecycle hook, as computed properties now automatically update with input changes. This advancement is a significant leap in Angular's reactive programming capabilities:
// Imaging this comes from the backend
enum UserRole {
ADMIN = 1,
VIEWER = 2,
}
const rolesMetadata = {
[UserRole.ADMIN]: { label: 'Admin', color: 'green' },
[UserRole.VIEWER]: { label: 'Viewer', color: 'blue' },
};
@Component({
selector: 'user-profile',
standalone: true,
template: `
<p [style.color]="roleMetadata().color">
Role: {{ roleMetadata().label }}
</p>
`,
})
export class UserProfile {
role = input.required<UserRole>();
roleMetadata = computed(() => rolesMetadata[this.role()]);
}
Transform Function for Input Value
The transform
function activates each time a new value is bound, enabling the transformation of the input before its actual update.
The enhanced version provides greater flexibility and power in transforming input values. This improved method surpasses the limitations of the previous approach, which utilized @Input({ transform: transformFn })
. The key advancements include the ability to employ generics, higher-order functions, and arrow functions, offering a more robust and versatile mechanism for input transformation.
// We can use arrow functions
const toBoolean = (v: string | boolean) => v === true || v !== '';
// We can use higher order function
const higherOrderFn =<T>(defaultVal: T) => (v: string) =>
v || defaultVal;
// Generics
export function coerceArray<T>(value: T | T[]): T[];
export function coerceArray<T>(value: T | readonly T[]): readonly T[];
export function coerceArray<T>(value: T | T[]): T[] {
return Array.isArray(value) ? value : [value];
}
const formatter = new Intl.ListFormat('en', {
style: 'long',
type: 'conjunction',
});
@Component({
selector: 'user-profile',
standalone: true,
template: `{{ formattedSkills() }}`,
})
export class UserProfile {
skills = input.required({
👇 🔥
transform: coerceArray<string>,
})
formattedSkills = computed(() => formatter.format(this.skills()));
}
Side Effects on Input Change
The effect
function runs side effects whenever the input changes:
@Component({
selector: 'user-profile',
standalone: true,
template: `{{ suspended() }}`,
})
export class UserProfile {
suspended = input<boolean>(false, { alias: 'disabled' });
constructor() {
effect(() => { console.log(this.suspended()); });
}
}
REAL Component Inputs Type
The return type of the signal input is InputSignal
, which allows us to infer the types of component inputs. This inference is particularly useful in typescript when dynamically creating components, ensuring type safety and consistency:
type ComponentInputs<C> = {
[K in keyof Pick<
C,
{
[K in keyof C]: C[K] extends InputSignal<any> ? K : never;
}[keyof C]
>]: C[K] extends InputSignal<infer _, infer Write> ? Write : never;
};
Be aware that in the current implementation, we can utilize both the @Input
decorator and signal inputs. Note that there is one small drawback that it will not work when using alias
.
This new Signal Input API in Angular is set to revolutionize the way developers interact with components, offering enhanced clarity, flexibility, and efficiency in Angular applications. Stay tuned for its official release and prepare to elevate your Angular projects to new heights.
You can learn more from the following pull requests:
Follow me on Medium or Twitter to read more about Angular and JS!