Angular 4.x + Cordova : FileReader fails silently (white screen of death)
Asked Answered
M

3

5

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 ?

Mcquillin answered 7/8, 2017 at 8:27 Comment(0)
M
11

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 aware target.addEventListener(prop)

source

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

Edit :

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;
                }
            });
        });
    }));
}
Mcquillin answered 7/8, 2017 at 8:27 Comment(3)
You are a genius. I have spent the best part of 3 days, trying to debug this problem. I have looked at, at least 20 different solutions, that all fail. I am using Angular 5, and when I build my app, using 'ng build --prod', my 'FileReader' routine fails, because the file is passed in to 'onload' rather than the event [event.target & event.target.result]. Your one line of shining brilliance fixed this. I am not entirely sure why, but the event and all its properties are now correctly passed into 'onload' and I suspect this works for all FileReader's other events. Thank you so much...Silsby
Please check the issue I opened on zonejs's repo, they added a patch, so upgrading zone and adding the patch may fix this without my dirty hack.Mcquillin
Thanks for the heads up, but I am using Angular, so I am not sure whether a patch is the solution, because I thought Angular has zones baked in. I may have to upgrade to Angular 6, which is a little scary!Silsby
S
2

Something late to the party but I had to fix an Ionic3/Angular4 project with this exact problem and I found that the answer from @n00dl3 was on point but there is something of a race condition when an FileReader instance is created in a global service. Because sometimes zone did not patched yet the FileReader window object so no __zone_symbol__OriginalDelegate is found.

So what I did to always get the correct class is a little factory function that returns a FileReader instance:

function HackFileReader(): FileReader {
  const preZoneFileReader = ((window as any).FileReader as any).__zone_symbol__OriginalDelegate;
  if (preZoneFileReader) {
    console.log('%cHackFileReader: preZoneFileReader found creating new instance', 'font-size:3em; color: red');
    return new preZoneFileReader();
  } else {
    console.log('%cHackFileReader: NO preZoneFileReader was found, returning regular File Reader', 'font-size:3em; color: red');
    return new FileReader();
  }
}

and to use it just do :

const reader = HackFileReader();

I hope it helps somebody

Snowmobile answered 20/10, 2019 at 7:47 Comment(2)
You should not use that hack anymore, as the ZoneJS team has published a patch. I'l add that detail to my answerMcquillin
@Mcquillin when the codebase is old and you are not allow to update your dependencies, yes you need it :(Snowmobile
G
1

If you're using ionic/cordova, there is no need for the HackFileReader solution from @disante (which I actually used)

Two things you need to do, first off is make sure you have the most current zone.js.

npm install --save zone.js@latest

Second you need to ensure that your index.html adds cordova.js AFTER build/polyfills.js

Garin answered 14/10, 2020 at 23:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.