Monday, December 9, 2013

Binding to Polymer Siblings


Thanks to the inestimable Erik Grimes, I have a nice suggestion on how I should have handled last night's attempt to link my custom <store-changes> Polymer with a “private” child element. Primarily for reason of separation of concerns, I have a separate element whose responsibility is to wait for <store-changes> to load its data from a persistent store and update an editable child element with the most recent change.

I want my <store-changes> Polymer to dynamically add this “on load” child element as follows:
    <store-changes>
      <store-changes-load></store-changes-load>
      <change-sink>
        <div contenteditable>
          <!-- actual changes occur here -->
        </div>
      </change-sink>
    </store-changes>
The <change-sink> Polymer normalizes change events from the content-editable <div> for use by <store-changes>. In turn, I want the dynamically added <store-changes-load> to populate the same content-editable <div> when the page first loads.

I got a solution last night, but after trying to write it up for Patterns in Polymer, I realized that it was not a good solution—at least not good enough. This is where Erik's pointers help. Instead of trying to dynamically create a Polymer inside another Polymer's enteredView() or ready() lifecycle method, I can just create it in the template.

So the element definition for <store-changes> becomes:
<polymer-element name="store-changes">
  <template>
    <store-changes-load></store-changes-load>
    <content></content>
  </template>
  <script type="application/dart" src="store_changes.dart"></script>
</polymer-element>
How easy is that?

But as easy as that is, the question of how this <store-changes-load> gets access to the content-editable that is distributed in the <content> remains just as difficult. I finally realize that the parent node of <store-changes-load> is the shadow root of <store-changes>. This gives me access to siblings with a bit of effort:
    editable = parentNode.
      query('content').
      getDistributedNodes().
      where((n) => n.runtimeType != Text).
      firstWhere((n) => n.query('[contenteditable]') != null).
      query('[contenteditable]');
I previously figured out how to navigate some of the ugliness of content and distributed nodes when I was testing. It feels awkward having to root through nodes like that, but it works.

The other aspect of setting the content based on the initial load value is actually getting the initial load value. This is easier. In <store-changes>, I do the load-from-persistent-polymer dance and fire events on the <store-changes-load> child elements:
@CustomTag('store-changes')
class StoreChangesElement extends PolymerElement {
  Map record = {'history': []};
  List stores = [];
  // ...
  StoreChangesElement.created(): super.created();
  // ...
  void _fetchCurrent() {
    stores.forEach((store) {
      store.fetch().then((_r) {
        record = _r;
        shadowRoot.queryAll('store-changes-load').
          forEach((el) {
            fire('store-changes-load', detail: record, toNode: el);
          });
      });
    });
  }
}
I can then listen in <store-changes-load> for that message to get passed:
@CustomTag('store-changes-load')
class StoreChangesLoadElement extends PolymerElement {
  Element editable;
  StoreChangesLoadElement.created(): super.created();
  ready() {
    super.ready();
    _findElements();
    scheduleMicrotask(_listenForLoad);
  }

  _listenForLoad() {
    addEventListener('store-changes-load', (e){
      var current = e.detail['current'];
      editable.innerHtml = current;
    });
  }
  // ...
}
I think I need to practice this message passing a bit more before I can claim to really have a handle on it. This seems an OK start, but still awkward. Perhaps I can practice with the JavaScript version of this code tomorrow.


Day #960

No comments:

Post a Comment