Saturday, December 7, 2013

Middleman Polymer Elements


On of the things that I do not have a handle on with Polymer is communication between elements. Consider, for instance, my <store-changes> Polymer element:
    <store-changes>
      <polymer-localstorage name="store-changes" value="{{value}}">
      </polymer-localstorage>
      <polymer-ajax url="http://localhost:31337/widgets/change-history"
                    handleAs="json">
      </polymer-ajax>
      <change-sink>
        <div contenteditable>
          <!-- actual changes occur here -->
        </div>
      </change-sink>
    </store-changes>
I am almost certainly over complicating some of this just for the sake of exploration, but...

Changes occur when updates are made in the contenteditable <div>. The <change-sink> Polymer normalizes and debounces the change events. The outermost <store-changes> Polymer listens for those normalized events, storing them in whatever data store is present (currently it supports polymer-localstorage and polymer-ajax).

All of that works pretty darn well. Contenteditable does not really expose nice change events, so the <change-sink> serves a legitimate purpose in there. And <store-changes> really does store a history of those changes in a REST-like backend or in localStorage. But one thing that is missing is reloading the page. The last change in <store-changes> should become the value in the contenteditable, but currently it just defaults back to the original value. The question is…

How should communication flow between <store-changes> and contenteditable when the page is first loaded?

Is it the contenteditable's responsibility to walk up the DOM tree to find a <store-changes>? Is it the responsibility of <store-changes> to check its descendants for a contenteditable (or other text input element)? My initial thought is that the latter is closer to the best approach, but regardless the ideal is probably somewhere in between.

So, for tonight, I am going to create a <store-changes-load> element that is a child of <store-changes>:
    <store-changes>
      <polymer-localstorage name="store-changes" value="{{value}}">
      </polymer-localstorage>
      <polymer-ajax url="http://localhost:31337/widgets/change-history"
                    handleAs="json">
      </polymer-ajax>
      <store-changes-load></store-changes-load>
      <change-sink>
        <div contenteditable>
          <!-- actual changes occur here -->
        </div>
      </change-sink>
    </store-changes>
It will be this element's responsibility to load the last change from <store-changes> and place it in the contenteditable. I do not know if this is a good idea. I am merely exploring it here as a possible solution.

I am still doing this in the Dart version of the code with Polymer.dart. I start by adding an import of the new element to the web page:
    <!-- Load component(s) -->
    <link rel="import" href="/packages/change_history/store-changes.html">
    <link rel="import" href="/packages/change_history/store-changes-load.html">
    <link rel="import" href="/packages/change_history/change-sink.html">
    <link rel="import" href="/packages/polymer_elements/polymer_localstorage/polymer_localstorage.html">
    <link rel="import" href="/packages/polymer_elements/polymer_ajax/polymer_ajax.html">

    <script type="application/dart">export 'package:polymer/init.dart';</script>
    <script src="packages/browser/dart.js"></script>
The definition need do nothing aside from ensuring that this really is a UI-less element and pointing to the code:
<polymer-element name="store-changes-load">
  <template>
    <style>
      :host {
        display: none;
      }
    </style>
  </template>
  <script type="application/dart" src="store_changes_load.dart"></script>
</polymer-element>
The backing class is then responsible for linking the two elements—<store-changes> and contenteditable—so it starts by declaring instance variables for each and scheduling the linking once Polymer is ready:
import 'package:polymer/polymer.dart';
import 'dart:html';
import 'dart:async';

@CustomTag('store-changes-load')
class StoreChangesLoadElement extends PolymerElement {
  PolymerElement store;
  Element editable;

  StoreChangesLoadElement.created(): super.created();

  enteredView() {
    super.enteredView();
    scheduleMicrotask(_linkElements);
  }
  // The linking will go here...
}
As for the actual linking, that turns out to be relatively straightfordward:
@CustomTag('store-changes-load')
class StoreChangesLoadElement extends PolymerElement {
  // ...
  _linkElements() {
    _findChangeSink();
    _findEditable();

    store.on['store-changes-load'].listen((_){
      _sync();
    });
  }

  _findChangeSink() {
    store = ownerDocument.query('store-changes');
  }

  _findEditable() {
    editable = query('[contenteditable]');
  }

  _sync(){
    editable.innerHtml = store.current;
  }
}
The _linkElements() method finds both the store and the contentenditable. Then, once the store fires an event indicating that it has loaded its data from whichever store is present, it synchronizes the two elements. Finding the elements is a simple matter of querying the right document (owner document or the current document fragment). Synchronizing is then trivial: I have the current value, I have the editable element, so I set the innerHtml value of the editable element to the current value.

And done. When I make a change, reload the page, the last value is now in the contenteditable <div>:



That seemed to work fairly well. I still need to play with it more to decide if it belongs in Patterns in Polymer, but it seems a promising approach so far!


Day #958

2 comments:

  1. Hi Chris! If you haven't seen it yet, there's an excellent article on communication & message passing between polymer elements @ http://www.polymer-project.org/articles/communication.html. You might also find some of the patterns used in the example @ https://github.com/ErikGrimes/everyday_dart/blob/master/example/showcase/client/everyday_showcase.html of interest.

    ReplyDelete
    Replies
    1. Much thanks for that pointer — I hadn't seen that article, but it looks like it'll be very helpful. Also thanks for the code sample, I think that approach will have more success that what I tried next. Dynamically generating Polymer tags inside another Polymer makes it hard, if not possible to gain access to the PolymerElement (instead of the HtmlElement) and its class methods and attributes. I'll give that a shot next.

      Much appreciated!

      Delete