Tuesday, October 8, 2013

Dynamically Positioning the Shadow DOM


Bother and confound it. Sometimes I think I ought to leave well enough alone. But then I think better of it, because where is the fun in well enough?

Tonight's fun is provided when I complete the sample Polymer.dart page in the ICE Code Editor project. I already have two instances of the <ice-code-editor> element on the page, with two different src attributes, making two slightly different embedded editors:



I had been experimenting with dynamically adding custom elements to the DOM, which appeared to work just fine. I say “appeared” because I was setting a non-existent src attribute in the dynamically created <ice-code-editor> element:
main() {
  var later = createElement('ice-code-editor');
  var iceElement = later.xtag;
  iceElement.src = 'embed_c.html';

  document.body.append(later);
}
That's no fun, right? So I created the embed_c.html file, adding Three.js contents that described a spinny, shiny, red donut. And when I reload the page:



At first I think that the various ICE elements are confusing each other so that my dynamically created element is drawing its preview on the same <canvas> as the first one. A quick bit of element inspection proves that theory incorrect. The previews are separate, but the dynamically created instance is placed in the wrong position.

The cause stems from the underlying Editor class that is responsible for combining the code editor and preview layer. When it was first built, it assumed that the container element would already be part of the DOM. For the full-screen IDE version of ICE, this is a perfectly valid assumption. Even when the custom elements are declared in the web page, this is a valid assumption. But when I use createElement in my dynamic code above, it is not attached to the DOM (until the eventual document.body.append()). The Editor constructor is invoked as soon as the <ice-code-editor> element is created. So when the constructor tries to apply styles (which include positioning), it fails:
  Editor(this._el, {preview_el}) {
    // ...
    // Fails to position b/c container is not in DOM 
    this._applyStyles();
  }
As I said, bother.

I think the responsibility for applying position styles in this case has to fall on the IceCodeEditorElement Polymer class. It is the Editor's responsibility to coordinate code editor and preview layer, not to account for weird insertion use-cases.

Happily, Polymer support the inserted() method, which is invoked when the Polymer element is… inserted into the DOM. In IceCodeEditorElement, which is a subclass of PolymerElement, I need only override inserted() to account for this:
@CustomTag('ice-code-editor')
class IceCodeEditorElement extends PolymerElement with ObservableMixin {
  @published String src;
  @published int line_number = 0;
  ICE.Editor editor;

  void created() {
    super.created();
    // Create the editor instance here...
  }

  void inserted() {
    super.inserted();
    editor.applyStyles();
  }
}
I do not know if super.inserted() strictly needs to be called when overriding it like this, but this seems a reasonable future-proofing to use—other Polymer life-cycle methods do need it.

Since I do need to invoke the applyStyles() method from outside of the Editor class now, I have to make it public. Happily, this is Dart, so I only need remove the leading underscore:
class Editor {
  // ...
  Editor(this._el, {preview_el}) {
    // ...
    this.applyStyles();
  }
  applyStyles() { /* ... */ }
}
And that does the trick! All of my instances of the <ice-code-editor> element are displaying their contents in the proper code editor / preview combo:



Aside from exposing one previously private method, no changes were required in the underlying code and a one-liner (not including the super call) was all was needed in the Polymer element code. That, to me, seems the very definition of riding a code library's rails. Which makes this a fine place to call it a night.


Day #898

No comments:

Post a Comment