Skip to content

Instantly share code, notes, and snippets.

@iansinnott
Created June 5, 2017 19:36
Show Gist options
  • Save iansinnott/3d0ba1e9edc3e6967bc51da7020926b0 to your computer and use it in GitHub Desktop.
Save iansinnott/3d0ba1e9edc3e6967bc51da7020926b0 to your computer and use it in GitHub Desktop.
A simple Rx abstraction over the FileReader API
/**
* Read the text contents of a File or Blob using the FileReader interface.
* This is an async interface so it makes sense to handle it with Rx.
* @param {blob} File | Blob
* @return Observable<string>
*/
const readFile = (blob) => Observable.create(obs => {
if (!(blob instanceof Blob)) {
obs.error(new Error('`blob` must be an instance of File or Blob.'));
return;
}
const reader = new FileReader();
reader.onerror = err => obs.error(err);
reader.onabort = err => obs.error(err);
reader.onload = () => obs.next(reader.result);
reader.onloadend = () => obs.complete();
return reader.readAsText(blob);
});
@ranouf
Copy link

ranouf commented Aug 2, 2018

Hi

Thanks for your function :)

For those who wants auto completion when they use this function.

const readFile = (blob: Blob): Observable<string> => Observable.create(obs => {
  if (!(blob instanceof Blob)) {
    obs.error(new Error('`blob` must be an instance of File or Blob.'));
    return;
  }

  const reader = new FileReader();

  reader.onerror = err => obs.error(err);
  reader.onabort = err => obs.error(err);
  reader.onload = () => obs.next(reader.result);
  reader.onloadend = () => obs.complete();

  return reader.readAsText(blob);
});

@codeedog
Copy link

codeedog commented Jun 14, 2019

Thank you for this. Concise and solved my problem. Also, demonstrates to me how to convert other types of event handlers into Observables. I modified the code slightly to fail earlier on the Blob instanceof test. Not sure if this is an equivalent situation or a matter of style. In this case, we only create the Observable if the blob is the correct type, otherwise we return the throwError Observable.

const readFile = (blob: Blob): Observable<string> =>
{
  if (!(blob instanceof Blob)) {
    return throwError(new Error('`blob` must be an instance of File or Blob.'));
  }

  return Observable.create(obs => {
    const reader = new FileReader();

    reader.onerror = err => obs.error(err);
    reader.onabort = err => obs.error(err);
    reader.onload = () => obs.next(reader.result);
    reader.onloadend = () => obs.complete();

    return reader.readAsText(blob);
  });
}

@jeserkin
Copy link

One question. How would one test this function/method? Especially if one uses TypeScript. BecauseFileReader interface for some reason is not part of Window interface. I am mentioning that, because it would seem only logical, that FileReader should be mocked out for testing purpose.

@iansinnott
Copy link
Author

If you wanted to mock out the file reader interface I would suggest simply passing in a constructor as a second argument with a default so that you can ignore the second argument outside of testing.

const readFile = (blob, Reader = FileReader) => { /* ... */ }

@jeserkin
Copy link

Sounds interesting. Will try it out. Thanks.

@jakehockey10
Copy link

If you were using Jasmine, could you use spyOn to mock FileReader?

@iansinnott
Copy link
Author

Not sure, but using the approach above you could pass in anything you want in place of file reader so I think it would solve this use case. Here's an updated example using typescript and the new Observable syntax from Rxjs 7.

const readFile = (blob: Blob, reader: FileReader = new FileReader()) => new Observable(obs => {
  if (!(blob instanceof Blob)) {
    obs.error(new Error('`blob` must be an instance of File or Blob.'));
    return;
  }

  reader.onerror = err => obs.error(err);
  reader.onabort = err => obs.error(err);
  reader.onload = () => obs.next(reader.result);
  reader.onloadend = () => obs.complete();

  return reader.readAsText(blob);
});

Then in your test code something like this. Not sure what you would want here but that would depend on your test suite.

class MockReader { /* ... */ }
const reader = new MockReader();
await readFile(someBlob, reader).toPromise();
expect(reader.readAsText).toHaveBeenCalled();

@OliverJAsh
Copy link

I think this should also call reader.abort() when the Observable is unsubscribed from.

@MuazSamli
Copy link

here is a simple implementation of opening an image in browser using RxJS 7.1

<input type='file' id='file-input'>
<br>
<img src="" id='image-preview' width='200px'>

<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.1.0/rxjs.umd.js"></script>
<script>
  const { fromEvent, Observable } = rxjs;
  const { flatMap } = rxjs.operators;
  
  var fileSelectStream = fromEvent(document.getElementById('file-input'), 'change');
  
  function createRxObservable(fileNameEvent){
    var file = fileNameEvent.target.files[0];
  
    const readFile_rx_observable = Observable.create(
      function(subscriber) {
        var reader = new FileReader();
        reader.onload = function(e) {
          file.content = e.target.result;
          subscriber.next(file);
        };
        reader.readAsDataURL(file);
      }
    );
    return readFile_rx_observable;
  }
  
  var fileReadStream = fileSelectStream
     .pipe(flatMap(createRxObservable));
  
  fileReadStream.subscribe((file)=>{
    document.getElementById('image-preview').src = file.content;
  });
</script>

a working fiddle is here: https://jsfiddle.net/MuazSamli/5urd9bcq/25/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment