Thursday, November 27, 2014

TDD a Polymer (Dart) Form Element


After last night, I am leaning toward being bad. Well, not too bad, but I have given up on trying to implement custom Polymer form elements the right way. Instead I begin to think the best approach is to break encapsulation of Polymer elements so that they can inject hidden <input> elements into the containing document containing the necessary data.

I am not entirely sure how this is going to work in practice, but I think I can describe some of the functionality as:
  var _el, _form, _container;
  group("<x-pizza>", (){
    setUp((){ /* ... */ });
    group('acts like <input>', (){
      test('value property is updated when internal state changes', (){});
      test('value attribute is updated when internal state changes', (){});
      test('containing form includes input with supplied name attribute', (){});

      test('setting the value property updates the value attribute', (){});
      test('setting the name property updates the name attribute', (){});
    });
  });
Most of those come for free from Polymer. In the <input> group, I setup as follows:
    group('acts like <input>', (){
      setUp((){
        _el.name = 'my_field_name';
        _el.model.firstHalfToppings.add('pepperoni');

        var completer = new Completer();
        _el.async(completer.complete);
        return completer.future;
      });
      // Tests here...
    });
This setup is mostly from last night. It sets the name of the "input" and updates the internal state such that my <x-pizza> element has pepperoni on the first half. The completer dance around Polymer's async() ensures that the following tests will not run until Polymer has updated the internal state of my custom element.

The first two tests just work™:
  var _el, _form, _container;
  group("<x-pizza>", (){
    setUp((){ /* ... */ });
    group('acts like <input>', (){
      test('value property is updated when internal state changes', (){
        expect(
          _el.value,
          startsWith('First Half: [pepperoni]')
        );
      });
      test('value attribute is updated when internal state changes', (){
        expect(
          _el.getAttribute('value'),
          startsWith('First Half: [pepperoni]')
        );
      });
      // More tests...
    });
  });
There is not much test driven development in this case. All that <x-pizza> needs is published, reflectable properties:
@CustomTag('x-pizza')
class XPizza extends PolymerElement {
  // ...
  @PublishedProperty(reflect: true)
  String name;

  @PublishedProperty(reflect: true)
  String value;

  updatePizzaState([_]) {
    value = 'First Half: ${model.firstHalfToppings}\n'
      'Second Half: ${model.secondHalfToppings}\n'
      'Whole: ${model.wholeToppings}';
    // ...
  }
  // ...
}
The next test is a little trickier mostly because Dart lacks the normal elements property on <form> elements. So instead of iterating across all form elements, I have to hack an approximation for it:
  var _el, _form, _container;
  group("<x-pizza>", (){
    setUp((){ /* ... */ });
    group('acts like <input>', (){
      setUp((){
        _el.name = 'my_field_name';
        // ...
      });
      // ...
      test('containing form includes input with supplied name attribute', (){
        var inputNames = _form.
          children.
          where((i)=> i.tagName == 'INPUT').
          map((i)=> i.name);
        expect(inputNames, contains('my_field_name'));
      });
    });
  });
Happily the test turns out to be the hardest part of this because updating the <input> that is injected into the light DOM is as easy as updating the element from last night when attributes change:
@CustomTag('x-pizza')
class XPizza extends PolymerElement {
  // ...
  Element lightInput;
  // ...
  void attributeChanged(String name, String oldValue, String newValue) {
    if (name == 'name') {
      lightInput.name = newValue;
    }
  }
  // ...
}
With that, I have my <x-pizza> Polymer element approximating the behavior of a native form element. I am likely overlooking an edge case or two—ah, who I am kidding? This is HTML so I am certainly missing at least a score of edge cases. Still, as proofs of concepts go, this seems reasonable to me. I can create custom Polymer elements that will work as expected with native <form> elements. I should double check that this works in JavaScript (where I'll have access to form.elements). That aside this is definitely promising.


Day #7

No comments:

Post a Comment