Reputation: 8096
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.
But this image uploader is in a tabbed area.
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
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