2

I've created a Plunker here - https://plnkr.co/edit/1Aztv5K2gqIc4erNgRnG?p=preview

TL;DR To see the problem you should open the developer tools window and look at the console log output. Then go to the <input> box and repeatedly add some text and then clear it. You will see the value of errors output to the console, but those errors never appear in the component's view.

I've written a custom component that implements NG_VALUE_ACCESSOR to give my control a databinding context, and NG_VALIDATORS so I can have access to the data source (AbstractControl) being validated.

I cannot see why the *ngFor in my component's template is not listing the errors that I can see are present by looking in the console.log output.

import {Component, NgModule, forwardRef} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import {
  AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors,
  Validator
} from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/distinctUntilChanged';

@Component({
  selector: 'validation-errors',
  template: `
    <div>Errors should appear below this line</div>
    <div *ngFor='let item of errors'>{{ item }}!!!</div>
    `,
  providers: [
    {
      provide: NG_VALIDATORS,
      useClass: forwardRef(() => ValidationErrorsComponent),
      multi: true
    },
    {
      provide: NG_VALUE_ACCESSOR,
      useClass: forwardRef(() => ValidationErrorsComponent),
      multi: true
    }
  ]
})
export class ValidationErrorsComponent implements Validator, ControlValueAccessor  {
  public errors: string[] = [];

  private control: AbstractControl = null;

  private updateErrors(errorsObject: any) {
    this.errors = [];
    if (errorsObject) {
      for (const errorType of Object.keys(errorsObject)) {
        this.errors.push(errorType);
      }
    }
    console.log('Errors: ' + JSON.stringify(this.errors));
  }

  validate(c: AbstractControl): ValidationErrors | any {
    if (this.control === null && c !== null && c !== undefined) {
      this.control = c;
      this.control.statusChanges
        .distinctUntilChanged()
        .subscribe(x => this.updateErrors(this.control.errors));
    }
  }

  writeValue(obj: any): void {
  }

  registerOnChange(fn: any): void {
  }

  registerOnTouched(fn: any): void {
  }

}

It doesn't seem to be a change detection problem, as I have also tried implementing this using Observables and the async pipe and still it shows no errors.

The consumer code looks like this

//our root app component
import {Component, NgModule, forwardRef} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import {
  AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors,
  Validator
} from '@angular/forms';
import { ValidationErrorsComponent } from './validation-errors.component';

@Component({
  selector: 'my-app',
  template: `
    <div [formGroup]='form'>
      <input formControlName='name' style="width: 100%"/> 
      <validation-errors formControlName='name'></validation-errors>
    </div>
  `,
})
export class App {
  public form: FormGroup;

  constructor(formBuilder: FormBuilder) {
    this.form = formBuilder.group({
      name: ['Delete this text to trigger required vaidation error', Validators.required]
    })
  }
}

@NgModule({
  imports: [ BrowserModule, FormsModule, ReactiveFormsModule ],
  exports: [    
    FormsModule,
    ReactiveFormsModule,
    ValidationErrorsComponent
  ],
  declarations: [ App, ValidationErrorsComponent ],
  bootstrap: [ App ]
})
export class AppModule {}
Peter Morris
  • 20,174
  • 9
  • 81
  • 146
  • It's not ngFor, I think it's errors is just empty when div is populated. put {{errors}} before the div and it's not renedered either – Vega Aug 15 '17 at 13:26
  • Yes, even {{ errors?.length }} always outputs zero. The view just isn't binding to it at all. – Peter Morris Aug 15 '17 at 13:49
  • you can follow this approach for the same it uses the same class but captures all the validation in class [git link](https://github.com/rahulrsingh09/AngularConcepts/blob/master/src/app/template-driven/template-driven.component.ts) . Working [example](https://rahulrsingh09.github.io/AngularConcepts/template) – Rahul Singh Aug 15 '17 at 13:52
  • @RahulSingh I am using reactive forms – Peter Morris Aug 15 '17 at 14:12
  • Somehow `this.errors` is not in the same scope as your class variable. The view is only bound to the initial constructed value of []. – Michael Aug 15 '17 at 14:30
  • @Michael But how can that be? It is only set once, in the constructor – Peter Morris Aug 15 '17 at 14:45
  • @PeterMorris I was unable to figure it out and can't look at it right now. I added checks to see what the value of `this.errors` was during the lifecycle checks and when it was updated. As far as I can tell there are two scenarios. 1. Somewhere between the end of the updateErrors function and the beginning of doCheck, all fields are getting reinitialized. 2. The scope of this.errors isn't what we should expect it to be. 1 seems a lot more likely than 2 based on the checks but I can't rule it out. I have to play with it more to better understand. – Michael Aug 15 '17 at 15:06
  • If you transform your `errors` Array into a Promise or Obsevable, you'll be able to use `async` pipe in the `ngFor` and therefore it will get updated. Check this thread out: https://stackoverflow.com/questions/34598054/angular-2-not-updating-when-array-is-updated – SrAxi Aug 15 '17 at 15:08
  • @PeterMorris my bad sorry i ovelooked it , Some how the error is not getting set in your case the var is getting reinitialised after the block i checked – Rahul Singh Aug 15 '17 at 15:45
  • @RahulSingh Which file + line number are you referring to that is being reinitialised? – Peter Morris Aug 15 '17 at 15:58
  • the error is getting reinitilized, what i would would suggest is a work around you can have a output emitter to emit this value back to parent and print i there – Rahul Singh Aug 15 '17 at 16:00
  • @PeterMorris i guess working with Directives here would be an advantage to get the things in scope dont you think instead of component we should use directives , stuck with this for an hour now ? – Rahul Singh Aug 15 '17 at 16:21
  • I can't really use a directive, because I can't insert UI inside html input controls, that's why I had to go with a component and could style (e.g. I need to col-sm-push-2 to align under the control rather than its label). I don't see why a directive would bind correctly when a component doesn't, do you? – Peter Morris Aug 15 '17 at 16:39
  • you can use a renderer to set html . I guess directive will keep the scope intact but here a component dosen't keep the scope intact i feel – Rahul Singh Aug 15 '17 at 17:18
  • @RahulSingh The problem is that there are 3 instances of the component being created. One in the view, one for the validator, and one for the value accessor. I will revert it to a directive and component tomorrow and see if I can fix my original problem instead :) – Peter Morris Aug 15 '17 at 19:59

1 Answers1

1

Update: The problem is that 3 instances of the component were being created. One for the markup in the template, one for NG_VALIDATORS provider, and one for the NG_VALUE_ACCESSOR provider. This is because I had specified useClass instead of useExisting in the provider declarations. You could use the original code, but I think this directive is nicer to implement by adding it to an <input> so it can share its formControlName.

I have reverted to my old code that consists of a directive that creates the instance of the validator. For anyone who wishes to achieve the same goal here is the source. Note that I create instances of ValidationError, which is just a simple class.

/**
 * Used to denote a validation error of some kind
 *
 * @class ValidationError
 */
export class ValidationError {
  /**
   * @constructor
   * @param (string) message Key to the translation of the text to display
   * @param parameters Any additional parameters (max-length, etc)
   */
  constructor(public message: string, public parameters: any) {}
}

Here is the source for the directive:

import { ComponentFactoryResolver, Directive, forwardRef, OnDestroy, OnInit, ViewContainerRef } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ValidationError } from '../../validation-error';
import { ValidationErrorsComponent } from '../../components/validation-errors/validation-errors.component';

@Directive({
  selector:
    '[formControl][showValidationErrors], ' +
    '[formControlName][showValidationErrors], ' +
    '[ngModel][showValidationErrors]',
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => ShowValidationErrorsDirective), multi: true
    }
  ]
})
export class ShowValidationErrorsDirective implements Validator, OnInit, OnDestroy  {
  errors: Observable<ValidationError[]>;

  private isSubscribedToControl = false;
  private isDestroyed = false;
  private validationErrorsComponent: ValidationErrorsComponent;
  private errorsSubject: BehaviorSubject<ValidationError[]>;

  constructor(
    private viewContainerRef: ViewContainerRef,
    private componentFactoryResolver: ComponentFactoryResolver
  ) {
    this.errorsSubject = new BehaviorSubject([]);
    this.errors = Observable.from(this.errorsSubject);
  }

  ngOnInit() {
    const factory = this.componentFactoryResolver.resolveComponentFactory(ValidationErrorsComponent);
    const componentReference = this.viewContainerRef.createComponent(factory);
    this.validationErrorsComponent = componentReference.instance;
    this.validationErrorsComponent.errors = this.errors;
  }

  validate(control: AbstractControl): ValidationErrors | any {
    this.subscribeToControlErrors(control);
    return null; // We haven't added any errors
  }

  private subscribeToControlErrors(control: AbstractControl) {
    if (!this.isSubscribedToControl) {
      this.isSubscribedToControl = true;
      control.statusChanges
        .takeWhile(x => !this.isDestroyed)
        .distinctUntilChanged()
        .map(x => control.errors)
        .subscribe(x => this.populateErrors(x));
    }
  }

  private populateErrors(errorsObject: any) {
    const errors = [];
    if (errorsObject) {
      for (const errorType of Object.keys(errorsObject)) {
        errors.push(new ValidationError(errorType, errorsObject[errorType]));
      }
    }
    this.errorsSubject.next(errors);
  }

  registerOnValidatorChange(fn: () => void): void {
  }


  ngOnDestroy(): void {
    this.isDestroyed = true;
  }
}

Here is the template for the component:

<div *ngFor="let error of errors | async">
  {{ error.message | translate }}
</div>

And the source for the component:

import { Component, Input } from '@angular/core';
import { ValidationError } from '../../validation-error';
import { Observable } from 'rxjs/Observable';

@Component({
  selector: 'validation-errors',
  templateUrl: './validation-errors.component.html',
  styleUrls: ['./validation-errors.component.scss'],
})
export class ValidationErrorsComponent  {
  @Input()
  errors: Observable<ValidationError[]>;
}

And finally, this is how it is used in your consumer:

<input formControlName="mobileNumber" showValidationErrors />
Peter Morris
  • 20,174
  • 9
  • 81
  • 146
  • That's really interesting. Definitely not something I've ever ran into. Doesn't seem like you were doing anything too crazy here, either. Please update with the github issue if you're making one, or post here again if you get a response. I'm very curious to see why it behaves this way. – Michael Aug 16 '17 at 13:16
  • I was accidentally using `useClass` instead of `useExisting` in the provider declarations and hadn't noticed :) – Peter Morris Aug 18 '17 at 08:46
  • 1
    Thanks for posting! That's definitely an area I don't understand well but it makes sense. – Michael Aug 18 '17 at 12:10