By TEN BITCOMB

A Simple Way To Integrate Web Components With Your Ember App

Web components, or custom elements, can integrate with an Ember app seamlessly, but there's one thing you need to do to make it happen.

I love web components! The community enthusiasm around these new standards have been lackluster, but I'm excited about their adoption. Having framework-like primitives come with browsers out of the box is convenient and allows for more interoperability of components between frameworks.

You can already use web components in most modern browsers, and possibly more if you use the polyfill.

Web Components in Ember

Integrating web components into an Ember app is relatively straight forward. It can be as simple as importing the code and using the custom element tag within your templates, or you might find yourself writing Ember components to provide more customizable integration.

Let's say you have a custom element that you want to make available in your app. All you need to do is define a tag name inside an initializer:

// app/initializers/custom-elements.js
import MyCustomElement from '../../lib/web-components/my-custom-element.js';

export function initialize(application) {
  // remember to use kabob-case!
  window.customElements.define('my-custom-element', MyCustomElement);
}

export default {
  initialize
};

Now your component/element will be available to use in your application templates like so:

<my-custom-element></my-custom-element>

It works just like any other element, but executes your own behavior on "connection" with lifecycle hooks similar to that in Ember components.

One of the limitations of web components is that attributes defined on them can only be strings, as is true with any other element. If your component needs access to something internal to your Ember app, it may make sense to define an Ember component that wraps your web component.

import Component from '@ember/component';

export default class MyCustomElementComponent extends Component {
  tagName = 'my-custom-element';

  didInsertElement(){
    this.didUpdateAttrs();
  }

  didUpdateAttrs(){
    this.element.someAttribute = this.someAttribute;
  }
}

Okay, but what if you want your web component to be inserted into the DOM dynamically(i.e. outside of Ember's template precompilation)? In that case, you can't use an Ember component as a wrapping interface. How can your component still be Ember-aware?

Let's say you have a shoppingCart service that you want your web component to access. The first thing you might try is to treat your web component class like you would an Ember object:

import { inject as service } from '@ember/service';

class MyCustomElement extends HTMLElement {

  @service shoppingCart;

  constructor() {
    super();
  }

  get items(){
    return this.shoppingCart.items;
  }
}

export default MyCustomElement;

Uh oh... Uncaught Error: Assertion Failed: Attempting to lookup an injected property on an object without a container, ensure that the object was instantiated via a container.

Bah, this means that web components suck, right?

Maybe they would if there weren't a simple fix to allow web components and other JavaScript objects to use Ember features!

Let's make sure our web components have access to the application container:

// app/initializers/custom-elements.js
import MyCustomElement from '../../lib/web-components/my-custom-element.js';

function defineCustomElement(tagName, element, application){
  Object.defineProperty(element.prototype, 'container', {
    get(){
      return application.__container__;
    }
  });
  window.customElements.define(tagName, element);
}

export function initialize(application) {
  defineCustomElement('my-custom-element', MyCustomElement, application);
}

export default {
  initialize
};

Now your web components will support injections as well as other Ember features.