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.

Conversation
  • Filippos Vasilakis says:

    Interesting! In those cases personally I have a common UIService that basically holds the state for distant components that depend each other.

  • Shawn Anderson Shawn Anderson says:

    Hi Filippos,

    I definitely toyed with the idea of having a service manage this. In the end, I landed here with a note to “refactor to a service if we use this more”. Does your service act as an event bus for specific DOM changes that anyone can bind to?

  • sheriffderek says:

    Is there a place we can see the results?

    • Shawn Anderson Shawn Anderson says:

      Hey Derek, thanks for the response.

      Unfortunately no, the application is not public facing. Would it be helpful if i put together a small sample app to demonstrate the technique?

      • Lucas says:

        I’m not Derek but I think it would be helpful.

        I think there is typos in the article – `pollForItemsRendered` but function seems to be called `pollForAnswersRendered`

        The second template with `{{#each matchPairs as |mp|}}` is it for `connactable-item`?

        • Shawn Anderson Shawn Anderson says:

          Thanks for the feedback Lucas!
          I have updated the example code and will put together a sample app soon.

  • Comments are closed.