Wednesday, December 4, 2013

Posting JSON Payload with Polymer-Ajax (Dart)


I was somewhat bummed to discover that Polymer's <polymer-ajax> does not support JSON HTTP request bodies—at least in the Dart version. But after a bit of digging, it seems that there is a <polymer-xhr> that seems to support arbitrary body data (in both Dart and JavaScript). So tonight, I explore this element in the hopes that I can use it to get the higher-level <polymer-ajax> to also support arbitrary HTTP payload content.

So I import the <polymer-xhr> tag definition:
    <!-- Load component(s) -->
    <link rel="import" href="/packages/change_history/change-sink.html">
    <link rel="import" href="/packages/change_history/store-changes.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">
    <link rel="import" href="/packages/polymer_elements/polymer_ajax/polymer_xhr.html">
Then, in the body of the page, I add <polymer-xhr> as a child to the <store-changes> element that I am trying to get working over Ajax:
    <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>
With that, I am ready to make my changes to the class that is backing <store-changes>. Instead of setting <polymer-ajax> parameters and then invoking go(), I just need to invoke request() on <polymer-xhr> with the necessary options. And, since this is a low-level call, there are a lot of options:
@CustomTag('store-changes')
class StoreChangesElement extends PolymerElement {
  StoreChangesElement.created(): super.created();
  // ...
  get store => children.
    firstWhere((el) => el.localName == 'polymer-xhr');
  storeChange(e) {
    // Convert the change event into an "update" record, then...
    store.
      request({
        'url': 'http://localhost:31337/widgets',
        'method': 'POST',
        'callback': (__res, __xhr){},
        'headers': {},
        'body': JSON.encode(update)
      });
    // ...
  }
}
And that turns out to just work™. When I make changes to the the contained <div contenteditable> element, change events propagate to the custom Polymer, <store-changes> which sends JSON encoded HTTP requests to the server:
{"id":"change-history","current":"\n          <h1>
Change #1</h1>
\n        ","history":["\n          <h1>
Change #1</h1>
\n        "]}
And the server is happy to respond with:
HTTP/1.1 201 Created
server: Dart/1.0 (dart:io)
access-control-allow-origin: *
access-control-allow-headers: Content-Type, X-Requested-With
access-control-allow-methods: GET,DELETE,PUT
content-type: application/json; charset=utf-8
transfer-encoding: chunked
content-encoding: gzip
Yay!

So the question then becomes, can I get <polymer-ajax> to support arbitrary HTTP bodies? To answer that question, I fork the polymer elements (Dart), and clone it locally. Then I point my application to this local copy of my fork by making the necessary change in pubspec.yaml:
name: change_history
dependencies:
  polymer: any
  polymer_elements:
    path: /home/chris/repos/polymer_elements
A quick pub getIn my new copy of polymer_elements, I add an xhrArgs public attribute to <polymer-ajax> (mimicking the property of the same name in the JavaScript version of the element):
@CustomTag('polymer-ajax')
class PolymerAjax extends PolymerElement {
  // ...
  @published
  Map xhrArgs;
  // ...
  go() {
    var args = xhrArgs != null ? xhrArgs : {};
    // ...
  }
}
If present, this Map will serve as the default values for the arguments sent to the underlying <polymer-xhr>. I already know that, if there is body key in that xhrArgs then I can POST whatever I want via <polymer-xhr>, so I supply just that in my <store-changes> element:
@CustomTag('store-changes')
class StoreChangesElement extends PolymerElement {
  StoreChangesElement.created(): super.created();
  // ...
  get store => children.
    firstWhere((el) => el.localName == 'polymer-ajax');
  storeChange(e) {
    // Convert the change event into an "update" record, then...
    store
      ..method = 'POST'
      ..xhrArgs = {'body': JSON.encode(update)}
      ..go();
    // ..
  }
}
After switching back to the higher level <polymer-ajax> in my web page:
<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>
It works! When I make changes in the wrapped <div contenteditable>, my <store-changes> Polymer sees the change and saves a history record over XHR. Only this time, it does it with <polymer-ajax> and mercifully fewer arguments.

There are a few other benefits to be reaped in <polymer-ajax>. I will likely try to reap them tomorrow.


Day #955

No comments:

Post a Comment