Wednesday, December 3, 2014

Is a Form Input (Extending Polymer.dart without Reading Docs)


Now for the fun. Now for Dart.

I kid (a little). One of the joys of writing Patterns in Polymer is slipping back and forth between JavaScript and Dart. If I am being honest, I prefer Dart, but both languages have their joys. More than experiencing those joys, I appreciate trying solutions for Polymer in one language and then another. If a solution is pleasant in both languages, then I feel like I have found a nice solution that transcends language—making it a true Polymer pattern.

In some ways, switch back and forth between languages feels a little like spiking solutions, then doing it for real. In real programming that is loads of fun (not to mention is leads to better solutions). Surprisingly, I have found this to be a bad idea writing the book. It is easy to mistake tinghe post-spike flow for a transcendent pattern. Of course it works when switching languages, I already pushed through the gaps in knowledge in the other language.

Instead, I have been making a concerted effort to push the solution forward when switching between languages. When I got my initial spike of <a-form-input> (it allows Polymer elements to work in native HTML forms) working in Dart, I did not try to get it working in JavaScript. Instead I tried building a base class / element in JavaScript. Now that I have that working in JavaScript, I try something a little different in Dart. Instead of just making <a-form-input> a base class in Dart, I am going to see if I can make it work with the is="a-form-input" syntax.

I start by reproducing my latest JavaScript work in Dart. That is, I create a Dart package for <a-form-input>. In a new git repository, I add a simple Dart Pub pubspec.yaml:
name: a-form-input
dependencies:
  polymer: any
Then, I copy in a_form_element.dart from my initial spike in some of the book's exploratory code into lib/a_form_input.dart. For the most part, the JavaScript version of this library looks identical to the Dart solution—aside from the code annotations:
@CustomTag('a-form-input')
class AFormInput extends PolymerElement {
  @PublishedProperty(reflect: true)
  String name;

  @PublishedProperty(reflect: true)
  String value;
  // ...
}
The same class in JavaScript is the more compact:
Polymer('a-form-input', {
  publish: {
    name: {reflect: true},
    value: {reflect: true}
  },
  // ...
});
I am going to be honest here: I love the Polymer.dart annotations. If you would have told this old dynamic language aficionado a year ago that I'd love annotations, I'd have thought you were crazy. But now I really appreciate how they distinguish meta-information in the class from actual code. These published properties are core to the functionality of <a-form-input>, but in JavaScript that information gets lost in the rest of the code:
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);
  },

  nameChanged: function(){
    this.lightInput.name = this.name;
  },

  valueChanged: function(){
    this.lightInput.value = this.value;
  }
});
Still, the task at hand tonight is not language comparing. Instead, I want to change the test element in Dart from extending the base. So. in the smoke test page for the book example, I add the is attribute:
<!DOCTYPE html>
<html lang="en">
  <head>
    <link rel="import" href="packages/form_example/elements/x-pizza.html">
    <link rel="import" href="packages/a-form-input/a-form-input.html">
    <script type="application/dart">export 'package:polymer/init.dart';</script>
  </head>
  <body>
    <form action="test" method="post">
      <h1>Plain-old Form</h1>
      <input type=text
             name="plain_old_param"
             placeholder="Plain-old form field">
      <x-pizza name="pizza_toppings" is="a-form-input"></x-pizza>
      <button>Order my pizza!</button>
    </form>
  </body>
</html>
Which almost works… except not at all (some of the functionality works, but ultimately crashes ugly).

I finally read the actual documentation to find that this is not how "is" works at all! It is meant to extends native DOM elements, not arbitrary elements like I am trying to do here. Sometimes there is nothing for it, but to admit that I am an idiot. OK, much times.

But while working this, I ran into an interesting problem. My AFormInput class does not seem to see attributes changing when moved into a package. That is, the same exact a_form_input.dart when used locally or used via a package… behaves differently. And it is an important difference. The attributeChanged() callback is never invoked in the super class via a package:
library a_form_input;

import 'package:polymer/polymer.dart';
import 'dart:html';

@CustomTag('a-form-input')
class AFormInput extends PolymerElement {
  @PublishedProperty(reflect: true)
  String name;

  @PublishedProperty(reflect: true)
  String value;
  // ...

  AFormInput.created(): super.created();
  void attached() {
    print('attached!');
    super.attached();
  }
  void attributeChanged(String name, String oldValue, String newValue) {
    print('$name: $oldValue → $newValue');
    if (name == 'name') lightInput.name = newValue;
    if (name == 'value') lightInput.value = newValue;
  }
}
If my test element (the old <x-pizza> standby) uses a local copy of this same file, then it can update value:
import 'package:polymer/polymer.dart';
import 'a_form_input.dart';
// import 'package:a-form-input/a_form_input.dart';

@CustomTag('x-pizza')
class XPizza extends AFormInput {
  // ...
  _updateText() {
    value = 'First Half: ${model.firstHalfToppings}\n'
      'Second Half: ${model.secondHalfToppings}\n'
      'Whole: ${model.wholeToppings}';
  }
  // ...
}
But if I swap the a_form_input.dart lines, then changes to the value attribute (or the name attribute) never trigger the attributeChanged() callback. I play with this for some time, but I cannot solve this. Everything else works—a hidden input is generated by <a-form-input> in either case. The attached() lifecycle method is called in either case. But I am clearly missing something in the packaged version. Something to solve tomorrow.

Day #13

No comments:

Post a Comment