Friday, December 6, 2013

Strategies for Polymer


After a pleasurable detour in Polymer.dart plumbing, I am ready to get on with original intent with this bit of Dart code: writing my custom <store-changes> element such that it will work with either <polymer-localstorage> or polymer-ajax>. I am not doing this because I think it is a good idea. Rather I am just curious what my code would look like if it can do either.

Currently, I am using the tags as follows:
    <store-changes>
      <!-- <polymer-localstorage name="store-changes" value="{{value}}"></polymer-localstorage> -->
      <polymer-ajax url="http://localhost:31337/widgets/change-history"
                    handleAs="json">
      </polymer-ajax>
      <!-- <polymer-xhr></polymer-xhr> -->
      <!-- changes will originate from elements here ... -->
    </store-changes>
And, in the Dart class that backs the <store-changes> custom element, I am similarly commenting out any code that works against localstorage:
@CustomTag('store-changes')
class StoreChangesElement extends PolymerElement {
  StoreChangesElement.created(): super.created();
  // ...
  void _fetchCurrent() {
    // if (store.value != null) record = store.value;

    var subscription;
    subscription = on['polymerresponse'].listen((e){
      // handle http response here....
    });
  }
  // ...
}
I would like either tag, if present, to work. The easiest way to accomplish that is via a common wrapper interface. Both the Ajax and localStorage data store strategies need to be able to fetch records and save records, given a Polymer element:
abstract class ChangeStore {
  PolymerElement el;
  ChangeStore(this.el);

  Future<Map> fetch();
  void save(Map record);
}
The Future return type on fetch() is somewhat premature, but I have already implemented it directly in <store-changes> and know perfectly well that it is asynchronous. So maybe not that premature.

Next up, I define my Ajax strategy as extending this abstract baseclass:
class AjaxStore extends ChangeStore {
  AjaxStore(el): super(el);
}
The fetch() method is the same code as yesterday, but with a Completer to give me the necessary Future:
class AjaxStore extends ChangeStore {
  Future<Map> fetch() {
    var subscription,
      completer = new Completer();

    subscription = el.on['polymerresponse'].listen((e){
      var res = e.detail['response'],
          data = (res != null && !res.isEmpty) ? res : null;
      completer.complete(data);
      subscription.cancel();
    });
    return completer.future;
  }
}
Since I do not care if the save is successful (so what if a little undo history is lost?), the save() method is particularly easy to implement:
class AjaxStore extends ChangeStore {
  // ...
  void save(rec) {
    el
      ..method = 'POST'
      ..xhrArgs = {'body': JSON.encode(rec)}
      ..go();
  }
}
With that, I have my Ajax strategy ready to roll. I place each strategy present inside a list of stores:
@CustomTag('store-changes')
class StoreChangesElement extends PolymerElement {
  // ...
  List<ChangeStore> stores = [];
  StoreChangesElement.created(): super.created();
  enteredView() {
    super.enteredView();
    addEventListener('change', storeChange);
    scheduleMicrotask(_attachStores);
    // ...
  }
  // ...
}
I keep bumping into the need to schedule a “microtask” as I have done here. Without it, Polymer has not had a chance to do its thing, leaving the code thinking that things like <polymer-ajax> and <polymer-localstorage> are unknown, basic HTML elements.

Attaching the stores is where I detect whether the <polymer-ajax> tag is present and, if so, adding the corresponding strategy to the list:
@CustomTag('store-changes')
class StoreChangesElement extends PolymerElement {
  List stores = [];
  StoreChangesElement.created(): super.created();
  // ...
  void _attachStores() {
    var polymer = children.firstWhere(
        (el) => el.localName == 'polymer-ajax'
      );
    if (polymer != null) stores.add(new AjaxStore(polymer));
  }
  // ...
}
Placing the storage strategies in a list makes them iterable. In other words, it is easy to call fetch() and save() on any strategies present (if any):
@CustomTag('store-changes')
class StoreChangesElement extends PolymerElement {
  Map record = {'history': []};
  List stores = [];
  // ...
  void _fetchCurrent() {
    stores.forEach((store) {
      store.fetch().then((_r) => record = _r);
    });
  }

  storeChange(e) {
    // build record from a change event, then ....
    stores.forEach((store)=> store.save(record));
  }
}
And that works.

After a quick and dirty implementing of the localStorage strategy, I am able to swap out <polymer-ajax> for <polymer-localstorage> in the web page and not miss a beat::
    <store-changes>
      <polymer-localstorage name="store-changes" value="{{value}}"></polymer-localstorage>
      <!-- <polymer-ajax url="http://localhost:31337/widgets/change-history" -->
      <!--               handleAs="json"> -->
      <!-- </polymer-ajax> -->
      <!-- changes will originate from elements here ... -->
    </store-changes>
And that works as well. In fact, since the strategies are iterable, I can have both <polymer-*> tags present and everything works just fine.

It took a few days of groundwork, but the actual implementation of this multi-strategy approach worked out quite nicely. I don't know that it rises to the level of an actual “pattern” for Patterns in Polymer. Maybe it is a basis pattern or something like that (which is, I suppose a pattern in the true sense of the term). I think I will hold off on this one before it makes an appearance in the book. For now, I am content to have a clean implementation.


Day #957

No comments:

Post a Comment