Skip to content

Instantly share code, notes, and snippets.

@gossi
Last active September 19, 2018 23:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gossi/3555408a431c3355825d091acdd9d900 to your computer and use it in GitHub Desktop.
Save gossi/3555408a431c3355825d091acdd9d900 to your computer and use it in GitHub Desktop.
Ember Components Element API

Ember Components Element API

Background

I don't know what happens to be there first: Ember Components or Web Components. Actually doesn't matter because markup wise they are pretty identical. You can create a custom element to be used in your HTML markup, next to existing HTML elements. The components itself have to follow a specific naming scheme to distinguish them from regular HTML elements. They should either be written in <kebap-case> or <StudlyCase>. There is also a special syntax for attributes and arguments of ember and glimmer components (arguments are passed with the @ sigil). These are the visually distinction from components to regular HTML elements. From just watching at the markup it is possible to identify regular HTML elements and components, though it is not possible to say whether it is a webcomponent, an ember component or a glimmer component, e.g. <video-player id='abc'> could be either of that.

Over the past years, the DDAU pattern has been established as a data flow to pass values in as arguments and install listeners to be notified on changes. That's almost similar to what we have for regular html elements, except we can not command the component to do something from the outside, our component is passive. Sure, we could do something like changing a property to make it react on that or to register kind of like a public API, which is kind of the ember way but it is not just ugly looking but also very black magic. An imperative API is missing from ember and glimmer components to turn them into active components. Let's look at an example and compare it with a html element.

Imagine we do have a video and want to play it. We could use the <video src="path/to/video.mp4"> element, load it and later we can query the element and call element.play() to start playing the video. We can also check if we canPlayType() and provide a nice error message upfront. Given our model is a little more complex, not just one single source but our video may consist of multiple clips (with start and end times) and each with multiple sources. We sure build a component which handles this complexity to play it as a sequential stream. Our component wouldn't be that much different from the video element, we want the same API as for our component as for the video element. Our markup will look like this: <VideoPlayer @video={{this.model}} id="player" /> (or <video-player video={{this.model}} />), wait for it to load and call document.getElementById('player').play() to start the playback. This, unfortunately, is not officially supported by ember components - although webcomponents do. They look pretty much identical in the markup, so we can actually expect that behavior.

Implementation Ideas

Ideas to name this API are: elementApi, publicInterface, publicApi, domApi and there are some different Ideas to implement it. A proof of concept for the shown ideas (1) and (2) is done by Aad Versteden (@madnificent) in the repo https://github.com/madnificent/ember-public-dom-api (you can have a look)

1: A property to be exposed as publicInterface

This is a property inside the component:

import Component from '@ember/component';

export default Component.extend({
  publicInterface: {
    myProperty: computed.alias('someProp'),
    myMethod: () => {
      return this.myMethod();
    }
  }
});

2: Using Decorators on respective methods/properties

This would be necessary when working with native classes anyhow:

import Component from '@ember/component';
import publicInterface from '...';

export default class MyComponent extends Component {

  @publicInterface
  myProp;

  @publicInterface
  myFunction() {
  }
  
  // with renaming, option a) pass by string
  @publicInterface('myMethod')
  myFunction2() {
  }
  
  // with renaming, option b) pass options hash
  @publicInterface({name: 'myReallyCoolMethod')
  myFunction3() {
  }
}

3: Using private/public keywords for visibility

Using typescript here:

import Component from '@ember/component';

export default class MyComponent extends Component {

  myProp;
  
  private myPrivateProp;
  
  myPublicMethod() {
  }
  
  private thisIsHidden() {
  }
}

Afterthoughts

As much as I love to see this idea come to light, this has to be done quite right. There is much more to tackle on this, more exploration in that field that for sure has to follow. This showcases the basic idea, not more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment