Wednesday, February 10, 2016

Factory Method Pattern and Polymer Paper Elements


I may regret this, but my web-based factory method pattern example lacked a "modern" touch. Well, that's not entirely true, it could add relatively new input elements like number, telephone, and week inputs to a form:



But darn it, what about awesome Paper Elements? Will a <paper-input> element serve as a product of the pattern? It sure would be neat if I could simply drop it into the pattern without many changes.

But alas, it seems that some changes are going to be required. Even though it behaves like an input field, the PaperInput class implements HtmlElement, not InputElement like my FormBuilder class wants:
abstract class FormBuilder {
  // ...
  void addInput() {
    var input = _labelFor(inputElement);
    container.append(input);
  }
  InputElement get inputElement;
  // ...
}
The factory method in this example is the inputElement getter, which returns an InputElement. Because of the PaperElement inheritance, I have to change that to return the more general HtmlElement:
abstract class FormBuilder {
  // ...
  HtmlElement get inputElement;
  // ...
}
How many other changes am I going to have to make?

Well, I certainly have to list Polymer as a dependency in my project's pubspec.yaml. To get <paper-input>, I need paper_elements as well:
name: factory_method_code
dependencies:
  polymer: ">=0.15.1 <0.17.0"
  paper_elements: ">=0.7.0 <0.8.0"
transformers:
- polymer:
    entry_points:
    - web/index.html
Next, I have to import the paper element into the containing page:
<!doctype html>
<html lang="en">
  <head>
    <!-- ... -->
    <link rel="import" href="packages/paper_elements/paper_input.html">
    <script type="application/dart" src="input_factory.dart"></script>
  </head>
  <body><!-- ... --></body>
</html>
I have to do the same with the backing code in the input_factory.dart script:
import 'package:polymer/polymer.dart';
import 'package:paper_elements/paper_input.dart';

abstract class FormBuilder {
  // ...
}
// Concrete factories here...
The final Polymer-y thing that needs to happen is an initPolymer() call in my main() entry point, also in the input_factory.dart script:
main() async {
  await initPolymer();
  // Start up the factory code once Polymer is ready...
}
That's it for the Polymer preliminaries. Now what needs to change in the pattern itself to accommodate PaperInput? Mercifully, not much. In the RandomBuilder class, which is a concrete factory implementation of the FormBuilder, I add a new random option (#5) for PaperInput:
class RandomBuilder extends FormBuilder {
  RandomBuilder(el): super(el);
  Element get inputElement {
    var rand = new Random().nextInt(7);

    if (rand == 0) return new WeekInputElement();
    if (rand == 1) return new NumberInputElement();
    if (rand == 2) return new TelephoneInputElement();
    if (rand == 3) return new UrlInputElement();
    if (rand == 4) return new TimeInputElement();
    if (rand == 5) return new PaperInput();

    return new TextInputElement();
  }
}
Unfortunately, I also have to make a change in the superclass. Previously, when all of the products were InputElement instances, I could count on them supporting the type property (e.g. telephone, number, time), which I could use as a default text label on the page:
abstract class FormBuilder {
  // ...
  _labelFor(el) {
    var text = el.type;
    var label = new LabelElement()
      ..appendText("$text: ")
      ..append(el);

    return new ParagraphElement()..append(label);
  }
}
That will not work for PaperInput, which does not support the type property. If I had control of the product classes, I could force PaperInput to support type. Since that is not possible, I either have to create wrapper classes or, more easily, add an object type test for InputElement:
abstract class FormBuilder {
  // ...
  _labelFor(el) {
    var text = (el is InputElement) ? el.type : el.toString();
    // ...
  }
}

By itself, that one line is not too horrible. That said, I can imagine things going quickly off the rails here by adding a non-InputElement into the mix. For a limited use case like this example, however, it works.

In the end, it was fairly easy to get a PaperInput element added to the mix of my InputElement factory method. Most of the effort was the usual Polymer overhead. The resulting interface mismatch likely will not cause too much trouble in this case, but could get ugly quickly in other scenarios.

Day #91

1 comment: