Block component templates at runtime


#1

For a very particular use case, I’d like to be able to do this:

{{#runtime-markup markup=htmlString}}
  inner markup
{{/runtime-markup}}

Where htmlString is something like <div class="xyz>{{yield}}</div> resulting in output similar to

<div class="xyz>
  inner markup
</div>

The idea is that my inner content stays the same, but I can arbitrarily setup wrapping markup using a string. Thoughts?


#2

Maybe? But we’ll be delving into untraditional usages. Then again this is untraditional use case.

// runtime-markup.js
Ember.Component.extend({
  markup: null,

  markupNormalized: Ember.computed('markup', function() {
    return Ember.String.htmlSafe(this.get('markup').replace('{{yield}}', '<!--morph:yield-->'));
  }),

  yieldMorph: Ember.computed(function() {
    return this.findMorph('yield');
  }).readOnly(),

  yieldedNodes: Ember.computed(function() {
    let start = this.findMorph('start');
    let end = this.findMorph('end');
    let cursor = start.nextSibling;
    let nodes = [];

    while (cursor !== end) {
      nodes.push(cursor);

      cursor = cursor.nextSibling;
    }

    return [].concat(start, nodes, end);
  }).readOnly(),

  findMorph(name) {
    let target = `morph:${name}`;

    return (function recur(element) {
      let cursor = element.firstChild;

      while (cursor) {
        if (cursor.nodeType === document.COMMENT_NODE && cursor.nodeValue === target) {
          return cursor;
        }

        let found = recur(cursor);

        if (found) {
          return found;
        }

        cursor = cursor.nextSibling;
      }
      
      return null;
    })(this.element);
  },

  swapNodes() {
    let fragment = document.createDocumentFragment();
    let morph = this.get('yieldMorph');
    let nodes = this.get('yieldedNodes');
    let swapper = document.createTextNode('');

    let first = nodes[0];

    first.parent.insertBefore(swapper, first);

    nodes.forEach(node => {
      frament.appendChild(node);
    });

    Ember.$(morph).replaceWith(fragment);
    Ember.$(swapper).replaceWith(morph);
  },

  didInsertElement() {
    this._super(...arguments);

    this.swapNodes();
  },

  willDestroyElement() {
    this._super(...arguments);

    this.swapNodes();
  }
});
// runtime-markup.hbs
{{markupNormalized}}
<!--morph:start-->{{yield}}<!--morph:end-->

#3

Thank you so much! This is quite helpful. And yes, this is quite untraditional - I’m working with markup that is returned from the server.

It’s working, but additionally I’m hoping for a way for that outer markup to change. Here’s what I have so far working from your example:

https://ember-twiddle.com/80427e7c56cb2d66fe4125f4a18559bd?openFiles=components.runtime-markup.js

If the markup computed property is changed, it re-renders but the inner nodes are lost. I tried experimenting with swapping them back on the willRender event but no dice.

I’m still playing with it, but curious to know if you had any more thoughts on the matter. Very grateful for your insight either way, it’s helped a lot!


#4

I’ve taken an entirely different approach using a helper, and running HTMLBars at runtime.

// ember-cli-build.js
app.import('bower_components/ember/ember-template-compiler.js');
import Ember from 'ember';
let count = 0;

export default Ember.Helper.extend({
  compute(params/*, hash*/) {

    let owner = Ember.getOwner(this);
    let uniqueId = `runtime/markup-${count++}`;

    let component = Ember.Component.extend({
      tagName: '',
      layout: Ember.HTMLBars.compile(params[0]),
      willDestroy() {
        this._super(...arguments);
        Ember.run.next(this, () => {
          component = null;
        });
      }
    });

    owner.register( `component:${uniqueId}`, component );

    Ember.run.next(this, () => {
      owner.unregister( `component:${uniqueId}` );
    });

    return uniqueId;
  }
});

// invocation
{{#component (runtime-markup markupString)}}
  Content
{{/component}}

I totally understand it’s not ideal, but it’s working. My only concern is that I could potentially be called Ember.Component.extend hundreds of times over the life of the application. With the above method, would there be any potential memory concerns?