Passing external data into a template


#1

I’ve just tried a few things and Googled a few searches, but haven’t gotten anywhere…

Specifically I want to pass some Stripe invoices into a template of my app. These invoices don’t come from the db, but from an API call to Stripe. The data is returned as json. Ideally I’d just be rendering this data within the template using {{#each invoices }}. Reactivity isn’t important in this case, but is always nice to maintain.

If I use the publish/subscribe method - how do I go about it? (I’ve looked for examples that don’t involve a database collection and can’t find any) - the only way I’ve set up publish/subscribe relationships is pretty reliant on there being a db collection. I’ve had a quick go trying to use methods, helpers etc. But haven’t really gotten anywhere - and rather than making something work I figured it’s best to check what the preferred method is.

tl;dr: How do I make arbitrary json available to a template, and where do I fetch it?


#2

The easiest way would be to make the call to Stripe, and store the invoices to a local collection (whether to do this through a cron job or pressing some button, that’s up to you). Then all you need to do is subscribe to that collection and it will be available to your template.

You could also do it through calling a Meteor.method, that calls the Stripe API and returns the json data in the callback.


#3

Hm, that’s an interesting option actually, however due to the frequency in which I expect users to access this data it made more sense (to me) to avoid the overhead of storing the data, and to instead deal with the short delay involved in fetching the invoices on the fly.

With regards to using a method - I did have a quick go at this but I’m not really sure how it fits into the workflow. i.e. Where do I call the method from? Router? Helper? I’ve only really used methods to action something, not retrieve and display data.


#4

@nathanhornby here’s an example from my freshly baked code:

The following example got rewritten to standard javascript template right here

template:

class showReadme extends BlazeComponent
  @register 'showReadme'

  onCreated: ->
    @readmeVar = new ReactiveVar('')
    @getReadmeFromGithub()

  readme: ->
    @readmeVar.get().content

  getReadmeFromGithub: ->
    Meteor.call 'getReadmeFromGithub',
      FlowRouter.getParam 'provider'  
      FlowRouter.getParam 'package'
      (error, result) =>
        @readmeVar.set result if not error

method:

if Meteor.isServer
  Meteor.methods
    getReadmeFromGithub: (provider, packageName) ->
      Meteor.http.call "GET", "https://api.github.com/repos" + provider + "/" + packageName + "/readme",
        headers: 
          "User-Agent": "brajt"
          "Accept": "application/vnd.github.v3.html"

Pay attention that I run this method only on server, without a stub, to avoid cross-server api calls.

In my example I call it once in onCreated as Github there’s no need for readme data to be reactive. But you can call it in autorun, a helper (make sure this helper actually gets initialized in your html) or in a click button action.


#5

Hi @brajt

Excuse my ignorance but I’m struggling to work out what the first block is;

Parsing the coffeescript isn’t making it any easier tbh - but it doesn’t look familiar. Is this a route, helper, or?


#6

Yeah I just copypasted it from my project.

It’s a package called peerlibrary:blaze-components. You can replace it with a typical Blaze template, it will still work.


#7

Sorry @brajt - what do you mean a typical blaze template? In my book that would be a bunch of HTML and curly brackets - but I don’t see how what you’ve provided fits in there… appreciate I’m obviously missing something!


#8

Well, a typical Blaze template contains two files. One is a html file, another a javascript file with helper and action functions.

So inside you’d have Template.myTemplate.onCreated with meteor call and Template.myTemplate.helpers with readme helper.


#9

Ok I’m following, I wasn’t really considering the helpers and actions as part of the template - but that of course makes sense.

However I’m still not sure how that code would apply… it’s likely more advanced than I am.

i.e. where is getReadmeFromGithub fitting in? Is that a helper? an action? The setup looks completely alien to me.

Maybe it;d help if I gave more context from my end - I’ve been trying to get a meteor call to actually give me something, and am getting nowhere, it’s always undefined (I’ve tried sync and async methods of using meteor call):

Router.route("/app/settings/billing", {
  name: 'BillingSettings',
  controller: 'DashboardController',
  data: {
    invoices: function() {
      var invoices = Meteor.call("GetStripeInvoices");
      console.log(invoices);
      return invoices;
    }
  },
  action: function () {
    this.render('BillingSettings');
  }
});

I tried sticking the call in data having seen a similar approach in an SO, but I’ve tried it pretty much everywhere, still always undefined and even if it wasn’t I’m not sure this is how I get it into the variable pool.

I had the same end result when trying to get the data into a helper (but this process is even less familiar to me, I’ve at least gotten data into templates via the router and publish/subscribe).


#10

I’ll rewrite it for you in JS with use of normal template helpers like in Meteor tutorial. Will be back in few minutes.


#11

Ah that’s very kind of you, above and beyond but appreciated!


#12

I’d do it like @brajt, call the method at the template level in the onCreated event. The catch is your data is not reactive.


#13

@nathanhornby rewritten as promised. I did it fast without checking, so there’s 1% chance it lacks a bracket somewhere.

template:

Template.myTemplate.onCreated({
  this.readmeVar = new ReactiveVar('');
  Meteor.call('getReadmeFromGithub', 
    FlowRouter.getParam('provider'), 
    FlowRouter.getParam('package'), 
    (function(_this) {
      return function(error, result) {
        if (!error) {
          return Template.instance().readmeVar.set(result);
        }
      };
    })
  (this));
});

Template.myTemplate.helpers({
  readme: function(){
    return Template.instance().readmeVar.get();
  }
});

method:

if (Meteor.isServer) {
  Meteor.methods({
    getReadmeFromGithub: function(provider, packageName) {
      return Meteor.http.call("GET", "https://api.github.com/repos" + provider + "/" + packageName + "/readme", {
        headers: {
          "User-Agent": "brajt",
          "Accept": "application/vnd.github.v3.html"
        }
      });
    }
  });
}

But like I said, you can also put this method.call into separate helper, as long (as you init this helper’s code in your HTML later by {{myHelper}}) or in a “click button” action.


#14

I had to remove the var from the first line as it tripped on that;

Unexpected token this

However even then I’m getting in the (browser) console:

Exception in template helper: TypeError: Cannot read property ‘get’ of undefined

For completeness my version of your code:

Template.BillingSettings.onCreated( function () {
  this.getInvoices = new ReactiveVar(null);
  Meteor.call('GetStripeInvoices', (function(_this) {
    return function(error, result) {
      if (error) {
        console.log(error);
        throw new Meteor.Error( 500, "Couldn't get invoices" );
      }
      return _this.getInvoices.set(result);
    };
  })
  (this));
})
Template.BillingSettings.helpers({
  invoices: function () {
    return this.getInvoices.get();
  }
})

This is feeling awfully complicated considering all I need to do is make some json available to a template :frowning:

(Edit: Thanks a lot for doing that btw @brajt - whether I succeed or not I appreciate it!)


#15

Yeah I could break something in the process of rewriting my code. But it should be enough as an inspiration.

I agree, it gets complex when I look at the JS version. :slight_smile: I did it for a particular use case and for me works fine.


#16

Bugger - again thanks for your time - I’ll continue hashing it out. Not often I wish I could just use PHP :smiley:


#17

That’s a leftover of rewriting Blaze Components to Blaze template. In your case you need to get the reactiveVar from Template.instance().

Template.BillingSettings.helpers({
  invoices: function () {
    return Template.instance().getInvoices.get();
  }
})

I assume you’ve got reactiveVar package in the project. :wink:


#18

Thanks for spotting that, did indeed get rid of the last error! (and yup, I have the ReactiveVar package!)

Running a log within the helper is still giving me undefined, however. Is that expected? i.e.

invoices: function () {
  console.log(Template.instance().getInvoices.get());
  return Template.instance().getInvoices.get();
}

 
undefined

(actually empty first, then undefined…)

I can’t get any output in the template still but I’m not sure if that’s because of how I’m trying to drill into the json - I’m guessing the helper should log it correctly, even if not immediately?


#19

Two things here.

First - is your api get call actually returning any data. Can you check it in your meteor.method with console.log? It doesn’t even have to be in the callback, just do it after the http.call.

Second is the proper way of forcing the meteor.call callback to use the template’s data context. By default, callback creates a new, separate context, which makes it difficult to pass the data to our reactiveVar. In CoffeeScript it’s very easy to implement with the use of => but in JS it’s more tricky. The way I implemented it in the rewritten JS should work, but it wasn’t tested.

So i’d do another console.log right before getInvoices.set(result) and check it.

Perhaps use Template.instance().getInvoices.set(result) there too.

Somebody more fluent with JS would be handy here, as I’m a CS guy. Please, help! :slight_smile:


#20

You can write the JS like this, much easier to read

Template.BillingSettings.onCreated(function () {
  this.getInvoices = new ReactiveVar();

  var self = this;
  Meteor.call('GetStripeInvoices', function (error, result) {
      if (error) {
        console.log(error);
        throw new Meteor.Error(500, "Couldn't get invoices");
      }
      self.getInvoices.set(result);
  });
});