Saturday, November 30, 2013

Wrap or Extend: What is the Best Polymer Practice?


I am really digging the possibilities opened up by UI-less Polymer elements. So far I found them super easy ways to establish event sinks or transforms on the documents in which they are inserted. They also make for a quick means of normalizing DOM events for tags that lack a nice interface. The possibilities are so wide open that it is hard to get a good sense of what makes for a good practice. But, since all of this is research material for a book named Patterns in Polymer, I need to develop that sense of best practice quickly.

Tonight, I am going to create an element that wraps last night's element. Whereas last night's <change-sink> wrapped contenteditable and <input> tags in a sane change event wrapper:
<change-sink>
  <div contenteditable><!-- Initial content here... --></div>
</change-sink>
<script>
  document.querySelector('change-sink').addEventListener('change', function(e){
    console.log('Was:');
    console.log(e.detail.was);
    console.log('But now it\'s different!');
  });
</script>
Tonight's should wrap the normalized change events from <change-sink> and save them for later use. The question that I would like to answer is, should I wrap elements like <change-sink> or is it better practice to combine change normalization and storage into a single element? I do not think that I will find a good answer tonight. Best practices are rarely found in a single day. Instead, I only hope to put in the necessary groundwork to answer that question down the road.

I start by importing the still-to-be-created store-changes.html into my web page and wrapping the <change-sink> element inside of the Polymer that will be defined:
<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- 1. Load Polymer before any code that touches the DOM. -->
    <script src="scripts/polymer.min.js"></script>
    <!-- 2. Load component(s) -->
    <link rel="import" href="scripts/change-sink.html">
    <link rel="import" href="scripts/store-changes.html">
  </head>
  <body>
    <store-changes>
      <change-sink>
        <div contenteditable><!-- Initial content here... --></div>
      </change-sink>
    <store-changes>
  </body>
</html>
Next up, I create the imported change-sink.html. Again this is a UI-less Polymer, so I can jump right into defining the element's methods:
<polymer-element name="store-changes">
  <script>
    Polymer('store-changes', {
      ready: function() {
        this.addEventListener('change', this.storeChange);
      },
      storeChange: function(e) {
        var change = e.detail;
        console.log('Will store: ' + change.is);
      }
    });
  </script>
</polymer-element>
This tracer bullet has hit the mark—when I make a change, my new <store-changes> Polymer sees the normalized change from <change-sink> and is ready to store it:



From there, it is just a matter of mucking around with localStorage, first ensuring that the necessary data structure is in place:
    Polymer('store-changes', {
      // ...
      storeChange: function(e) {
        this.ensureStorage();
        // ...
      },
      ensureStorage: function() {
        if (localStorage.store_change !== undefined) return;
        localStorage.store_change = JSON.stringify({
          current: undefined,
          previous: []
        });
      }
    });
Of note here is that Polymer's addEventListener() appears to bind this to the current instance of the Polymer. That is, if I was to reference the current Polymer object, I can use this without having to explicitly bind it inside the addEventListener().

Next, I save a list of recent changes with each change event:

    Polymer('store-changes', {
      // ...
      storeChange: function(e) {
        this.ensureStorage();

        var change = e.detail,
            store = JSON.parse(localStorage.store_change);

        store.current = change;
        store.previous.unshift(change);
        store.previous.splice(10);

        localStorage.store_change = JSON.stringify(store);
      },
      // ...
    });
With that, I have myself a simple change history, local storage mechanism all triggered from simple change events. If I add a getter to the Polymer class:
    Polymer('store-changes', {
      // ...
      get: function(i) {
        return JSON.parse(localStorage.store_change).previous[i];
      },
      // ...
    });
Then I have a quick and simple means of retrieving that history from outside of the element:
el = document.querySelector('store-changes')
el.get(0)
// "<h1>Change #5</h1>"
el.get(1)
// "<h1>Change #4</h1>"
el.get(3)
// "<h1>Change #2</h1>"
That certainly looks like something that I could wrap in yet another Polymer to yield rudimentary undo/redo (maybe even with an actual UI!).

So far, I have to say that I like the separate elements like this. It feels very much like a convenient way of realizing separation of concerns in my code. That said, this could easily lead to deeply nested code and weird contextual behaviour depending on whether or not elements are parent, sibling, or child of other elements. And, since there are other ways to separate concerns, this seems a topic worth further exploration tomorrow.


Day #951


No comments:

Post a Comment