How can I solve the same issue in Angular, that ng-messages solved in AngularJS?

前端 未结 3 1516
盖世英雄少女心
盖世英雄少女心 2021-02-12 13:44

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

相关标签:
3条回答
  • 2021-02-12 14:01

    See my other answer for a library you could use for this purpose. The remainder of this answer goes into making your own components.

    Below I supply an example (didn't compile or run it, but it should give you enough information to get going). The logic for only showing messages when touched, dirty, etc can be added to this easily.

    Usage

    <validation-messages [for]="control">
      <validation-message name="required">This field is required</validation-message>
    </validation-messages>
    

    Implementation

    import { Component, OnInit, ContentChildren, QueryList, Input, OnDestroy } from '@angular/core';
    import { FormControl } from '@angular/forms';
    import { Subscription } from 'rxjs';
    
    @Component({
      selector: 'validation-messages',
      template: '<ng-content></ng-content>'
    })
    export class ValidationMessagesComponent implements OnInit, OnDestroy {
      @Input() for: FormControl;
      @ContentChildren(ValidationMessageComponent) messageComponents: QueryList<ValidationMessageComponent>;
    
      private statusChangesSubscription: Subscription;
    
      ngOnInit() {
        this.statusChangesSubscription = this.for.statusChanges.subscribe(x => {
          this.messageComponents.forEach(messageComponent => messageComponent.show = false);
    
          if (this.for.invalid) {
            let firstErrorMessageComponent = this.messageComponents.find(messageComponent => {
              return messageComponent.showsErrorIncludedIn(Object.keys(this.for.errors));
            });
    
            firstErrorMessageComponent.show = true;
          }
        });
      }
    
      ngOnDestroy() {
        this.statusChangesSubscription.unsubscribe();
      }
    }
    
    
    @Component({
      selector: 'validation-message',
      template: '<div *ngIf="show"><ng-content></ng-content></div>'
    })
    export class ValidationMessageComponent {
      @Input() name: string;
      show: boolean = false;
    
      showsErrorIncludedIn(errors: string[]): boolean {
        return errors.some(error => error === this.name);
      }
    }
    
    0 讨论(0)
  • 2021-02-12 14:11

    Building on the code that @David Walschots provided, I noticed that there we 2 problems with it (in my case).

    • it didn't validate on submit, but I didn't want to disable the submit button
    • it didn't validate on blur, only the error color would appear, but no messages.

    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: '<ng-content></ng-content>'
    })
    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<ValMessageComponent>;
    
      /**
       * 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;
      }
    }
    
    0 讨论(0)
  • 2021-02-12 14:13

    Next to my other answer, you could also use a library I've created called angular-reactive-validation which can be found here.

    You indicate that showing at most one validation message per control at any given time is important to you. This library supports that behaviour. It also reduces the amount of HTML you need to write to show validation messages, because the declaration of validation messages is moved to the Component where you declare your FormControls. Another handy feature is having dynamic validation values by passing a function to the Validator.

    Below I've provided an example form and the model behind it to give you an idea of the basic usage.

    <form [formGroup]="form">
      <div formGroupName="name">
        <label>First name:
          <input formControlName="firstName">
        </label>
        <label>Middle name:
          <input formControlName="middleName">
        </label>
        <label>Last name:
          <input formControlName="lastName">
        </label>
        <br />
        <arv-validation-messages [for]="['firstName', 'middleName', 'lastName']">
        </arv-validation-messages>
      </div>
      <label>Age:
        <input type="number" formControlName="age">
      </label>
      <arv-validation-messages for="age"></arv-validation-messages>
      <br />
      <input type="submit" />
    </form>
    
    import { Validators } from 'angular-reactive-validation';
    ...
    
    form = this.fb.group({
      name: this.fb.group({
        firstName: ['', [Validators.required('A first name is required'),
          Validators.minLength(1, minLength => `The minimum length is ${minLength}`),
          Validators.maxLength(50, maxLength => `Maximum length is ${maxLength}`)]],
        middleName: ['', [Validators.maxLength(50, maxLength => `Maximum length is ${maxLength}`)]],
        lastName: ['', [Validators.required('A last name is required'),
          Validators.maxLength(50, maxLength => `Maximum length is ${maxLength}`)]]
      }),
      age: [null, [
        Validators.required('An age is required'),
        Validators.min(0, 'You can\'t be less than zero years old.'),
        Validators.max(150, max => `Can't be more than ${max}`)
      ]]
    });
    
    0 讨论(0)
提交回复
热议问题