Angular — Four Practical Tips to Build a Good Shared Component

Component is the basic building block of an Angular application. When working on a large application, it is very essential to have shared components for common features. Here are four tips for you to build a good shared Angular component. You may also use them to improve your existing components.

1. Provide Default Value for Optional Inputs

The first and easiest improvement we can do is to simply give our optional inputs default value. Providing default value can facilitate the configuration of our components as other developers can simply ignore the inputs they don’t need. For example:

@Component({
selector: 'list-of-card',
template: `
<div *ngFor="let value of displayValue">
{{value}}
</div>
`
})
export class ListOfCardComponent {
@Input() data: string[];
@Input() filter: any;
get displayValue(): string[] {
return this.data.filter(this.filter);
}
}

This is a very badly designed component. The reason is, even if we don’t need the filter, we still need to supply a function for the filter:

<list-of-card [data]="data" [filter]="filter"></list-of-card>

It creates unnecessary complexity. This can be improved easily by providing default value:

@Component({
selector: 'list-of-card',
template: `
<div *ngFor="let value of displayValue">
{{value}}
</div>
`
})
export class ListOfCardComponent {
@Input() data: string[];
@Input() filter = () => true;
get displayValue(): string[] {
return this.data.filter(this.filter);
}
}

Now we can simply do this when we don’t need the filter:

<list-of-card [data]="data"></list-of-card>

2. Replace Direct Method Call with Binding

One common issue which causes lots of trouble is relying too much on direct component public method calls. Here is another example of a badly designed component:

@Component({
selector: 'custom-button',
...
})
export class CustomButtonComponent {
activated = false;
public setActivated(value: boolean) {
this.activated = value;
}
}

Functionality-wise, this component is totally fine. Yet, it is not ideal as it doesn’t utilize binding.

data: string[] = [];
@ViewChild('customButton') customButton: CustomButtonComponent;
...
onDataLoad() {
this.customButton.setActivated(!!this.data.length);
}

This is the only way we can use this kind of component, which is unfavorable as we need to handle the change cycle ourselves. By replacing the method call with binding, we can simply throw the state management to Angular:

@Component({
selector: 'custom-button',
...
})
export class CustomButtonComponent {
@Input() activated = false;
}

Now we can do:

<custom-button [activated]="!!data.length"></custom-button>

On top of that, we can also bind to a setter method for more complicated use cases:

@Component(...)
export class TestComponent {
state: number[] = [];
@Input() set state(data: number[]) {
this.state = data.map(val => val*2)
}
}

With the setter binding, we can actually replace most of the public component methods with binding.

3. Stick with the Naming Convention

Another very common issue is about the naming. Here I would like to explain with an example:

@Component({
selector: 'custom-button',
...
})
export class CustomButtonComponent {
@Input() isDisabled = false;
@Output() onClick = new EventEmitter<void>();
}

Again, this component is totally fine functionality-wise.

<custom-button [isDisabled]="true" (onClick)="onClick()">
</custom-button>

The problem is about the naming of the input and output. Many people prefix boolean with is and event with on, which breaches the convention of Angular. It is not wrong to use isDisabled or onClick. Yet, the reason we want to align with the convention is to minimize the chance of causing trouble to other developers who are going to use our components.

Assume we make a component with the onClick event, when someone mistakenly subscribes to the click instead of onClick, it is not intuitive to find out that the prefix is missing when the click event doesn’t work during testing. We are actually making traps for other developers when we use this kind of unconventional naming. Besides, enabling two way binding is another reason we want to follow the convention:

@Component({
selector: 'counter',
...
})
export class CounterComponent {
@Input() counterValue = 0;
@Output() counterValueChange = new EventEmitter<number>();

...
}

We can estasblish a two way binding for this kind of components:

<counter [(counterValue)]="state"></counter>

Yet, it won’t work if we put onCounterValueChange instead of counterValueChange.

4. Add Types

The last tip is to always add types for all our inputs and outputs. It helps other developers to understand the functionalities without the need to dig deep into our codes.

@Component(...)
export class TestComponent {
@Input() inputA: string;
@Input() inputB: number;
@Input() inputC: boolean;
...
@Output() outputA = new EventEmitter<string>();
...
}

For more complicated input object, we may create an interface or class:

export interface State {
value1: string;
value2: number;
value3: boolean;
}
@Component(...)
export class TestComponent {
@Input() state: State;
@Input() inputA: string;
@Input() inputB: number;
@Input() inputC: boolean;
...
@Output() stateChange = new EventEmitter<State>();
...
}

Thanks for reading! Hope you find this article helpful. Any comments would be highly appreciated. :D

Update in July 2020: I have written another article about four more tips to improve the component. Check it out if you are interested!

Web developer from Hong Kong. Most interested in Angular and Vue. Currently working on a Nuxt.js + NestJS project.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store