-
Notifications
You must be signed in to change notification settings - Fork 32
Proposal: Add Support for JS Constructor Interoperability #222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Thanks for the proposal! It's always cool to see people interested in expanding functionality. A few notes:
Dart lowers this as a We could instead lower the One is that you're not getting a Dart function back. When compiling to JS, calling the tear-off sort of works as intended because of the similarity between a compiled Dart function and a JS function, but this will certainly not work when compiling to Wasm. Another issue is that interop method/constructor invocations are transformed at the invocation site in order to account for optional parameters (the lack of an optional parameter is interpreted as not passing that optional rather than passing That being said, maybe there's some way to declare something like: external JSFunction get thisConstructor; // returns the constructor of the JS type within an interop type. Although, this is only marginally better than declaring the top-level interop member to get the constructor. :) Definitely open to other ideas if we can think of any, though.
As you've noticed, we don't really have a good way to declare a custom element in Dart. Extension types definitely won't work because they don't lower to an actual class. In the past, users have used some wrapper classes to make this work in the JS compilers: dart-lang/sdk#46248 and @donny-dont's package here: https://github.com/rampage-dart/rampage/tree/main. Of course, if you're already declared the class in JS, defining it with If you come up with a proposal to make custom elements work, please let us know! |
Just a note that I've been playing with migrating to |
Oh I see, I forgot this used some JS code to define the class inline: https://github.com/rampage-dart/rampage/blob/main/html/lib/browser/custom_element.js. Then yeah, this should work with dart2wasm when this migrates to |
I would really want to know how this can be implemented in |
Thanks for your notes so far. I do have some ideas in mind @srujzs but before I get into that, just a few takeouts from what you've said:
I do like the implementation that the rampage package has been able to go with. I'll see if I can make a unified solution (i.e having everything together in one class than having the main class and an implementation class) from what is there. However, because of the new interop being worked on, I'm looking towards a solution using extension types.
This could help for most use cases, however I would want to ask if we could consider it being a static member. That way we do not need to call the constructor of the object (for cases like in html elements). However, static members aren't inherited. Still looking at and towards other possible options, else we would probably have to go with using the |
It looks like if we don't need this to be external JSFunction get constructor; |
We wouldn't be able to do so for classes like void main() {
print(HTMLElement().constructor); // Error: Illegal Constructor
} |
I'm not sure where you're getting the document.createElement('html').constructor == HTMLHtmlElement; // true edit: Oh, I see, it's an older version of |
Oh I see now. My Bad. extension type DartArrayBuffer._(JSObject _) implements ArrayBuffer {
// ...
} |
Since |
You create a /// A JavaScript object that wraps a Dart object.
extension type DartWrapper._(JSObject _) implements JSObject {
external JSBoxedDartObject? get dartObject;
external set dartObject(JSBoxedDartObject? dartObject);
} You would then have a Dart object that stores a /// A bidirectional wrapper between Dart and JavaScript.
class DartJsWrapper implements JsWrapper {
/// Creates a [DartJsWrapper] around the [jsObject].
DartJsWrapper.fromJsObject(this.jsObject) {
final wrapper = jsObject as DartWrapper;
assert(
wrapper.dartObject == null,
'Another Dart Object is already attached to the JsObject',
);
wrapper.dartObject = this.toJSBox;
}
@override
final JSObject jsObject;
} You then build the same hierarchy in Dart by descending from class HtmlElement extends DartJsWrapper {
HtmlElement.fromJsObject(super.jsObject) : super.fromJsObject();
void click() {
(jsObject as js.HtmlElement).click();
}
} Then we need a Dart aware implementation of So 👇 's class CustomElementInterop {
constructor() {
this.constructorCallbacks = {};
this.connectedCallback = (d) => { throw new Error('connectedCallback not set') };
this.disconnectedCallback = (d) => { throw new Error('disconnectedCallback not set') };
this.attributeChangedCallback = (d, attr, oldVal, newVal) => { throw new Error('attributeChangedCallback not set') };
}
define(name, construct, observed) {
let that = this;
customElements.define(name, class extends HTMLElement {
constructor() {
super();
this.dartObject = construct(this);
}
connectedCallback() {
that.connectedCallback(this.dartObject);
}
disconnectedCallback() {
that.disconnectedCallback(this.dartObject);
}
attributeChangedCallback(attr, oldVal, newVal) {
that.attributeChangedCallback(this.dartObject, attr, oldVal, newVal);
}
static get observedAttributes() { return observed; }
});
}
}
window.CustomElementInterop = CustomElementInterop; On the Dart side we then create a The typedef CustomElementConstructor = HtmlElement Function(js.HtmlElement object);
class CustomElementRegistry {
CustomElementRegistry() : _registry = js.CustomElementInterop() {
_registry
..connectedCallback = _connectedCallback.toJS
..disconnectedCallback = _disconnectedCallback.toJS;
}
final js.CustomElementInterop _registry;
void define(
String name,
CustomElementConstructor constructor, [
List<String>? attributes,
]) {
js.JSBoxedDartObject? construct(js.HtmlElement jsObject) =>
constructor(jsObject).toJSBox;
_registry.define(name.toJS, construct.toJS, js.JSArray());
}
static void _connectedCallback(js.JSBoxedDartObject element) {
(element.toDart as CustomElement).connected();
}
static void _disconnectedCallback(js.JSBoxedDartObject element) {
(element.toDart as CustomElement).disconnected();
}
} And then we wrap HtmlElement createElement(String name) {
final jsObject = js.document.createElement(name.toJS) as js.DartWrapper;
final dartObject = jsObject.dartObjectAs<HtmlElement>();
return dartObject ?? HtmlElement.fromJsObject(jsObject);
} |
@srujzs Oh I see now. My Bad. Since the remaining cases have to do with custom components, it would be much more fitting to redirect to a new proposal, because you can't really do I have been making some progress concerning creating custom components with @donny-dont Thanks for the explanation! Really Helpful. I will redirect this to the new proposal. If there is anything else to discuss concerning this let me know. If not I'll have to close the issue, progress onto the Web Components proposal and redirect. |
Synopsis
This proposal aims to suggest the possible addition of support for Dart-JavaScript Interoperability with JavaScript constructor methods and other related functionality.
Dart-JavaScript interop works for a good majority of use cases, such as for interoping with classes (via extension types) and functions. However, one feature of JavaScript that Dart doesn't have support for is being able to interop and emulate javascript constructors.
Overview
In JavaScript, if you called
ArrayBuffer
, it would return a function, representing the constructor function. If you calledArrayBuffer()
however (I'm aware there should be anew
keyword infront), it would return an objectAs of now, if we wanted to perform interop with the javascript
ArrayBuffer
class, we would have to pick between one of these: either to interop with the class object as an extension type, or to interop with the constructor as a function callIf we wanted to incorporate both, we would need to give separate names.
TThis proposal suggests incorporating an extension type into a single interface, where the object's constructor can be represented through an API call in the extension type. Although it would be preferable to simply use the object's name, an alternative solution could be considered.
I would be glad to enlighten more, but in order to not make this proposal too lengthy as it already is, I would respond to any questions concerning this in the comments (if any).
Importance
One of the main reasons behind this proposal has been for the adoption of Web Components (which will be discussed in more detail in a later proposal). I tried working on making Web Components on my own using current Dart interop, but it wasn't successful in any direction.
The best case I had was the fact that I wasn't able to use the
window.customElements.define
method because I couldn't obtain the constructor of the new class. I also couldn't callMyCustomElement()
, asHTMLElement
, as well as any objects that extend it, do not allow calls to their constructors directly.It also would not be possible to obtain the constructor for a class/extension type
A
made in Dart that extends/implementsB
that is in Dart but exported from JavaScript, sinceA
isn't defined in JavaScript.Additional Information/Closing
If the Dart team would be interested, I would be glad to join in helping to make this possible/contributing for this proposal.
Web Components is another issue of it's own I guess, so I'd want to start with this one.
The text was updated successfully, but these errors were encountered: