Saturday, December 6, 2014

Actually, Polymer Dart Mixins Do Work (Kinda)


I may have given up prematurely on Dart mixins with Polymer.dart code. I still do not believe that it will work well enough that I would use mixins often, but maybe in certain specialized cases. If nothing else, I am curious to find the minimum viable Polymer mixin that can work.

As suggested in the comments from last night's post, I start by commenting out the @PublishedProperty Polymer annotations from my mixin class:
library a_form_input;

abstract class AFormInputMixin {
  // @PublishedProperty(reflect: true)
  String name;

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

  // More code to go here...
}
The goal of this mixin is to enable arbitrary Polymer elements to work in native HTML forms, hence the need for name and value properties and attributes.

I still need both name and value to be published by my concrete Polymer that mixes this in, but as I found last night, I cannot use this annotation in my mixin class. Instead, I have to declare the instance variables in the mixin (so that it can reference them), but mark them to be published in the concrete class, in this case my <x-pizza> element:
import 'package:polymer/polymer.dart';

import 'package:a-form-input/a_form_input_mixin.dart';
// import 'package:a-form-input/a_form_input.dart';

@CustomTag('x-pizza')
class XPizza extends PolymerElement with AFormInputMixin {
// class XPizza extends AFormInput {

  @PublishedProperty(reflect: true)
  String name;

  @PublishedProperty(reflect: true)
  String value;
  // More code here...
}
And that actually works. At least, the code both compiles and runs in the browser, which is more than I was able to accomplish last night.

I still do not have the desired functionality in my <a-form-input> mixin, however. Specifically, I want the mixin to be responsible for injecting a hidden <input> element into the owner document and keep this hidden <input> in sync with the internal state of <x-pizza>:
library a_form_input;

import 'dart:html' show HiddenInputElement;

abstract class AFormInputMixin {
  // @PublishedProperty(reflect: true)
  String name;

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

  Element lightInput;

  void attached() {
    lightInput = new HiddenInputElement();
    if (name != null) lightInput.name = name;
    parent.append(lightInput);

    changes.listen((list){
      list.forEach((change) {
        if (change.name == #name) lightInput.name = change.newValue;
        if (change.name == #value) lightInput.value = change.newValue;
      });
    });
  }
}
Annotating properties in the concrete class when they were declared in the mixin is pretty ugly, but as I found last night, there is no other option available to me.

I am using the attached() Polymer lifecycle method above. When the Polymer element is attached to the DOM, this method is invoked. I have almost no hope that it will work however, because this is a mixin, not a superclass. The problem is that I would normally invoke this from the concrete class using inheritance via super.attached():
import 'package:polymer/polymer.dart';

import 'package:a-form-input/a_form_input_mixin.dart';

@CustomTag('x-pizza')
class XPizza extends PolymerElement with AFormInputMixin {
  @PublishedProperty(reflect: true)
  String name;

  @PublishedProperty(reflect: true)
  String value;
  // ...
  void attached() {
    super.attached();
    // attachForm();
    _updateGraphic();
  }
  // ...
}
I had expected that the concrete class would have to invoke a method from the mixin, such as attachForm(), but simply calling super.attached()… actually seems to work. The hidden <input> field is added to the containing document and the change listener even works:



When I update the <x-pizza> element, both the <x-pizza> attributes and the the hidden <input> attributes are updated. This even works if I use the attributeChanged() form of the listener in the <a-form-input> mixin:
abstract class AFormInputMixin {
  // @PublishedProperty(reflect: true)
  String name;

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

  Element lightInput;

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

  void attributeChanged(String name, String oldValue, String newValue) {
    print('$name: $oldValue → $newValue');
    if (name == 'name') lightInput.name = newValue;
    if (name == 'value') lightInput.value = newValue;
  }
}
So really, Polymer mixins are nowhere near as horrible as I had feared. Unfortunately, annotations do not work. This is a significant drawback as the mixin class is neither as expressive nor does it get some of the assurance that would otherwise come from static analysis. It also makes for extra work for the developer trying to use this mixin. Even so, it is not so onerous that I would dismiss it outright—especially in cases that did not require a published property in the mixin.



Day #16

7 comments:

  1. I think I have a solution. I'll post it soon. It'll allow you to put PublishedProperty annotation into the mixin. It's achieved by using

    abstract class PolymerMixin implements Observable, Polymer {
    @PublishedProperty(reflect: true)
    String get name => readValue(#name) ;
    set name(val) => writeValue(#name, val) ;

    attached() {
    ...
    }
    }

    I think that will do what you want.

    ReplyDelete
    Replies
    1. I don't think that'll work. For me, at least, the concrete class trying to use the mixin generates a runtime “mixin class must extend class 'Object'” error. I'm more than happy to be proven wrong though :)

      Delete
    2. Don't worry, I'll endeavour to prove you wrong. :-). Maybe it's something to do with the version of Dart. I'm using the development channel.

      Delete
    3. Try this Gist https://gist.github.com/terrasea/3bf401e6b5caf55128ee which should prove you wrong, I hope :-)

      Delete
    4. Your mistake was doing this

      @PublishedProperty(reflect: true)
      String name;

      What you should be doing is using a getter and setter for this, using the readValue and writeValue methods. It won't work any other way for some reason, which I don't know at this moment.

      @PublishedProperty(reflect: true)
      String get name => readValue(#name) ;
      set name(val) => writeValue(#name, val) ;

      Delete
  2. Indeed, I use mixins to add adaptive behavior to Polymer elements, and some versions ago it worked with fields but one day I had to replace them with properties.

    ReplyDelete
    Replies
    1. You're ahead of me. I've only just started looking at them thanks to Chris and his blog

      Delete