9

I am trying to convert my promise based code to RxJs but have a hard time to get my head around Rx especially RxJs.

I have a an array with paths.

var paths = ["imagePath1","imagePath2"];

And I like to load images in Javascript

var img = new Image();
img.src = imagePath;
image.onload // <- when this callback fires I'll add them to the images array

and when all Images are loaded I like to execute a method on.

I know there is

Rx.Observable.fromArray(imagepathes)

there is also something like

Rx.Observable.fromCallback(...)

and there is something like flatMapLatest(...) And Rx.Observable.interval or timebased scheduler

Based on my research I would assume that these would be the ingredients to solve it but I cannot get the composition to work.

So how do I load images from a array paths and when all images are loaded I execute a method based on an interval?

Thanks for any help.

silverfighter
  • 6,762
  • 10
  • 46
  • 73

7 Answers7

13

At first you need a function that will create a Observable or Promise for separate image:

function loadImage(imagePath){
   return Rx.Observable.create(function(observer){
     var img = new Image();
     img.src = imagePath;
     img.onload = function(){
       observer.onNext(img);
       observer.onCompleted();
     }
     img.onError = function(err){
       observer.onError(err);
     }
   });
}

Than you can use it to load all images

Rx.Observable
  .fromArray(imagepathes)
  .concatMap(loadImage) // or flatMap to get images in load order
  .toArray()
  .subscribe(function(images){
    // do something with loaded images
  })
Bogdan Savluk
  • 6,274
  • 1
  • 30
  • 36
2

I think you don't have to create an Observable yourself for this.

import { from, fromEvent } from 'rxjs';
import { mergeMap, map, scan, filter } from 'rxjs/operators';

const paths = ["imagePath1","imagePath2"];

from(paths).pipe(
   mergeMap((path) => {
      const img = new Image();

      img.src = path;
      return fromEvent(img, 'load').pipe(
          map((e) => e.target)
      );
   }),
   scan((acc, curr) => [...acc, curr], []),
   filter((images) => images.length === paths.length)
).subscribe((images) => {
   // do what you want with images
});
DongBin Kim
  • 1,799
  • 5
  • 23
  • 43
1

I don't think you can do that easily with observables, as there's nothing there to indicate a finish (unless you have an initial size). Look at the other answers for the Rx version.

However, you can use an array of Promises:

/**
 * Loads an image and returns a promise
 * @param {string} url - URL of image to load
 * @return {Promise<Image>} - Promise for an image once finished loading.
 */
function loadImageAsync(url) {
    return new Promise(function(resolve, reject) {
        var img = new Image();
        img.src = imagePath;
        image.onload = function() { resolve(img); };
        image.onerror = reject;
    });
}

And with that, you can easily do something like this:

var imageUrls = ['url1', 'url2', 'url3'];
Promise.all(imageUrls.map(loadImageAsync))
    .then(function(arrayOfImageElements) {
        // All done!
    });
Madara's Ghost
  • 172,118
  • 50
  • 264
  • 308
  • Even though I said that, I would seriously love if someone were to prove me wrong, and show a way with Rx observables. – Madara's Ghost Jul 12 '15 at 10:47
  • Couldn't you use [startAsync](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/startasync.md) and [merge](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/merge.md) to achieve the same with a complete callback? – marekful Jul 12 '15 at 10:53
  • @marekful Wouldn't that be exactly the same, only with the added overhead of the Rx init? – Madara's Ghost Jul 12 '15 at 10:55
  • uhh... RxJS observables have completion callbacks. Of course you can indicate finish. – Benjamin Gruenbaum Jul 12 '15 at 10:58
  • @BenjaminGruenbaum How can you indicate the finish of loading a bunch of images? If I read it correctly, in his current code, the `onload` handler pushes a new value into the observable. How can you tell when all of them finished? – Madara's Ghost Jul 12 '15 at 11:00
  • @MadaraUchiha I've added an answer showing how to do this. – Benjamin Gruenbaum Jul 12 '15 at 11:09
  • Thanks for the reply, i have it running already with Promises looking especially to convert it to RX. Image is an only from code this fires onload when it is loaded. Maybe I need to write a custom observer? – silverfighter Jul 12 '15 at 11:25
  • @silverfighter Have a look at the other answers for an observable example. Basically, you create a small observable for each image, and then merge them to form a larger observable. – Madara's Ghost Jul 12 '15 at 11:25
  • @silverfighter it's perfectly fine to mix and match Rx with promises. Rx supports this. – Benjamin Gruenbaum Jul 12 '15 at 12:09
1
function loadImage(url){
    var img = new Image;
    img.src = url;
    var o = new Rx.Subject();
    img.onload = function(){ o.onNext(img); o.onCompleted(); };
    img.onerror = function(e){ o.onError(e); }; // no fromEvent for err handling
    return o;
}

var imageUrls = ['url1', 'url2', 'url3'];
var joined = Rx.Observable.merge(imageUrls.map(loadImage));

// consume one by one:
joined.subscribe(function(item){
    // wait for item
});

joined.toArray().subscribe(function(arr){
    // access results array in arr
});

Or in short:

