How to get results from async function in a helper?

Correct - and that’s how I’ve done it before (in onCreated)! My brain must be elsewhere today.

@robfallows’s .set and .get are both inside the helper

  getImage: function(url) {
    var instance = Template.instance();
    //console.log("page:" + url);
    url = Helpers.addHttp(url);

    //var result;

    resolver.resolve(url, function(result) {
      if (result) {
        //console.log(result.image);
        /*//// set the reactiveVar in the callback (when it arrives) */
        instance.imageResolverRequests.set(result.image);
      } else {
        console.log("No image found");
        return '';
      }
    });
    // return the image into your template (when it gets updated)
    return instance.imageResolverRequests.get();
  },

but yeah, now how to deal with the infinite loop. I thought this might work, but when I try it, i get no images at all

    var imgUrl = instance.imageResolverRequests.get();
    instance.imageResolverRequests.set(undefined);
    return imgUrl;

@shock is correct - putting the set in the helper will cause reactive runaway ™. The setting needs to be outside of the helper - in onCreated is a good place, or in an event if you’re kicking things off from a click.

it’s the whole of this

var instance = Template.instance();
//console.log("page:" + url);
url = Helpers.addHttp(url);

//var result;

resolver.resolve(url, function(result) {
  if (result) {
    //console.log(result.image);
    /*//// set the reactiveVar in the callback (when it arrives) */
    instance.imageResolverRequests.set(result.image);
  } else {
    console.log("No image found");
    return '';
  }
});

perhaps in onRendered

not sure how that can be done. The url comes from the data. So we pass it to the helper as a param. There is no click event. The page loads and displays the url and image (ideally).

you should be able to access through this.data in the onRendered callback

sorry i’m lost. So I move just the reactiveVar .set statement from the helper to onRendered? Or move the whole imageGrabber code to the onCreated and put only the .set in the helper?

In onRendered you need to use var instance = this; - it’s the same scope as onCreated.

so remove var instance = Template.instance(); from the helper
and add var instance = this; to onRendered`

you removed the .set statement from the helper. We don’t need it anymore ?

onCreated - declare the reactiveVar

onRendered - handle the whole image grabber, you should be able to access the url from this.data and note Rob’s context comment above, and set the value of the reactiveVar still inside the callback

helper - return the image to the template by getting the reactive var

also, I would have the list of urls in a parent template, and call the image grabber in an #each block per url

Maybe this:

Template.discover.helpers({
  getImage: function() {
    var instance = Template.instance();
    return instance.imageResolverRequests.get();
  }
});

Template.discover.onCreated(function() {
  var instance = this;
  var url = Helpers.addHttp(this.data.url);
  instance.imageResolverRequests = new ReactiveVar();
  resolver.resolve(url, function(result) {
    if (result) {
      instance.imageResolverRequests.set(result.image);
    } else {
      console.log("No image found");
    }
  });
});

Edited for missing ReactiveVar line.

so the instance var is not available in the helper so I changed it to Template.instance()
but seems i’m not getting url in the data in onRendered

here is what I got:

<template name="discover">

...
 {{#each itemsInParcel this}}
                  <div class="panel-footer">
                    name: {{productName}} <br>
                    description: {{productDescription}} <br>
                    url: <a class="text-info" href="{{addHttp productUrl}}" target="_blank">{{productUrl}}</a> <br>
                    <img src="{{getImage productUrl}}" alt="">
                    price: {{priceJPY}}
                  </div>
                {{/each}}
...
Template.discover.onCreated(function() {
  this.imageResolver = new ReactiveVar();
});
var resolver = new ImageResolver();
resolver.register(new ImageResolver.FileExtension());
resolver.register(new ImageResolver.MimeType());
resolver.register(new ImageResolver.Opengraph());
resolver.register(new ImageResolver.Webpage());

Template.discover.onRendered(function() {
  var instance = Template.instance();
  console.log("page:" + this.url);
  url = Helpers.addHttp(this.url);

  resolver.resolve(url, function(result) {
    if (result) {
      instance.imageResolver.set(result.image);
    }
  });

  //$(window).on("scroll", this.viewmodel.getScrollHandler());

  var self = this;
  this.autorun(function() {
    self.viewmodel.loadingParcels(true);
    self.subscribe('parcelsByDays', self.viewmodel.parcelLoadDays(), {
      onReady: function() {
        self.viewmodel.loadingParcels(false);
      },
    });
  });
});
Template.discover.helpers({

  getImage: function() {
    // return the image into your template (when it gets updated)
    return Template.instance().imageResolver.get();
  },

onRendered console log is writing this

page:undefined

I thought onRendered fired once for the page. Will it fire for each url in the for each? or should i create a separate template for the each loop and more the grabber code to that template’s onRendered? Will try.

no luck. crazy. this is one of the hardest problems I’ve had in Meteor

Template.discover.onRendered(function() {
  var instance = Template.instance();

in onRendered it is

instance = this

and this.url means url on this template instance
you probably want something like

Template.currentData().url

or address it somehow else, where you have that url ?

Thanks for all the help on this. 3AM now so I’m going to take a break. Must not be thinking clearly anymore at all.

Cheers!

So I just tested my code above and it works as expected. However, I used an async HTTP call to an external API rather than ImageResolver (which I’ve never used), but the principle is the same. Repo here.

Seems we were all over-thinking it. My CTO came in and solved it without any onRendered or onCreated code. Like this, in a single helper

var resolvedUrls = new ReactiveDict();

Template.discover.helpers({
  getImage: function(itemUrl) {
    itemUrl = Helpers.addHttp(itemUrl);

    resolver.resolve(itemUrl, function(result) {
      if (result) {
        resolvedUrls.set(itemUrl, result.image);
      }
    })
    return resolvedUrls.get(itemUrl);
  },
});
1 Like

I would perhaps add a check before the resolver though, so that you don’t resolve a url several times. A lot of things can trigger a rerun of a template helper.

var resolvedUrls = new ReactiveDict();

Template.discover.helpers({
  getImage: function(itemUrl) {
    itemUrl = Helpers.addHttp(itemUrl);
    var resolvedImage = resolvedUrls.get(itemUrl);
    
    if (resolvedImage) return resolvedImage;

    resolver.resolve(itemUrl, function(result) {
      if (result) {
        resolvedUrls.set(itemUrl, result.image);
      }
    });
    
  },
});

The var resolvedUrls = new ReactiveDict(); is a good idea in this case. However, this variable will be scoped to the file, and not to the instance. This could create issues in some cases where the dict actually contains instance specific state, and you have multiple instances of the template on your page.

1 Like

I noticed that helpers are sometimes called before the template is ready. In cases that happened I was able to fix it by checking if the Template has actually rendered.

var instance = Template.instance();
if(instance.view.isRendered)
{
 // action 
}
1 Like