Revolutionizing Angular: Introducing the New Signal Input API

Netanel Basal
Netanel Basal
Published in
4 min readJan 6, 2024

--

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!

--

--

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.