Saturday, November 29, 2014

Hacking Native Form Inputs with Polymer


Enough fun, let's code JavaScript.

I kid, I kid. I love JavaScript and JavaScript has been very good to me. I might prefer Dart, but I'll always have a special place in my heart for JavaScript. Today, I get to experience some of that specialness by adapting a Polymer.dart approach into the JavaScript flavor of Polymer.

I am rethinking the approach that I suggested in Patterns in Polymer for Polymer elements that behave like normal HTML <form> input elements. The new approach is very much a hack—it injects hidden <input> elements into the containing document, breaking Polymer element encapsulation. But, since Polymer does not (yet) support directly extending native form elements, I am warming to this hack as the best approach to take.

So tonight, I give it a try in JavaScript. I had originally hoped to get this running under test, but a quick review of the documentation for extending custom elements in Polymer scared me off this idea. Mostly, I worry that inheritance in JavaScript Polymer is not as elegant as the Dart counterpart. Working in the "superclass" shadow DOM seems particularly worrisome since the point of this element will be to create hidden <input> elements in the containing document's light DOM.

Still, I can place myself well for testing. So I start by creating a completely new repository for <a-form-input>. In that repository, I make a test directory that will hold a test element / fixture for my tests. I define test/x-double.html as:
<link rel="import" href="../a-form-input.html">
<polymer-element name="x-double" extends="a-form-input" attributes="in">
  <template></template>
  <script>
    Polymer("x-double", {
      attached: function(){
        this.super();
        this.inChanged();
      },
      inChanged: function(){
        this.value = parseInt(this.in) * 2;
      }
    });
  </script>
</polymer-element>
I have previously used this fixture in other Polymer tests, so the bulk of this is copied and pasted. What is new is the extends attribute, indicating that <x-double> will extend the soon-to-be-created <a-form-input>—in other words, <x-double> is a form input element. The attributes listed are also slightly different. In previous fixtures, I have needed an out attribute to reflect the doubling of the input value, but in this case, I will publish on the value attribute, which will come from <a-form-input>. The last difference is the need to call the attached() method in <a-form-input> from <x-double> via this.super()—it is not real inheritance, but it should work.

Next, I create the element that I want define in a-form-input.html:
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="a-form-input" attributes="name value">
  <template></template>
  <script src="a_form_input.js"></script>
</polymer-element>
It is a personal preference to keep my JavaScript separate from my HTML. I may wind up combining this when I publish the package. I am unsure of the need to include the <template> tag. For now I do include it, but experimentation will be required.

I define the actual backing class for <a-form-input> in a_form_input.js:
Polymer('a-form-input', {
  publish: {
    name: {value: 0, reflect: true},
    value: {value: 0, reflect: true},

    attached: function(){
      this.lightInput = document.createElement('input');
      this.lightInput.type = 'hidden';
      this.lightInput.name = this.getAttribute('name');

      this.parentElement.appendChild(this.lightInput);
    },

    valueChanged: function(){
      this.lightInput.value = this.value;
    }
  }
});
This is adapted from last night's Dart approach. The name and value attributes need to be published and reflected (i.e. they reflect internal changes in the attributes) so that they behave like normal form input elements. When a-form-input is attached to the DOM, it injects a hidden input into the parent element. It names the hidden input with with the same value as the element itself. Lastly, I establish an attribute watcher on value so that the hidden input in the parent element will always synchronize to the value of the current element.

I can then smoke test this in a page served over a Python simple HTTP server:
<!doctype html>
<html>
  <head>
    <title>Smoke Test</title>

    <!-- 1. Load Polymer before any code that touches the DOM. -->
    <script src="bower_components/webcomponentsjs/webcomponents.js"></script>
    <!-- 2. Load component(s) -->
    <link rel="import" href="test/x-double.html">
  </head>
  <body>
    <div class="container">
      <h1>Smoke Test: a-form-input</h1>

      <form>
        <x-double name="doubled" in=5></x-double>
      </form>
    </div>
  </body>
</html>
Which does the trick:



So it seems that I need not have worried about the shadow DOM. At least in this case. I can use my is-a-form-input <x-double> element as a form input. If I give it a typical input name attribute of something like "doubled", then a corresponding hidden input is added to the containing <form> with the same name attribute. In other words, when I submit this form, it will contain a doubled form value.

There is still some experimentation warranted with all of this, but the overall approach still seems promising. Up tomorrow: tests.



Day #9

1 comment:

  1. You had me on, "Enough fun, let's code JavaScript" :-P

    ReplyDelete