Howto: dynamically importing moment.js in helper

I want to dynamically import the moment.js package. I’m hoping someone will be kind enough to help me.

I just upgraded to Meteor 1.5.

I ran the following command:

$ meteor remove momentjs:moment && npm install --save moment

I have a Blaze helper function that uses moment to format dates:

  FormatDate () {
    const moment = import('moment');

    if (this.DateUpdated) {
      return moment(new Date(this.DateUpdated)).startOf('hh').fromNow();
    } else {
      return 'no activity';
    }
  }

I run the app and from the console, this is what the function looks like:

  FormatDate: function () {                                                                                        // 119
    var moment = module.dynamicImport('moment');                                                                       // 120
                                                                                                                       //
    if (this.DateUpdated) {                                                                                           // 122
      return moment(new Date(this.DateUpdated)).startOf('hh').fromNow();                                                        // 123
    } else {                                                                                                           // 124
      return 'no activity';                                                                                            // 125
    }                                                                                                                  // 126
  }    

And I get the following error:

Exception in template helper: TypeError: moment is not a function

My package.json:

  "dependencies": {
    "babel-runtime": "^6.23.0",
    "bcrypt": "^1.0.2",
    "moment": "^2.18.1",
  },

Edit, as I forgot the dynamic part =)

It seems like helpers and async don’t really play well together, so I’d probably put the import statement:

await import moment from 'moment';

in my onCreated, and block the contents of the template from loading until the import was resolved?

I got the syntax from Ben Newman’s video specifically (about 13:30 min in) and other related stuff I had to scour for in general.

  1. I thought we (can) use the import syntax assigned to a variable

  2. I thought we use (only) await inside async functions

Your syntax looks like something I would place at the top of a module (file)

From the video:

You’re right. I just opened up the 1.5 version of my app and I have:

const x = await import('/imports/ui/modules/index');

The async part is true, which is why I suggested moving the import function out of the helper. If you take a look here, you’ll see that one workaround is to rely on the onCreated and a reactiveVar, which is what I was proposing, albeit in not as specific terms.

Template.myTemplate.onCreated(async function () {
  const self = this;
  self.readyVar = new ReactiveVar(false);
  self.moment = await import('moment');
  self.readyVar.set(true);
});

Template.myTemplate.helpers({
  FormatDate () {
    const moment = Template.instance().moment;
    if (this.DateUpdated) {
      return moment(new Date(this.DateUpdated)).startOf('hh').fromNow();
    } else {
      return 'no activity';
    }
  },
  ready () {
    return Template.instance().readyVar.get();
  },
});
<template name="myTemplate">
  {{#if ready}}
   <!-- your helper, etc -->
  {{/if}}
</template>
1 Like

The onCreated function is aync out of the box by using ‘async function’ on the inside the call like you have shown? Is this the only way to get dynamic imports inside the helper?

Please see the example from the original post. I took that code from the console; it shows that Ben has transpiled the import statement inside the helper into a dynamicImport. I’ll post it here again for reference:

  FormatDate: function () {                                                                                        // 119
    var moment = module.dynamicImport('moment');                                                                       // 120
                                                                                                                       //
    if (this.DateUpdated) {                                                                                           // 122
      return moment(new Date(this.DateUpdated)).startOf('hh').fromNow();                                                        // 123
    } else {                                                                                                           // 124
      return 'no activity';                                                                                            // 125
    }                                                                                                                  // 126
  }

I would like to see this, too, but I’m not sure how to make it happen solely within the helper.

Wait, it just occurred to me it could be a possibility, does the code have to reside inside the imports directory in order for dynamic imports to work?

Is anyone putting thier Blaze templates inside the imports directory?

All of our Blaze templates live in /imports. We use entry index.js files that can then easily be linked to routes to load a block of templates.

Can I look at an example project or code snippets? This is my SO question: https://stackoverflow.com/questions/44529444/using-imports-how-does-the-server-com-with-the-client

It’s a private project, but here is a simplified example of what I do (using validated methods):

// /imports/lib/methods/posts.js`

import Posts from '/imports/lib/collections/posts';
import { AuthMixin } from './mixins';

export default {
  delete: new ValidatedMethod({

    name: 'PostMethods.delete',
    action: 'Delete this Post',

    mixins: [AuthMixin],

    validate: new SimpleSchema({
      postId: { type: String },
    }).validator({ clean: true }),

    run({ postId }) {
      return Posts.update({ _id: postId }, { $set: { deleted: true } });
    },
  }),

  restore: new ValidatedMethod({

    name: 'PostMethods.restore',
    action: 'Restore this Post',

    mixins: [AuthMixin],

    validate: new SimpleSchema({
      postId: { type: String },
    }).validator({ clean: true }),

    run({ postId }) {
      return Posts.update({ _id: postId }, { $set: { deleted: false } });
    },
  }),

};
// /imports/ui/posts/postItem.js

import PostMethods from '/imports/lib/methods/posts';

Template.postItem.events({
  'click .js-delete' () {
    PostMethods.delete.call({ postId: this._id }); // assuming you loop through an #each
  },
});
// /server/main.js

import PostMethods from '/imports/lib/methods/posts';

Per the SO answer, you can see that the methods need to be imported server-side, which is what I do in /server/main.js.

I’d be curious to see how others do this, but this strategy has worked well for me so far. It also allows for blocking off methods with {{#if !this.isSimulation}}, when you want to keep code from being imported into the client bundle.

It also keeps the import statements a bit cleaner, as using a default export helps prevent importing a dozen or more named imports that all belong to the same method module.

I always feel like things like this are very wild-western in that there are probably a dozen different ways to structure imports/exports/folders/etc, and best practices are hard to come by.

3 Likes

Thank you. I’ll try to adapt this to what I’m working on.