Skip to content

Instantly share code, notes, and snippets.

@machty
Last active August 29, 2015 13:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save machty/b468f59076dc4241155a to your computer and use it in GitHub Desktop.
Save machty/b468f59076dc4241155a to your computer and use it in GitHub Desktop.
web-components-y components in Ember.Component

Media Player component

<!-- components/media-player.handlebars
     Note the use of template element here to distinguish this Shadow-DOM-y
     API from classic Ember.Component API. -->
<template>
  <content select="previous-button">
    <!-- the context here is MediaPlayerComponent, which is also
         what it'll be in any consumer blocks that override the
         previous-button placeholder. In other cases, content
         placeholders (and their consumer override blocks) will
         have different contexts than their parent components
         (think loops) -->
    <fancy-button on-click="{{prevTrack}}">Previous</fancy-button>
  </content>

  <content select="next-button">
    <fancy-button on-click="{{nextTrack}}">Next</fancy-button>
  </content>

  <content select="play-button">
    <!-- context is still a MediaPlayerComponent, and togglePlay
         refers to one of its actions -->
    <fancy-button on-click="{{togglePlay}}">
      <!-- context is still a MediaPlayerComponent -->
      {{#if isPlaying}}
        Pause
      {{else}}
        Play
      {{/if}}
    </fancy-button>
  </content>

  <content select="track-list" 
           tracks="{{tracks}}" 
           selectedTrack="{{selectedTrack}}"
           on-select-track="{{selectTrack}}">

    <!-- context is NOT a MediaPlayerComponent. It's an object with
         only the "hash" properties passed to this content tag
         (other than 'select'), so { tracks: tracks, selectedTrack: selectedTrack }
    {{#each track in tracks}}
      <!-- Note: this is the first use of nested content -->
      <content select="tracklist-item" 
               isSelected="{{isEqual track selectedTrack}}"
               track="{{track}}"
               selectTrack="{{selectTrack}}">
        <!-- context is a hash of {isSelected, track, selectTrack} -->

        <!-- Not that this is a good naming convention, but note how
             select="tracklist-item" is distinct from the insertion
             of the tracklist-item element/component below; the
             former is specifying insertion points while providing a
             default, the latter is actually inserting a
             component/element named tracklist-item -->
        <tracklist-item isSelected="{{isSelected}}" 
                        track="{{track}}" 
                        on-select="{{selectTrack}}">
          <!-- context is a TracklistItemComponent, with {isSelected, track} -->
          Title: {{track.title}}
          <button {{action on-select}}>Play Me</button>
        </tracklist-item>
      </content>
    {{/each}}
  </content>
</template>

Then in your JS

export default Ember.Component.extend({
  // ... whatever properties
  actions: {
    prevTrack: function() { /* ... */},
    nextTrack: function() { /* ... */},
    toggle: function() { /* ... */}
  }
});

Media Player consumption

Render default media player:

<media-player tracks="{{songs}}"></media-player>

Remove the prev button:

<media-player tracks="{{songs}}">
  <previous-button>
    <!-- this prevents the fancy-button in the default
         "previous-button" content from rendering, effectively
         removing the prev button -->
  </previous-button>
</media-player>

Replace the prev button with a normal html button:

<media-player tracks="{{songs}}">
  <previous-button mp>
    <!-- the context here is the outer controller context. The
         `mp` boolean attribute is treated as a block param
         to access the context that is passed into into this overriding 
         template block, which is a MediaPlayerComponent instance. This 
         preserves the extremely convenient access to the outer scope, just
         like classic Ember.Component API, while empowering the 
         overriding template to choose the name of data being
         passed into it so that no scope clobbering takes place. 
         Block params, baby, you knew they'd make their way into 
         here somehow. -->
         
    <!-- without target=mp this would fire on the outer controller -->
    <button {{action 'prevTrack' target=mp}}>Previous</button>
  </previous-button>
</media-player>

The above certainly works, but I'd definitely like a JS-centric way of overriding the component that gets rendered into the previous-button "placeholder" (or replace all fancy-buttons with something else). We have all the primitives we need from HTMLBars' CONTENT hook to overriding container lookup on components to do our magical bidding.

Duplication?

H8rz will point out the seeming duplication in a case like:

<content select="next-button">
  <fancy-button on-click="{{nextTrack}}">Next</fancy-button>
</content>

Can "next-button" and "fancy-button" be combined in some way? Can't "next-button" just be a component that we override?

If we were doing a JS-centric way of configuring component lookups, bindings, actions, etc., (which I've spiked on in this JSBin), then we could remove this duplication, since "next-button" is really just a placeholder that we can hook into to render whatever in the world we want, i.e. a tagless fragment, an element, another component, etc. But if we desire a way to override things via named template blocks, all within a controller's (or subcomponent's) template, there are many ambiguities:

  1. Are you overriding a component's innerHTML? Or replacing the entire tag?
  2. What's the scope? Even with block params, is the provided block param the component whose template you're overriding? Or is it the parent component that it's being rendered into? Are both passed, some how?
  3. Isn't this stepping away from the Web Components parity goal?

These problems go away if we embrace the idea that named template blocks that you pass into a components simple override placeholders/fragments, rather than inner components. Within the default template block (or the overriding one that's passed in), you can insert whatever components you want, but the template block itself is just a fragment, and not an override to some component. This avoids most/all the ambiguities I can think of.

Is this really Web Components?

There are some minor differences with how this content selection works; mainly that in my examples above, the select attribute is always set to some custom element, e.g. "previous-button", but when a supplied "previous-button" is passed-in at render time, only its content (and not the entire previous-button tag) is inserted into the insertion point. If we really hate this divergence from the Web Components spec, we can change our <content> tags to <content-for> or something, with otherwise the exact same semantics, but the way I have it above allows for the flexibility of passing in named template blocks as fragments rather than requiring that they must be entire elements.

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