micah
micah

Reputation: 8096

Async Validators Causing "Expression has changed" error

I have a custom image upload validator. This validator makes sure the mime type is correct, checks the file size, and that the dimensions are correct. Part of this validator creates an image element and gives it the datauri (as src) to check the width/height. Because of that last check, it uses the img.onload event, and thus, this is an async validator.

My validator will accept urls as well as data uris.

Here is the source of my validators. I've created a FileValidator base class that checks mimeType and file size while the ImageValidator checks dimensions.

The validation classes are as follows. I made these classes because they require state given the ValidationRules. The validator method is the actor

FileValidator

export class FileValidator {
  constructor(protected _rules: FileValidationRules) {}

  public validator(control: FormControl): Promise<any> {
    return new Promise((resolve, reject) => {
      var value = control.value;

      if (value) {
        if (this.isDataUri(value)) {
          if (this._rules.acceptedMimeTypes && this._rules.acceptedMimeTypes.length) {
            var mimeType = this.getMimeType(value);
            var allowedMimeType = false;

            for (var i = 0; i < this._rules.acceptedMimeTypes.length; i++) {
              if (this._rules.acceptedMimeTypes[i] === mimeType) {
                allowedMimeType = true;
                break;
              }
            }

            if (!allowedMimeType) {
              resolve({
                fileValidator: `File type not allowed: ${mimeType}. Allowed Types: ${this._rules.acceptedMimeTypes}`
              });
            }

            if (this._rules.maxSize) {
              var blob = this.dataURItoBlob(value);
              blob.size > this._rules.maxSize;

              resolve({
                fileValidator: `File is too large. File Size: ${this.getFriendlyFileSize(blob.size)}, Max Size: ${this.getFriendlyFileSize(this._rules.maxSize)}`
              });
            }
          }
        } else if (!this.isUrl(value)) {
          resolve({
            fileValidator: 'Unknown format'
          });
        }
      }

      resolve();
    });
  }

  ... Helper Methods

}

ImageValidator

export class ImageValidator extends FileValidator {

  constructor(_rules: ImageValidationRules) {
    super(_rules);
  }

  public validator(control: FormControl): Promise<any> {
    return new Promise((resolve, reject) => {
      super.validator(control).then((results) => {
        if (results && results.fileValidator) {
          resolve({
            imageValidator: results.fileValidator
          });
        }

        var value = control.value;

        if (value) {
          var rules = <ImageValidationRules>this._rules;
          if (this.isDataUri(value)) {

            if (rules.width || rules.height) {

              var img: HTMLImageElement = document.createElement('img');
              img.onload = () => {
                var validSize = true;

                if (rules.width && img.width !== rules.width) {
                  validSize = false;
                }

                if (rules.height && img.height !== rules.height) {
                  validSize = false;
                }

                if (!validSize) {
                  resolve({
                    imageValidator: `Image must be ${rules.width || 'any'}x${rules.height || 'any'}. Actual Size: ${img.width}x${img.height}` 
                  });
                }
              };
              img.src = value;

            }
          }
        }

        resolve();
      });
    });
  }

}

This all works. If I select an image that doesn't meet requirements I get the proper error message.

enter image description here

But this image uploader is in a tabbed area.

enter image description here

If I cycle between tabs I get this error-

Error: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.

Simply put, my tabbed area markup is-

    <li *ngFor="let page of pages"
        [ngClass]="{active: page === activePage}">
      <a (click)="setActivatePage(page)">
        <i *ngIf="getFormGroup(page).invalid" class="fa fa-exclamation-circle font-red"></i>
        {{page.title}}
      </a>
    </li>

And my tabbed content is (simply)-

<element *ngFor="let input of activePage.inputs"
         ... etc>
</element>

This line-

<i *ngIf="getFormGroup(page).invalid" class="fa fa-exclamation-circle font-red"></i>

Is causing the error. But I can't figure out why. I think it has something to do with the fact that I'm using async validators.

If I comment out all resolve calls in my async validators it works fine (but obviously my validation stops working).

So from what I can gather, changing the active page reapplies validators. And the async validators are updating the validity after the validity was checked. And for some reason this is a problem for angular, like it doesn't consider asynchronous tasks will update state.. asynchronously.

Has anyone any idea what that might be. Sorry I couldn't provide a simplified working example, my system is complex.

EDIT

Additionally, it only occurs if the value for the input is set. If I call reset on the control or set the value to null (in the validator), then this does not occur when cycling tabs.

Upvotes: 0

Views: 551

Answers (1)

micah
micah

Reputation: 8096

The answer was that I was resolving the validator when nothing changed-

resolve();

I figured the async validator should be resolved no matter what. But that doesn't seem to be the case. Removing this empty resolve fixed the problem.

Upvotes: 1

Related Questions