I have an Angular 4.3 + Cordova application that used to work very well. But now, I get a blank screen on app start-up, and nothing happens any more.
After digging a while I realized where it comes from :
my home page is protected by a CanActivate
guard that will check some file-system-persisted preferences and redirect the user to another page if this is the first run or if a required preference is missing, to fill-in the required properties.
So the launch of the app depends on my CanActivate
guard that depends on a PreferenceService
that itself depends on a FileSystemService
that I implemented myself. The problem is that when I try to read the file where user's preferences are stored, not a single callback is fired, nothing happen, not even an error.
this is the part of my FileSystemService
that fails without any error :
read(file: FileEntry, mode: "text" | "arrayBuffer" | "binaryString" | "dataURL" = "text"): Observable<ProgressEvent> {
return this.cdv.ready.flatMap(() => {
return Observable.create(observer => {
file.file(file => {
let reader = new FileReader();
reader.onerror = (evt: ErrorEvent) => {
this.zone.run(() => observer.error(evt)); //never triggered
};
reader.onload = (evt: ProgressEvent) => {
this.zone.run(() => observer.next(evt)); //never trigerred
};
switch (mode) {
case "text":
reader.readAsText(file);
break;
case "arrayBuffer":
reader.readAsArrayBuffer(file);
break;
case "binaryString":
reader.readAsBinaryString(file);
break;
case "dataURL":
reader.readAsDataURL(file);
break;
}
});
});
});
}
Why does this even happen and how can I deal with that so my callbacks get triggered ?
EDIT
As stated long ago, I openned an issue on zone.js repository and zone.js owner quickly patched the code. You can avoid the pain of using my dirty hack just by importing zone.js/zone-patch-cordova
inside your polyfills.
Original answer
While debugging this code I realized that the FileReader
constructor was patched by both cordova and zone.js. From what I understood regarding zone.js patching is that it changes every "onProperty" (onload
,onloadend
,onerror
) to its addEventListener(...)
counterPart.
Module Name:
on_property
Behavior with zone.js :
target.onProp
will become zone awaretarget.addEventListener(prop)
But cordova does not use the dispatchEvent(...)
API to notify listeners operation has ended.
One solution might be to deactivate onProperty
module from zone.js but it might break angular's behavior.
So this is how I coped with the situation :
read(file: FileEntry, mode: "text" | "arrayBuffer" | "binaryString" | "dataURL" = "text"): Observable<ProgressEvent> {
return this.cdv.ready.flatMap(() => {
return Observable.create(observer => {
file.file(file => {
let FileReader: new() => FileReader = ((window as any).FileReader as any).__zone_symbol__OriginalDelegate
let reader = new FileReader();
reader.onerror = (evt: ErrorEvent) => {
this.zone.run(() => observer.error(evt)); //never triggered
};
reader.onload = (evt: ProgressEvent) => {
this.zone.run(() => observer.next(evt)); //never trigerred
};
switch (mode) {
case "text":
reader.readAsText(file);
break;
case "arrayBuffer":
reader.readAsArrayBuffer(file);
break;
case "binaryString":
reader.readAsBinaryString(file);
break;
case "dataURL":
reader.readAsDataURL(file);
break;
}
});
});
});
}
The secret here is that zone.js keeps the original constructor in the __zone_symbol__OriginalDelegate
property, so calling this will actually call Cordova's FileReader
directly without zone.js patch.
This solution being a dirty hack,I have openned an issue on zone's repository
Had the same problem with FileWriter
(it internally calls a FileReader
) so I wrote this little shim :
function noZonePatch(cb: () => void) {
const orig = FileReader;
const unpatched = ((window as any).FileReader as any).__zone_symbol__OriginalDelegate;
(window as any).FileReader = unpatched;
cb();
(window as any).FileReader = orig;
}
then wrapped my calls to read/write operations :
write(file: FileEntry, content: Blob) {
return this.cdv.ready.flatMap(() => {
return Observable.create((out: Observer<ProgressEvent>) => {
file.createWriter((writer) => {
noZonePatch(() => {
writer.onwrite = (evt: ProgressEvent) => {
this.zone.run(() => {
out.next(evt);
out.complete();
});
};
writer.onerror = (evt) => {
this.zone.run(() => out.error(evt));
};
writer.write(content); // this is where FileReader is called internally
})
}, err => out.error(err));
});
});
}
read(file: FileEntry, mode: ReadMode = "text"): Observable<ProgressEvent> {
return this.cdv.ready.switchMap(() => Observable.create((observer: Observer<ProgressEvent>) => {
file.file(file => {
noZonePatch(() => {
let reader = new FileReader();
reader.onerror = (evt: ErrorEvent) => {
this.zone.run(() => observer.error(evt));
};
reader.onload = (evt: ProgressEvent) => {
this.zone.run(() => observer.next(evt));
};
switch (mode) {
case "text":
reader.readAsText(file);
break;
case "arrayBuffer":
reader.readAsArrayBuffer(file);
break;
case "binaryString":
reader.readAsBinaryString(file);
break;
case "dataURL":
reader.readAsDataURL(file);
break;
}
});
});
}));
}