Reactive template helper?

Hey,
Quite new to full stack javascript in general so be nice, just started with Meteor

Now what I’m trying to do is to get the Steam avatar based on the Steam ID in my template like this:

 {{#if Template.subscriptionsReady}}
        <div class="comment">
        <a class="avatar">
                <img src="{{steamAvatar userid}}">
        </a> 

steamAvatar is the helper function i wrote that queries the steam api using the SteamID (userid) as argument

This is the javascript of the template:

Template.serverchat.helpers({
    'steamAvatar': (async function(steamID) {
        try {
Template.instance().avatar.set(await Meteor.callPromise('steam.getAvatarURL',steamID))
        Template.instance().avatarsDownloaded.set(true);
        return Template.instance().avatar.get();
    } catch(err) {
        console.log(err);
    }

I hope everything is in somewhat right direction,
The error i get in the browser is:

TypeError: Cannot read property 'avatar' of null
    at Object.steamAvatar (serverchat.js:35)

Any help is greatly appreciated

What does your Template onCreated hook look like? Do you actually define the avatar ReactiveVar?

Hey,
Yeah I do. Should’ve posted it but forgot!

Template.serverchat.onCreated(function () {
    var instance = this;

    // Steam Avatars
    instance.avatar = new ReactiveVar();
    instance.avatarsDownloaded = new ReactiveVar(false);
    
    instance.autorun(() => {
    instance.avatarsDownloaded.set(false);
    });
});

Unfortunately helpers can’t be async. You will need to do the fetching in onCreated and only grab the fetched data and return it in your helper.

Or even better, grab the avatarUrl when the user document is created and store it in the database. That way you save having to query it every time (maybe with a cron job to check for changes every now and then)

Also, be careful of using both .set and .get in the same function. Since using get registers a dependency on that call and every set will flag the function to be run again, you create an infinite loop where each run triggers the function to be run again.

1 Like

Thanks a lot for taking the time explaining.
Yeah I suspected I would have a hard time since googling it for several days only ended up with such examples.

While I am at it, I was thinking about having a collection called like “SteamCache” that saves a date everytime it fetches new data from the API. So it only performs a request a set time after the last one Is there a smart way to check if an item in the collection is x days old?

My starting point is this:

let avatarURL = await steam.getUserSummary(SteamID).then(response => {return response['avatar']['medium']});
            // Update or Insert (if it does not exist) values from the Server
            SteamCache.update({ _id: SteamID},{
                _id: SteamID,
                avatarURL: avatarURL,
                cachedTime: new Date()
            }, {upsert: true});

Please feed me back too if my code can be improved, thanks again

There are a few things you could do to improve the code:

That will only ever run once, as there are no reactive dependencies (.set() of a ReactiveVar does not establish a dependency). You could remove that section of code - it does no more than the instance.avatarsDownloaded = new ReactiveVar(false); you declared above it. However, you probably don’t need that flag at all!

The canonical way of getting data from a Meteor Method and presenting it with a Template Helper is to put the method call in the onCreated. That means something like this:

Template.serverchat.onCreated(function () {
  // Steam Avatars
  this.avatar = new ReactiveVar();

  Meteor.call('steam.getAvatarURL', steamID, (error, result) => {
    if (error) {
      // handle error
    } else {
      this.avatar.set(result);
    }
  });
});

(Note that you do not seem to define steamId anywhere - you just suddenly use it: its value will be undefined at that point.)

Your helper is then simplified to something like this:

Template.serverchat.helpers({
  steamAvatar() {
      return Template.instance().avatar.get();
  },
});

Reading between the lines, your Meteor method should be something like this (I’ve left out caching of data for simplicity):

Meteor.methods({
  async 'steam.getAvatarURL'(SeamId) {
    try {
      const response = await steam.getUserSummary(SteamID);
      return response.avatar.medium;
    } catch (error) {
      throw new Meteor.Error('E123', error.message);
    }
  },
});

I’m assuming your actual method sets up the steam object appropriately for the method.

The easiest way to do this based on your caching code would be to check the database first:

const cachedData = SteamCache.findOne({ _id: SteamId, cachedTime: { $gt: timeNow - xDays } });

Which will only return the cached data if it exists and is recent (you’ll need to replace timeNow - Days with sensible code). If you get nothing back, you will need to re-cache. However, as it stands, that is not a safe way to handle caching: if you add another property and reset the cachedTime, it will affect all properties on the document. You may be better off attaching a timestamp to each property you add:

avatarURL: {
  value: avatarURL,
  cachedTime: new Date()
},

and changing the query appropriately.

3 Likes

You are a legend man! I am soaking it all up, gonna test it and see if it works :smiley:

1 Like

Its supposed to take the value from the blaze template

<div class="comment">
        <a class="avatar">
                <img src="{{steamAvatar userid}}">
        </a> 

The userid in my case is also the SteamID, being output from a collection containing the list of ids

1 Like

Okay… trying to keep the sanity together a bit.
While your answer was tremendous help to understand the Meteor concepts @robfallows unfortunately it did not resolve the problem itself.

My understanding is since the
template helper steamAvatar is depending on the argument userid which is referred to as SteamID in the functions/methods, but userid is not available or rendered in the template so a null value is passed to my method.

I can’t call steam.getAvatarURL in the onCreated because it is relying on {{each SteamUser}} to be rendered? (which i happened to omit, i’m sorry) here is the full template

{{#each SteamProfile}}
        {{#if Template.subscriptionsReady}}
        <div class="comment">
        <a class="avatar">
                <img src="{{steamAvatar userid}}">
        </a> 
        <div class="content">
                <a class="author">{{username}}</a>
                <div class="metadata">
                <span class="date">{{time}}</span>
                </div>
                <div class="text">{{splitaftercolon message}}</div>
        </div>  
        </div>
        {{else}}
        <div class="ui fluid placeholder">
                <div class="ui active inverted dimmer">
                        <div class="ui text loader">Loading...</div>
                </div>
        </div>
        {{/if}}
        {{/each}}

You are using Template.subscriptionsReady in your template, but you do not seem to have any pub/sub going on in the code you’ve shared. However, I see each SteamProfile, so I’m assuming that’s a helper using the results of a subscription?

At the moment, I’m unsure what you’ve got in place (not helped by a few typos in my code sample, either!). That’s making it very difficult to help.

Yes, you are correct they are fetching data from a collection.
This is the rest of the code for the subs/pubs

var SteamProfileCollection = new Mongo.Collection('steamprofiles');
Template.serverchat.helpers({
    'SteamProfile': function(){
        return SteamProfileCollection.find();
        });
    }
});

OK. I’m getting a clearer picture now. I think you would find it simpler to use a sub-template within your serverchat template and moving the contents of #each into that new template.

If you do that, you’ll immediately get the current document in the template context, which means the method call will know the correct userid to use. Incidentally, there seem to be several variations on the name to use for userid. It would make life easier if these were standardised - perhaps to the name in the steamProfileCollection document. I’m assuming userid for now.

Split your current serverchat template. I’ve created a new steamuser template. Note that I’ve moved the Template.subscriptionsReady test outside the loop, or it all gets messy. Something like this:

<template name="serverchat">
  {{#if Template.subscriptionsReady}}
    {{#each SteamProfile}}
      {{> steamuser}}
    {{/each}}
  {{else}}
    <div class="ui fluid placeholder">
      <div class="ui active inverted dimmer">
        <div class="ui text loader">Loading...</div>
      </div>
    </div>
  {{/if}}
</template>

<template name="steamuser">
  <div class="comment">
    <a class="avatar">
      <img src="{{steamAvatar}}">
    </a>
    <div class="content">
      <a class="author">{{username}}</a>
      <div class="metadata">
        <span class="date">{{time}}</span>
      </div>
      <div class="text">{{splitaftercolon message}}</div>
    </div>
  </div>
</template>

and move the helpers referenced in the new (steamuser) template:

Template.steamuser.onCreated(function () {
  // Steam Avatars
  this.avatar = new ReactiveVar();

  Meteor.call('steam.getAvatarURL', this.data.userid, (error, result) => {
    if (error) {
      // handle error
    } else {
      this.avatar.set(result);
    }
  });
});

Template.steamuser.helpers({
  steamAvatar() {
    return Template.instance().avatar.get();
  },
  splitaftercolon(message) {
    // something in here
  },
});
1 Like

It works! It’s beautiful.
Can’t thank you enough for the very articulate response and even providing code!

1 Like