Useful snippets from a reusable Angular Typescript input component
Angular allows creation of reusable components to simplify development. I created an input component in order to simplify how the HTML form pages looked (it allowed a large block of code to be abstracted and replaced; later it allowed all the error texts to be put in one place instead of being repeated for each field).
Initially, I asked the user to provide many pieces of information: id, name, required, max length, and so on that I was later able to derive from the formControlElement and other pieces. This post is to note the techniques I used.
Minimally, I ask that the user provide a FormControlElement (to be updated with the input text) and a label (to display next to the input). It would be nice to have something like nameof
(as in C#), as the label is often the titlecased property name, but no such luck (although note that you can write your own nameof
for type safety).
@Input()
formControlElement: FormControl;
@Input()
label: string;
Derive the name and placeholder from the label
In ngOnInit, I set the name and placeholder text from the label, with potential overrides. These are passed into <input>
as [name]
and [placeholder]
.
ngOnInit(): void {
this.inputName = this.nameOverride != null ? this.nameOverride : this.label.toLowerCase();
this.inputPlaceholder = this.placeholderOverride != null ? this.placeholderOverride : 'Enter ' + this.inputName;
}
Currently the label is hardcoded text, but if this were dynamic you would need a ngOnChanges(changes: SimpleChanges): void
method to handle updates of label
.
Derive the id from the containing form element
For test methods, we wanted every component to have a unique id. I initially abstracted this to having the user provide a “idStart” input, to which we then appended the inputName. This is boilerplate, though, and we can do better.
el: HTMLElement;
constructor(el: ElementRef) {
this.el = el.nativeElement;
}
get inputId() {
return this.closestParentId(this.el) + '-' + this.inputName;
}
private closestParentId(el: HTMLElement) {
if (el.parentElement == null) {
return 'form';
}
if (el.parentElement.tagName === 'FORM') {
return el.parentElement.id;
}
return this.closestParentId(el.parentElement);
}
Each form field should be contained inside a form. Assuming the form has an id (if it doesn’t, one should be able to be added), a good descriptor for the field is “
Derive whether the field is required
We add a red * to fields that are required, so the component needs to know whether a field is required so it can attach the appropriate class.
The form control doesn’t store any information in itself we could use to check whether a “required” validator is applied, and the .validator
property gives a conglomeration of all validators applied. We can, however, check whether a validator is applied by seeing if we get an error when we pass in a control that fails validation.
get requiredField(): boolean {
const validator = this.formControlElement.validator && this.formControlElement.validator({} as AbstractControl);
return validator && validator.required;
}
<div [ngClass]="{ required: requiredField }">
Derive whether the field has a maximum length, and what it is
We want to add a warning message as the user gets within 10 characters of hitting the character limit, so that we can let them know that further input won’t work. We can do this in a similar way to the required
check.
get maxlength(): number | null {
const validator = this.formControlElement.validator && this.formControlElement.validator(new FormControl({length: Number.POSITIVE_INFINITY}));
if (validator && validator.maxlength) {
return validator.maxlength.requiredLength;
}
return null;
}
<input [attr.maxlength]="maxlength" />
One note here is that we don’t actually need to provide a string to the FormControl
: we can provide anything with a length
property, which saves actually computing the string.
Pick between an <input>
and <textarea>
tag
Some of our form fields (for example, “notes”) use a <textarea>
tag, because they expect to receive more characters than usual. I knew that most of the logic would be shared between these components, and I didn’t want to duplicate it all. Fortunately, we can achieve this by passing in the tag type as a parameter.
@Input()
tagType: 'input' | 'textarea' = 'input';
<ng-container [ngSwitch]="tagType">
<input *ngSwitchCase="'input'" type="text" [...]>
<textarea *ngSwitchCase="'textarea'" type="text" [...]></textarea>
</ng-container>
I had initally duplicated the code, so I set my new component to point at the new style. This creates an unnecessary <kp-textarea-row>
component around the input row component, but it does make it clearer which fields are textareas or not.
@Component({
selector: 'kp-textarea-row',
template: `<kp-input-row [tagType]="'textarea'" [formControlElement]="formControlElement" [label]="label"></kp-input-row>`,
})