Friday, November 28, 2014

Extracting Polymer Behaviors from Existing Polymer Elements


My hack to get Polymer elements working with native forms (breaking encapsulation and inserting hidden <input> elements into the containing document) is growing on me. It was fairly easy to implement as a proof of concept. It was nearly as easy to write a bunch of tests verifying that the 80% use-cases work. So tonight I push ahead.

My normal next step while writing Patterns in Polymer would be to reimplement in the other language in which the book is written. In this case, since I have written the initial implementation in Dart, I would normally redo the implementation in JavaScript. If a solution works similarly as well in both languages, then I have some assurance that I have a solution that transcends languages—that it is a true pattern of the library.

But that's not what I am going to do tonight. I can't help myself—I must refactor. I wrote a handful of tests describing how I'd like Polymer elements to behave when they are used as HTML form elements. Tests are a great way to verify that things do not break over time. That is a necessary, but boring, reason to test. More fun is refactoring. I have all of these tests describing how I expect my element to behave. Unless I have poor tests (always a possibility), these tests will pass no matter how I might rework the implementation.

And the reimplementation that I hope to realize tonight is extracting this is-a-form-input behavior into a separate Polymer class that my custom Polymer elements can extend as subclasses.

The core <input> tests that I would like to continue to pass tonight are:
PASS: <x-pizza> acts like <input> - value property is updated when internal state changes
PASS: <x-pizza> acts like <input> - value attribute is updated when internal state changes
PASS: <x-pizza> acts like <input> - containing form includes input with supplied name attribute
PASS: <x-pizza> acts like <input> - setting the name property updates the name attribute
I should be able to pull all of last night's code out of the <x-pizza> element into <a-form-input> leaving <x-pizza> unchanged from when I began save for a new superclass:
import 'package:polymer/polymer.dart';
import 'a_form_input.dart';

@CustomTag('x-pizza')
class XPizza extends AFormInput {
  // Code reverted back to original implementation
}
That turns out to be quite easy to make happen. I define the AFormInput class as:
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;

  Element lightInput;

  AFormInput.created(): super.created();

  void attached() {
    super.attached();

    lightInput = new HiddenInputElement();
    if (name != null) lightInput.name = name;
    parent.append(lightInput);
  }

  void attributeChanged(String name, String oldValue, String newValue) {
    if (name == 'name') {
      lightInput.name = newValue;
    }
  }
}
All of that code is a direct copy of the code from last night except for the attached() lifecycle method. Polymer calls this method when the custom element is attached to a live DOM. This was more complicated last night because <x-pizza> had to do some work when attached as did this is-a-form-input code. Now that this code is properly encapsulated, I no longer need to keep these different behaviors in separate methods. Thanks to proper class encapsulation, these behaviors are pushed back up to the direct methods applying them.

Even better, my tests continue to pass:
PASS: <x-pizza> acts like <input> - value property is updated when internal state changes
PASS: <x-pizza> acts like <input> - value attribute is updated when internal state changes
PASS: <x-pizza> acts like <input> - containing form includes input with supplied name attribute
PASS: <x-pizza> acts like <input> - setting the name property updates the name attribute
Better still, is that my Polymer-is-a-form-input hack still works in live code:



Best of all is that I was absolutely certain that it would continue to work—thanks to the tests that I wrote last night.


Day #8


No comments:

Post a Comment