var imageUrls = ['url1', 'url2', 'url3'];
fromArray(imageUrls).map(url => {
    var img = new Image;
    img.src = url;
    return fromEvent(img, "load");
}).toArray().subscribe(function(arr){
    // access results here
});
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Aha, so basically very similar to the Promises version. +1. – Madara's Ghost Jul 12 '15 at 11:11
  • @MadaraUchiha I've added a 'sugar' version at the bottom just now. – Benjamin Gruenbaum Jul 12 '15 at 11:11
  • Using `Subject` is overkill, also in case of an error you will not be able to use `retry` operator on `loadImage` result. Using `merge` to concatenate results also is a bit wrong - you will get images in load order, but not in order from initial url list. – Bogdan Savluk Jul 12 '15 at 11:29
  • @BogdanSavluk I've provided an answer with subject and an answer without it using `fromEvent` for error handling. You can wrap loadImage in a way that makes it retriable easily but that's not really important. In practice I'd use the `fromEvent` code most likely. Order isn't important and if it was I'd just `reduce` or `flatMap`, it's not important since OP can always use the `.src` on the results and know which is which. - the key part is the `.toArray` anyway which converts a sequence of n events into one event for an n result array. – Benjamin Gruenbaum Jul 12 '15 at 11:32
  • It looks like fromEvent does not wait on the callback, so it returns an AnonymousObservable with an undefined source. fromCallback works kind of but does return the callback instead of the image - Can you confirm this or did I misunderstand something? – silverfighter Jul 13 '15 at 08:00
1

The other RX based solutions here did not really work for me. Bogdan Savluk’s version did not work at all. Benjamin Gruenbaum’s version waits until an image is loaded before starting to load the next image so it gets really slow (correct me if I am wrong) Here is my solution which just compares the total amount of images with the number of already loaded images and if they are equal, the onNext() method of the returned Observable gets called with the array of images as an argument:

var imagesLoaded = function (sources) {

  return Rx.Observable.create(function (observer) {

    var numImages = sources.length
    var loaded = 0
    var images = []

    function onComplete (img) {
      images.push(img)
      console.log('loaded: ', img)

      loaded += 1
      if (loaded === numImages) {
        observer.onNext(images)
        observer.onCompleted()
      }
    }

    sources.forEach(function (src) {
      var img = new Image()
      img.onload = function () {
        onComplete(img)
      }
      console.log('add src: ' + src)
      img.src = src
      if (img.complete) {
        img.onload = null
        onComplete(img)
      }

    })

  })

}

Usage:

console.time('load images'); // start measuring execution time

imagesLoaded(sources)
  // use flatMap to get the individual images
  // .flatMap(function (x) {
  //   return Rx.Observable.from(x)
  // })

  .subscribe(function (x) {
    console.timeEnd('load images'); // see how fast this was
    console.log(x)
  })
gang
  • 1,758
  • 2
  • 23
  • 36
1

Here is the Angular / Typescript version to load an Image with RxJS:

import { Observable, Observer } from "rxjs"; 

public loadImage(imagePath: string): Observable<HTMLImageElement> {
  return Observable.create((observer: Observer<HTMLImageElement>) => {
    var img = new Image();
    img.src = imagePath;
    img.onload = () => {
      observer.next(img);
      observer.complete();
    };
    img.onerror = err => {
      observer.error(err);
    };
  });
}
Tilo
  • 605
  • 7
  • 15
0

Here a really better implementation that cancels loading of the image in case you unsubscribe from the Observable https://stackblitz.com/edit/rxjs-loadimage?file=index.ts

import { Observable, Subscriber } from "rxjs";

/**
 * RxJS Observable of loading image that is cancelable
 */
function loadImage(
  url: string,
  crossOrigin?: string
): Observable<HTMLImageElement> {
  return new Observable(function subscriber(subscriber) {
    let img = new Image();
    img.onload = function onload() {
      subscriber.next(img);
      subscriber.complete();
    };
    img.onerror = function onerror(err: Event | string) {
      subscriber.error(err);
    };
    // data-urls appear to be buggy with crossOrigin
    // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767
    // see https://code.google.com/p/chromium/issues/detail?id=315152
    //     https://bugzilla.mozilla.org/show_bug.cgi?id=935069
    // crossOrigin null is the same as not set.
    if (
      url.indexOf("data") !== 0 &&
      crossOrigin !== undefined &&
      crossOrigin !== null
    ) {
      img.crossOrigin = crossOrigin;
    }
    // IE10 / IE11-Fix: SVG contents from data: URI
    // will only be available if the IMG is present
    // in the DOM (and visible)
    if (url.substring(0, 14) === "data:image/svg") {
      // TODO: Implement this :)
      // img.onload = null;
      // fabric.util.loadImageInDom(img, onLoadCallback);
    }
    img.src = url;
    return function unsubscribe() {
      img.onload = img.onerror = undefined;
      if (!img.complete) {
        img.src = "";
      }
      img = undefined;
    };
  });
}

// Example
const cacheBurst = new Date().getTime();
const imgUrl = `https://i.pinimg.com/originals/36/0c/62/360c628d043b2461d011d0b7f9b4d880.jpg?nocache=${cacheBurst}`;

const s = loadImage(imgUrl).subscribe(
  img => {
    console.log("Img", img);
  },
  err => {
    console.log("Err", err);
  }
);

setTimeout(() => {
  // uncomment to check how canceling works
  // s.unsubscribe();
}, 100);

the_smoke
  • 31
  • 4