"Pattern" for custom Blaze component package

#1

Hey all :blush:

I’m trying to modernize a large Meteor app to benefit from all the amazing stuff Meteor 1.7+ has to offer.

I think I want to move our common components, such as text_box, modal, prompt, title etc. to a separate package and I’m wondering how I should go about doing it in the most flexible, “tree-shakeable” and convenient way.

So far the best approach I’ve come up with is to just put the files inside the package, exclude them from the mainModule and then cherry-pick them by importing directly, like so:

// package contents
text_box/
   index.js
   text_box.m.css
   text_box.html
section_title/
   index.js
   section_title.m.css
   section_title.html
package.js

This is how you consume them in the app:

import 'meteor/arggh:ui/text_box';

Template.signup_form.helpers({ ...
<template name="signup_form">
   {{> text_box }}
</template>

Using this approach:

:+1: Only the imported templates are actually imported

:-1: I need to use the file system as part of the import call

:-1: For brevity and not having to type the component’s name twice, all js files have to be named index.js

Any tips or best practices to share, before I spend the next 48hrs refactoring about 400 components in this fashion?:thinking:

1 Like
#2

I’d suggest something I like to call the Component Loader pattern. We successfully manage about 1500 components and templates like this, with conditional loading (via dynamic imports) where needed. This pattern makes a lot of sense in the case of heavy components.

So have all your components in the package, organised as you say. I’d suggest having them grouped, then for each group an imports.js file:

import './text_box';
import './section_title';

Then add a loader.html file:

<template name="text_box_loader">
  {{#if loading}}
    <div class="my-loading-spinner-or-some-dummy-content-height-etc"></div>
  {{else}}
    {{> text_box}}
  {{/if}}
</template>

<template name="section_title_loader">
  {{#if loading}}
    <div class="my-loading-spinner-or-some-dummy-content-height-etc"></div>
  {{else}}
    {{> section_title}}
  {{/if}}
</template>

And ultimately a loader.js file, where you orchestrate everything in a loop:

import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';

import './loader.html';

[
  'text_box_loader',
  'section_title_loader',
].forEach((t) => {
  Template[t].onCreated(function () {
    this.loading = new ReactiveVar();
  });

  Template[t].onRendered(async function () {
    if (this.imported || this.loading.get()) { return; }
    this.loading.set(true);
    await import('./imports.js');
    this.loading.set(false);
    this.imported = true;
  });

  Template[t].helpers({
    loading: () => Template.instance().loading.get(),
  });
});

If you don’t want to group your imports, just write a loader for each component using the pattern above, and add the loader to mainModule.

Then everywhere in your app, in the HTML files, you simply use the <...>_loader templates.

You can improve even further with Template.dynamic, and loading components as the router renders layouts. We found that the possibilities are huge.

P.S. The imported flag is necessary, because you will notice a flicker caused by this.loading reactive var changing every time you render the component loader.

2 Likes
#3

Thanks for the suggestion! I use a similar approach in a situation where I have different layouts for the same component (which sounds wacky, but in our use case it makes perfect sense).

In that case, based on some props, the “loader” -component loads whatever layout is required and then uses the props to render that.

However, in most cases I would personally keep the loaders/spinners/dummies outside of the component logic, if we’re talking about granular, simple components that get widely reused. Just to avoid “spinner hell”.

I’m more interested in how to best structure a package consisting purely of Blaze components, and how to import these components in Meteor, so I’d have the best possible…

  • support for Intellisense
  • minimal bundle size
  • easy naming conventions / importing.

In my fantasy world, I would just do this:

import { title } from 'meteor/arggh:ui';
{{#title}}So nice, wow{{/title}}

…in a way, that doesn’t auto-import all other 500 components in the arggh:ui ! Though I think I might be nitpicking, as it’s not that far off from…

import 'meteor/arggh:ui/title`;
{{#title}}So nice, wow{{/title}}
#4

You are right and I only put that there for illustration purposes. We use a spinner only when we load one large UI component, say an entire app section, where it actually improves the UX.

That would be ideal, but we know it’s impossible.

Maybe I did a poor job at describing the pattern, or I got your requirements wrong. What I suggested should actually help with 1) minimal bundle size, and 2) easy importing. It surely does not help with Intellisense.

This is not what I meant. If you split your 400 or so components into categories, grouping those that are likely to be rendered together (e.g. form elements), then you will have only the category pulled via dynamic import when one of the category’s components’ loader is rendered.

Or for those large components that are worth the effort, have a separate lightweight loader for each.

And only these loaders should be added to mainModule.

P.S. I personally find it easier to only add

{{> title_component_loader}}

…than to put

{{> title_component}}

and also remember to put import 'meteor/arggh:ui/title;` in the accompanying JS file. This way I keep the import logic & strategy within the loader.

#5

Another approach using dynamic imports could export a Proxy, that itself resolves properties with a wrapped Promise.all. The package.js would only contain an `api.mainModule’ with such a code:

function _load (...args) {
  return Promise.all(args)
}

const templates = {
  title () {
    return import('./title/title')
  },
  description () {
    return import('./description/description')
  },
  load () {
    return _load
  }
}

export default new Proxy(templates, {
  get: function (obj, prop) {
    return obj[prop]()
  }
})

In your template you would of course be required to wait until the templates have been loaded:

import temp from 'meteor/my:templates'
import './main.html';

Template.hello.onCreated(function helloOnCreated() {
  const instance = this
  instance.loadComplete = new ReactiveVar(false);

  // getting the names resolves the templates
  // already due to the Proxy handler
  temp.load(temp.title, temp.description).then(values => {
    instance.loadComplete.set(true)
  })
});

Template.hello.helpers({
  loadComplete () {
    return Template.instance().counter.get()
  }
});

Where the HTML would look like this:

<template name="hello">
{{#if loadComplete}}
    {{> title text="asadasd"}}
    {{#description}}sdadasdadasdasdsadsadasdasd{{/description}}
{{/if}}
</template>

Not sure if this overcomplicates things but I like the idea of maxing out the dynamic import capability. Does this makes any sense?

#6

That’s almost exactly how I already handled the dynamic importing of different layouts/themes for certain components, but for now, I opted to keep dynamic imports out of the component package and instead go with the plan I presented in the opening post.

I have a index.js as a mainModule in the component package that imports all of the components, but it’s marked as { lazy: true }.

This way, I can omit all of those components from the initial bundle simply by not importing my arggh:components directly at that point, but instead cherry-pick the needed components: import 'meteor/arggh:components/login_form'

After the user is past the login or signup form, I will dynamically import rest of the components in the same manner as needed, or just all of them at once with a single import call import('meteor/arggh:components');

So far this seems to be working pretty well.