Skip to content
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

Request for library: slim web component library #162

Open
fitzgen opened this issue Apr 25, 2018 · 21 comments
Open

Request for library: slim web component library #162

fitzgen opened this issue Apr 25, 2018 · 21 comments

Comments

@fitzgen
Copy link
Member

fitzgen commented Apr 25, 2018

It would be super cool if someone wrote a slim library for writing web components and custom elements in rust!

I'm imagining the library would have a trait, and if I (the library user) implement that trait for my type, then the library wires up all the glue to turn that trait implementation into a web component.

A great way to integrate nicely with the JS ecosystem! A website could use these web components without even needing to know that it was implemented in rust and wasm.

A potential web component to implement with this library might be a graphing/charting library that has to churn through a lot of data before displaying it.

@rail44
Copy link

rail44 commented May 22, 2018

https://github.com/rail44/squark
I'm working and trying to resolve rustwasm/wasm-bindgen#42 🤔

@fitzgen
Copy link
Member Author

fitzgen commented May 22, 2018

https://github.com/rail44/squark

Neat! Although I don't see any mention of web components, so I think it may be orthogonal to this issue...

I'm working and trying to resolve rustwasm/wasm-bindgen#42

Awesome! Do you have any particular questions or anything? Probably best to move this part of the discussion into that issue.

@rail44
Copy link

rail44 commented May 23, 2018

I don't see any mention of web components

Oh, sorry 😅
Currently, I use stdweb for binding to web browser.
I think, Importing WebIDL is necessary to provide more binding for browser world such as web component.

@ctjhoa
Copy link

ctjhoa commented Jan 26, 2019

Hi,

I want to give a try on this but I'm quickly stuck on few things.
My first try is simple:

Here are my questions:

  • define expect a js_sys::Function as argument. How can I retreive the JS prototype of the class created in the first step?
  • If we want to use shadow DOM we need to call attach_shadow in the custom element constructor. I don't see how a struct can extend web_sys::Element nor web_sys::HTMLElement as rust do not have inheritance.

@fitzgen
Copy link
Member Author

fitzgen commented Jan 28, 2019

Hi @ctjhoa! Excited to see some movement here :)

The design that I think makes sense is to have the actual HTMLElement subclass be a JavaScript class that creates a the inner #[wasm_bindgen]-exposed Rust struct in the connectedCallback and then frees it in the disconnectedCallback. E.g. https://rustwasm.github.io/sfhtml5-rust-and-wasm/#74

This approach saves users from having to manage memory and lifetimes of objects themselves. It also side steps some of your questions above.

Also, the primary interface between a web-component and the outside world is the custom element's attributes. It would be A+ if we could provide a serde deserializer from attributes to rich rust types that the web component uses (or at minimum does things like parse integers from attribute value strings).

Are you available to come to the next WG meeting? If possible, it would be great to have some high-throughput design discussion on this stuff :) #252

@Pauan
Copy link

Pauan commented Jan 31, 2019

@fitzgen It's not feasible to use connectedCallback and disconnectedCallback to manage Rust lifetimes, because they aren't actually tied to the lifecycle of the DOM node, and both callbacks can be called multiple times:

https://codepen.io/Pauan/pen/961b58f8fc23677268ad11f37e3c6cc9

(Open the dev console and see the messages)

As you can see, every time a DOM element is inserted/removed, it fires the connectedCallback/disconnectedCallback.

This happens even when merely moving around the DOM node (without removing it).

Unfortunately I don't see a clean way to fix this. You mentioned (during the WG meeting) using setTimeout (or similar), which would technically work in some situations, but not others. It also feels very hacky.

So I think we're still stuck with manual memory management, manually calling a free method (or similar).

@fitzgen
Copy link
Member Author

fitzgen commented Jan 31, 2019

@Pauan, thanks for making an example and verifying this behavior!

I still think a delayed (and cancellable!) destruction is what we want here, because of the superior UX. Custom elements are supposed to "just" be another element, and they are supposed to completely encapsulate themselves without requiring users do anything that "normal" elements require.

