In AngularJS there was a form directive named ng-messages which helped us to make it so that not all form errors showed at the same time. So that for example if an input has 3 e
Building on the code that @David Walschots provided, I noticed that there we 2 problems with it (in my case).
So after trying for a while I found a solution that would also work in the cases described above.
For this I needed to add an extra listener for the blur event and make sure that the form would emit on submit (without creating boiler plate code).
The extra listener
The injected FormControl
is not directly bound to the input field and does not have a blur event connected to it. So we need to find the html input element that is connected to it. For this we can use the Renderer2
provided by Angular, but first we need to find the name of the control so we can create a CSS selector:
/**
* Tries to find the name of the given control
* Since `Angular 4.0.4` the `FormControl` can have access to it's parent
* @param {FormControl} control - The control of which the name should be determined
* @returns {string | null} The name of the control or null if no control was found
*/
private static getControlName(control: FormControl): string | null {
const formGroup = control.parent.controls;
return Object.keys(formGroup).find(name => control === formGroup[name]) || null;
}
After this we can create a CSS selector and find the element on the page:
if (this.control.updateOn === ValMessagesComponent.UPDATE_ON_BLUR) {
const controlName = ValMessagesComponent.getControlName(this.control);
const input = this.renderer.selectRootElement('input[formControlName=' + controlName + ']');
}
We now have the HTML element the FormControl
is bound to, so we can add the blur event and do stuff with it:
this.inputSubscription = this.renderer.listen(input, 'blur', () => {
// Blur happened. Let's validate!
})
When we incorporate this code into the answer of @David Walschots, we get the following code:
@Component({
selector: 'val-messages',
template: ' '
})
export class ValMessagesComponent implements OnInit, OnDestroy {
/**
* The form control on which the messages should be shown
* @type {FormControl}
*/
@Input()
private control: FormControl;
/**
* Whether or not the form should be validated on submit
* @type {boolean}
* @default
*/
@Input()
private onSubmit: boolean = true;
/**
* All the children directives that are defined within this component of type `sh-message`
* These children hold the `when` and the `message` that should be shown
* @type {ValMessageComponent}
*/
@ContentChildren(ValMessageComponent)
private messageComponents: QueryList;
/**
* All subscriptions that are used to monitor the status of the FormControl
* @see control
* @type {Subscription[]}
*/
private controlSubscriptions: Subscription[] = [];
/**
* A listener for a change on the input field to which the formControl is connected
* @type {() => void}
*/
private inputSubscription: () => void;
/**
* The key that indicates that the model is updated on blur
* @type {string}
* @default
*/
private static readonly UPDATE_ON_BLUR = 'blur';
constructor(private renderer: Renderer2) {
}
public ngOnInit(): void {
this.controlSubscriptions.push(this.control.valueChanges.subscribe(() => {
this.hideAllMessages();
this.matchAndShowMessage(this.control.errors);
}));
this.controlSubscriptions.push(this.control.statusChanges.subscribe(() => {
this.hideAllMessages();
this.matchAndShowMessage(this.control.errors);
}));
if (this.control.updateOn === ValMessagesComponent.UPDATE_ON_BLUR) {
const controlName = ValMessagesComponent.getControlName(this.control);
const input = this.renderer.selectRootElement('input[formControlName=' + controlName + ']');
this.inputSubscription = this.renderer.listen(input, 'blur', () => {
this.hideAllMessages();
this.matchAndShowMessage(this.control.errors);
})
}
}
public ngOnDestroy(): void {
if (this.inputSubscription) {
this.inputSubscription();
}
for (const subscription of this.controlSubscriptions) {
subscription.unsubscribe();
}
}
/**
* Checks if the model is invalid and if it is, finds and shows the corresponding error message
* @param {ValidationErrors} errors - Any errors that are thrown on the model
*/
private matchAndShowMessage(errors: ValidationErrors): void {
if (errors) {
const messageComponent = this.messageComponents.find(messageComponent => {
return messageComponent.shouldShowError(Object.keys(errors));
});
if (messageComponent) {
messageComponent.showMessage();
}
}
}
/**
* Hides all the messages on the model
*/
private hideAllMessages(): void {
this.messageComponents.forEach(messageComponent => messageComponent.hideMessage());
}
/**
* Tries to find the name of the given control
* Since `Angular 4.0.4` the `FormControl` can have access to it's parent
* @param {FormControl} control - The control of which the name should be determined
* @returns {string | null} The name of the control or null if no control was found
*/
private static getControlName(control: FormControl): string | null {
const formGroup = control.parent.controls;
return Object.keys(formGroup).find(name => control === formGroup[name]) || null;
}
}