6 Comments

Ember.js Components with DOM Dependencies

Ember.js is a great framework for building single-page applications. Its mantra of “data down, actions up” sets a clear guideline on how to structure most of your application.

Your route gets some data and tees it up for your controller/component to render it. When something changes the UI (say entering text), you fire an action with the updated values and use it to update your data. This approach also helps with rendering speed under the hood. Your view is totally driven by your model. Perfect. Unless…

What happens when things aren’t so cut and dry? Let’s walk through a case where part of the view depends on how another part renders.

Recently, I had to build a matching question form that required drawing lines between matched elements. I wanted to float an SVG over the parent element and position the lines based on the items on each side of the match. But since Ember components don’t have access to each other, how would I know when it’s ready?

First, a couple of basic handlebars templates:


  <div class="leftCol">
    {{#each leftItems as |left|}}
      {{connectable-item item=left }}
    {{/each}}
  </div>
  <div class="rightCol">
    {{#each rightItems as |right|}}
      {{connectable-item item=right }}
    {{/each}}
  </div>
  {{magic-connection-lines matchPairs=pairs}}

  <svg>
    {{#each matchPairs as |mp|}}
      <line x1={{mp.x1}} y1={{mp.y1}} x2={{mp.x2}} y2={{mp.y2}} />
    {{/each}}
  </svg>

We let the parent component wrangle jQuery and wait for our children to be done rendering:


import Ember from 'ember';

export default Ember.Component.extend({
  isPollingForItems: false,

  didInsertElement(){
    this._super(...arguments);
    Ember.run.next(this, this.get('pollForItemsRendered'));
  },

  didUpdateAttrs() {
    // look again if our model has changed
    Ember.run.next(this, this.get('pollForItemsRendered'));
  },

  _isElementLoaded($el) {
    let isLoaded = true;

    if($el && $el.length > 0) {
      var imgs = $el.find('img');
      for(var i = 0; i < imgs.length; i++){
        if(!imgs[i].complete){
          isLoaded = false;
        }
      }
    } else {
      isLoaded = false;
    }

    return isLoaded;
  },

  pollForItemsRendered(){
    var allLoaded = true;
    this.get('items').forEach(item => {
      var $item = Ember.$(`[data-id='${item.id}']`);
      if(!this._isElementLoaded($item)) { allLoaded = false; }
    });

    if(allLoaded){
      this.set('items', Ember.merge({}, this.get('items')));
    } else {
      Ember.run.next(this, this.get('pollForItemsRendered'));
    }
  },

  leftItems: Ember.computed('items', function() {
    this.get('items').filterBy('left').map(item => {
      var $item = Ember.$(`[data-id='${item.id}']`);
      return {
        x1: $item.position().left,
        y1: $item.position().top,
        ...
      };
    });
  }),

So, we’ve managed to wire up our top-level component to:

  • Use jQuery to get the positions of previously rendered DOM elements
  • Wait for all images to load; poll if they are not
  • Restart the process if our underlying model changes

You can definitely feel that we’re going against the eloquent grain of Ember, but we still have the flexibility to make things work.