Modernizing Meteor ideas: `<blaze-component>` Custom Element (Web Component)

We’re trying to “make Meteor great again”:

I think Meteor is still great! But! It is in need of modernization.

So, let’s build get there one step at a time:

One problem Meteor has is that accounts-ui is built with Blaze, and therefore is not compatible with template systems of React, Vue, Angular, Svelte, Solid, etc.

Solution: a <blaze-component> Custom Element

Custom Elements are native HTML elements defined in JavaScript.

I have a custom element I made locally called <blaze-component> that allow me to use any Blaze component declaratively in any framework’s template system (JSX, Vue SFC, Svelte SFC, or even plain HTML). I’ll publish it soon.

It looks like this currently, in plain HTML:

<blaze-component
  tmpl="loginButtons"
  data='{"align": "right"}'
></blaze-component>

The data attribute (for example when used in a plain HTML file, or with .setAttribute) accepts a JSON string. This maps to basically the following equivalent JS inside the Custom Element:

// data is reactive, if the element data value changes, the Blaze component is updated.
Blaze.renderWithData('loginButtons', () => this.data, this)

Because this is “just an HTML element”, and all web frameworks can control HTML elements, this can be used in any framework. For example, in a Solid.js template it looks like this with a dynamic data prop:

function MySolidComponent() {
  const [data, setData] = createSignal({align: 'right'})

  return <blaze-component
    tmpl="loginButtons"
    data={data()}
  />
}

In this Solid.js example, we’re sending an actual object to the .data property of the <blaze-component> element, and this updated the underlying Blaze view.

And similar in Vue, Svelte, etc, with reactive bindings in their template systems.

The future?

Up next, we can wrap the <blaze-component> element to create elements such as <login-buttons> to replace direct usage of the Blaze {{> loginButtons}} component

We can later deprecate and remove Blaze from the default setup, and remove <blaze-component>, leaving only higher level components such as <login-buttons> that are no longer implemented with Blaze under the hood.

Anyone who is already using new elements such as <login-buttons> will simply have nothing to do after Blaze is removed, as they will already be using the standard HTML/DOM interface.

And! The best part!:

Type Definitions

The definition of the <blaze-component> element (and any other elements we build with it such as <login-buttons>) have type definitions for type checking, autocompletion, and intellisense in all modern frameworks that supports types in their template systems.

Custom Elements that I write are currently type checked in Solid, React, Preact, Vue, Svelte, Ember.js, and partially in Angular (a limitation of Angular they will hopefully fix soon).

Any other JSX framework that uses TSX is easy to add type definitions for too.

This means, write once, use in any framework, with a complete IDE experience. No need to make wrappers for all frameworks.

A full stack meta framework like Meteor, with its own set of official UI elements, can support all frameworks with a single set of element definitions, while people can enjoy bringing their favorite component system. Win win.

Even more future?

Meteor could have new UI elements for things such as user cards, etc. And they’d work no matter what component system the app author prefers.

If Meteor had its own set of general purpose UI elements (buttons, dropdowns, toggle switches, tabs, etc) I think it would be nice.

I believe that people here want an all-in-one solution (with any part of it replaceable by those who want to use other libraries).

4 Likes

This sounds awesome. Do you plan to open source <blaze-component>?

This sounds really cool and a good way bring back the prior convenience of Meteor. When I first started with it in 2017 my boss at the time was quite fond of the fact you could seemingly scaffold half an app so quickly, but since then we adopted Vue so we’ve been missing out on a bit of that :frowning: (and I can’t say my own attempt at a replacement login page in Vue is honestly that good…)

2 Likes

Yeah, I will publish it soon. I already published the website that is using it so technically you can view it here:

and

Just right-click on the “sign-in” at the top right, and look in the element inspector to see <blaze-component> (or use find in the element inspector).

That definition is loaded from here:

The clients are plain modules, no build (except that I’m transforming files from TS to JS using tsc instead of Meteor), and that’s served straight out of the Meteor public/ folder where all the client source code lives.

I’ll write a thread here in the forum on how I connected a separate client (docs.lume.io) to the backend (which is on lume.io).

Note that https://lume.io/elements/BlazeComponent.js currently is limited to my domains with CORS headers, but you can copy/paste the file into your public/ folder, and then you’d need to make an importmap to resolve meteor/* imports as well as @lume/element for the element definition.

Example of that:

  • The html payload has <script src="/importmap.js"> to load the import map
  • see https://lume.io/importmap.js which simply writes a <script type="importmap"> to specify where imports like import ... from '@lume/element' or import ... from 'meteor/accounts-base' will import from.
  • Note at the bottom of the importmap entries like "meteor/meteor": "/meteor-packages.js",. Meteor API s global, and /meteor-packages.js simply re-exports them to expose them via modules on the client.
  • see https://lume.io/meteor-packages.js which has simple definitions like export const Tracker = window.Package.tracker.Tracker;

Note, you can generate importmaps in the JSPM Generator web UI. Those jsdelivr entries I simply copy/pasted from there.

Once you’ve done like the above, you should be able to use the <blaze-component> element by running import('/path/to/BlazeComponent.js') (or eval("import('/path/to/BlazeComponent.js')") if your client is built by Meteor to prevent Meteor from compiling the import expression, or alternatively install dependencies in the project and include it with your build. Then you can use <blaze-component> anywhere whether HTML, React, Vue, etc.

You could also re-write the element not using Lume or other non-Meteor dependencies, with your own way of observing attribute/prop changes (@lume/element makes it so that changes to attributes/props automatically update the instance, for example changing the value of data="..." will automatically send new values to the Blaze component, but you could also do this with plain static observedAttributes and making your JS properties be getters/setters to react to changes).

I’ll publish it soon though, that will make it easier.

1 Like

Oh no, I went to try it out on my phone (end of the day) and it looks like Safari doesn’t support extending from built in elements eg HTMLAnchorElement :frowning:

Apparently iOS hasn’t opened up to Chromium yet or Chrome still uses Blink for now, not 100% sure.

Looks like you can extend from a generic HTMLElement OK though

Yeah, HTMLElement is the only base class we can extend from cross-browser without a polyfill, so people who don’t use a polyfill (that’s ideal) wrap the elements rather than extend from them. In vanilla Custom Element code, I mean something this:

cont html = String.raw

class CoolButton extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) this.attachShadow({mode: 'open'})

    this.shadowRoot.innerHTML = html`
      <button foo="bar">Click me.</button>
      <style>/*... style the cool button ...*/</style>
    `

    const btn = this.shadowRoot.children[0]

    //... logic for the cool-button ...
  }
}

Just embrace the common parts and be happy :smiley:. Custom Elements are making lots of progress in other ways and this specific part of them doesn’t prevent all of the utility they have (for example libs like Shoelace, Lume 3D elements, etc).

1 Like

Oh, I meant to say I couldn’t use your production example on iOS. I tried looking through the source code to try and figure out why and I thought I saw HTMLAnchorElement extended somewhere.

When I tap on “sign in”, nothing happens.

(Sorry for the confusion! I could be misdiagnosing the issue)

(Also to clarify I’m not trying to see the dev tools in this case, I was just trying to click through and see the interaction)