I think using requestIdleCallback would actually fit this use case really well:

import { RustCustomElement } from "path/to/rust-custom-element";

class CustomElement extends HTMLElement {
  constructor() {
    this.inner = null;
    this.idleId = null;

    if (this.isConnected) {
      this.inner = new RustCustomElement(/* ... */);
    }
  }

  connectedCallback() {
    if (!this.inner) {
      window.cancelIdleCallback(this.idleId);
      this.idleId = null;
      this.inner = new RustCustomElement(/* ... */);
    }
  }

  disconnectedCallback() {
    this.idleId = window.requestIdleCallback(() => {
      const inner = this.inner;
      this.inner = null;
      inner.free();
    });
  }
}

@Diggsey
Copy link

Diggsey commented Jan 31, 2019

The other option would be to require the state to be serializable. That way you can store all the state directly on the component (or in a WeakMap) and you don't actually need to free anything.

@Pauan
Copy link

Pauan commented Jan 31, 2019

@fitzgen I still think a delayed (and cancellable!) destruction is what we want here, because of the superior UX. Custom elements are supposed to "just" be another element, and they are supposed to completely encapsulate themselves without requiring users do anything that "normal" elements require.

I agree that's a good goal, I'm just not seeing a clean way to accomplish that.

If a user removes the custom element from the DOM, then waits for a bit (perhaps using setTimeout, or perhaps waiting for an event), and then re-inserts it into the DOM, then things will be broken since the Rust objects will have been freed.

An unusual case, sure, but it definitely can happen, and I can even imagine use-cases for it (e.g. a modal dialog which is only inserted into the DOM while the modal is open, and is otherwise detached from the DOM).

So proper support probably requires WeakRef or similar.

Having said that, we can totally experiment with custom elements even without WeakRef (just with the above caveat).

@Pauan
Copy link

Pauan commented Jan 31, 2019

Also, as for encapsulation, custom elements can actually have custom methods, which the consumer can then access:

class Foo extends HTMLElement {
    myMethod() {
        ...
    }
}

customElements.define("my-foo", Foo);

var foo = document.createElement("my-foo");

foo.myMethod();

So I don't think it's that unusual to call methods on custom elements. I imagine that's probably the preferred way to update the custom element's internal state from the outside.

In addition, because JS doesn't have finalizers, I think there will be custom elements (written entirely in JS) which require a free method (or similar), for the sake of cleaning up event listeners (and other resources which can't be claimed by the JS garbage collector).

So overall I don't think it's that unusual to have a free method for custom elements, but I guess we should wait and see how the ecosystem evolves.

@fitzgen
Copy link
Member Author

fitzgen commented Jan 31, 2019

If a user removes the custom element from the DOM, then waits for a bit (perhaps using setTimeout, or perhaps waiting for an event), and then re-inserts it into the DOM, then things will be broken since the Rust objects will have been freed.

The reinsertion will trigger a new Rust object to be created, so this won't result in a bug. See the if (!this.inner) check in the connectedCallback in the JS snippet in my previous comment.

Yes, there can be multiple Rust objects used across the lifetime of the JS custom element: the idea is that creating and destroying the inner Rust thing on every attach or detach is the baseline, and the delay is an optimization to cut down on thrashing when just moving the element instead of removing it.

Unless I am misunderstanding what you are saying?

@fitzgen
Copy link
Member Author

fitzgen commented Jan 31, 2019

So I don't think it's that unusual to call methods on custom elements. I imagine that's probably the preferred way to update the custom element's internal state from the outside.

I think the usual way is via setting attributes (eg same as value, min, and max for <input type="range"/>) unless there are Other Reasons where that doesn't make sense.

Ultimately, yes we want finalizers, and we can actually polyfill it now. Which is something we need to get published...

@Pauan
Copy link

Pauan commented Jan 31, 2019

@fitzgen Ah, okay, I had (incorrectly) assumed there would be a 1:1 relationship between the Rust struct and the custom element.

If you instead make it N:1 then yeah, it can just dynamically create/destroy the Rust objects on demand. I don't think that'll work for every use case, but it should work for most.

I'm a bit concerned about the user's expectations though, I think other users will also expect a 1:1 relationship.

@Pauan
Copy link

Pauan commented Jan 31, 2019

As for attributes, (at least in HTML) those are primarily used for static pre-initialized things. For dynamic things, users instead use setters/methods like foo.min = 5. The same goes for adding event listeners (and other things).

Since custom elements can listen to attribute changes, they can respond to dynamic attribute changes, but using foo.setAttribute("bar", "qux") seems rather inconvenient and unidiomatic compared to foo.bar = "qux" (or similar).

Since getters/setters/methods are so common with regular DOM elements, I expect them to be similarly common with custom elements (I don't have any statistics or experience to back that up, though).

@ctjhoa
Copy link

ctjhoa commented Feb 2, 2019

I've made a proof of concept
https://github.com/ctjhoa/rust-web-component
It uses requestIdleCallback to free rust resources.
I've tried to use other techniques with WeakMap & WeakSet without success.

So what's going on in this project.

  • First, it creates a web component which contains a rust object instance. This web component increments the rust model every 100ms.
  • This web component is then moved in the DOM every 2s.
  • Finally, after 3 moves this component is destroyed and another one is instantiated.

@trusktr
Copy link

trusktr commented May 25, 2019

Seems like any wasm module just needs a JS custom element glue class to call into the module (to trigger the lifecycle hooks). Even if wasm gets ability to reference DOM elements in the future, there will be no way to avoid the JS glue class (to pass into customElements.define()), unless the Custom Elements API evolves to accept wasm modules and not just JS classes.

@olanod
Copy link

olanod commented Jun 6, 2019

Can't you create the "class" from rust creating a function object changing the prototype, etc, etc?

@Pauan
Copy link

Pauan commented Jun 6, 2019

@olanod That still requires JS glue to create the function object (and change the prototype).

Secondly, as far as I know, it's not possible to use ES5 classes for custom elements:

function Foo() {
    console.log("HI");
}

Foo.prototype = Object.create(HTMLElement.prototype);

customElements.define("my-foo", Foo);

// Errors
var x = document.createElement("my-foo");

This is because ES5 classes cannot inherit from special built-ins like Array, RegExp, Error, or HTMLElement.

But ES6 classes were specifically designed so that they can inherit from built-ins. So ES6 classes aren't just a bit of syntax sugar, they actually have new behavior.


However, even given the above, we only need a single JS function to create all of the classes (this was mentioned by @trusktr ):

export function make_custom_element(parent, observedAttributes, connectedCallback, disconnectedCallback, adoptedCallback, attributeChangedCallback) {
    return class extends parent {
        static get observedAttributes() { return observedAttributes; }

        connectedCallback() {
            connectedCallback();
        }

        disconnectedCallback() {
            disconnectedCallback();
        }

        adoptedCallback() {
            adoptedCallback();
        }

        attributeChangedCallback(name, oldValue, newValue) {
            attributeChangedCallback(name, oldValue, newValue);
        }
    };
}

Now wasm can just call the make_custom_element function.

In fact, it should be possible to do that right now, no changes needed to wasm-bindgen.

@eggyal
Copy link

eggyal commented Aug 27, 2019

With rustwasm/wasm-bindgen#1737, it's possible to do:

#[wasm_bindgen(prototype=web_sys::HtmlElement)]
struct MyCustomElement {}

#[wasm_bindgen]
impl MyCustomElement {
    #[wasm_bindgen(constructor)]
    fn new() -> WasmType<MyCustomElement> {
        instantiate! { MyCustomElement{} }
    }
}

// ...

web_sys::window()
    .unwrap()
    .custom_elements()
    .define("my-custom-element", &js_sys::JsFunction::of::<MyCustomElement>())?;

// ...

Any thoughts or input into that PR and/or the (early draft) RFC for which it's a prototype would be greatly appreciated!

@SephReed
Copy link

@eggyal Oh no! What happened with your draft?

What would it take to get this train moving again? I could really use the ability to extend and implement AudioWorkletProcessor in Rust.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants