domenic / html-as-custom-elements

HTML as Custom Elements
https://domenic.github.io/html-as-custom-elements/
Apache License 2.0
260 stars 20 forks source link

How to automate WebIDL type conversions and reflected attributes? #31

Closed domenic closed 9 years ago

domenic commented 9 years ago

In all cases let's analyze this particular interface, since it's representative of a wide cross-section of features.

interface Foo : Bar {
    attribute unsigned long x;
    readonly attribute unsigned long y;
    boolean method(DOMString arg);
}

In this case we also want that "the x attribute must reflect the content attribute of the same name". Note that reflecting has special rules for each type that are largely about string parsing, and cannot be fully encapsulated in the WebIDL type alone.

.idl files generating wrappers

We'd commit an IDL file almost exactly like the above, but we'd add the [Reflect] annotation that is often used by implementers:

// Foo.idl
interface Foo : Bar {
    [Reflect] attribute unsigned long x;
    readonly attribute long y;
    boolean method(DOMString arg);
}

Then we'd write the following ES6:

// FooImpl.js
class FooImpl {
    get y() { return Math.random() * 1000; }
    method(arg) { return arg.toLowerCase(); }
}

Things to note:

What will then happen is that we do something like

registerImpl("custom-foo", FooImpl);

and registerImpl generates a wrapper class Foo that delegates to FooImpl and does all the other stuff determined by the IDL. If we were to write it out, it would look like

// FooGenerated.js
import reflector from "./lib/webidl/reflector";
import conversions from "./lib/webidl/conversions";

class Foo extends Bar {
    get x() { return reflector["unsigned long"].get(this, "x"); }
    set x(v) { reflector["unsigned long"].set(this, "x", v); }

    get y() {
        var implGetter = Object.getOwnPropertyDescriptor(FooImpl.prototype, "y").get;
        var implResult = implGetter.call(this);
        return conversions["long"](implResult);
    }

    method(arg) {
        arg = conversions["DOMString"](arg);
        var implMethod = FooImpl.prototype.method;
        var implResult = implMethod.call(this, arg);
        return conversions["boolean"](implResult);
    }
}

Traceur Annotations

This solution avoids an external IDL file in favor of baking the same information into the source code. That has a lot of attraction to it.

Annotations are an experimental feature of Traceur. They purely decorate code with information that can later be used. There are a few possibilities, especially with regard to reflected attributes:

// Foo1.js
import { unsignedLong, long, boolean, DOMString } from "./lib/webidl/types";
import { Reflect } from "./lib/webidl/annotations";

class Foo extends Bar {
    @unsignedLong @Reflect get x() {};
    @unsignedLong @Reflect set x() {};

    @long get y() { return Math.random() * 1000; }

    @boolean method(@DOMString arg) { return arg.toLowerCase(); }
}
// Foo2.js
import { unsignedLong, long, boolean, DOMString } from "./lib/webidl/types";
import { Reflect } from "./lib/webidl/annotations";

@Reflect("x", unsignedLong)
class Foo extends Bar {
    @long get y() { return Math.random() * 1000; }

    @boolean method(@DOMString arg) { return arg.toLowerCase(); }
}

Again we'd use a level of indirection to translate the annotated class into one with the desired behavior, e.g.

registerAnnotated("custom-foo", Foo);

Things to note:

Using Traceur's types feature we can do something a bit less messy, at least assuming we can fix a bug that currently exists where return types have no affect unless you check the type-assertions option.

// Foo1.js
import { unsignedLong, long, boolean, DOMString } from "./lib/webidl/types";
import { Reflect } from "./lib/webidl/annotations";

class Foo extends Bar {
    @Reflect get x() : unsignedLong {};
    @Reflect set x(v : unsignedLong) {};

    get y() : long { return Math.random() * 1000; }

    method(arg : DOMString) : boolean { return arg.toLowerCase(); }
}

There's similarly a Foo2.js which omits the dummy getter/setter in favor of @Reflect("x", unsignedLong)

My thoughts:

Yehuda has a proposal for decorators. This is not implemented in Traceur, and so the fact we'd need to shave that yak might kill this idea out of the gate. But it does help our use cases quite a lot over annotations, as you will see.

// Foo.js
import { unsignedLong, long, boolean, DOMString } from "./lib/webidl/types";
import { Reflect, params } from "./lib/webidl/annotations";

class Foo extends Bar {
    +Reflect("x", unsignedLong)
    -long get y() { return Math.random() * 1000; }
    -boolean -params(DOMString) method(arg) { return arg.toLowerCase(); }
}

Things to note:

So far I like .idl files and Traceur types the most. I think I am leaning toward .idl files, largely because Traceur types are nonstandard (and I am still not satisfied with how they would handle reflected things).

domenic commented 9 years ago

I am going to go with the ".idl files generating wrappers" approach. Opening a new issue to track that.