anp / moxie

lightweight platform-agnostic tools for declarative UI
https://moxie.rs
Apache License 2.0
827 stars 27 forks source link

Allow users to declare custom html elements #259

Open simon-bourne opened 3 years ago

simon-bourne commented 3 years ago

This would allow moxie-dom users to use third party web components. It looks like making html_element public would be a good direction to support this.

simon-bourne commented 3 years ago

Straw Man Plan

html_element!

I don't think there's much that would need to be added to html_element! to support declaring custom html elements. We just need a way of declaring custom events. For example:

html_element! {
    <fast-tree-item>

    categories { Flow }

    children {
        categories {
            Flow
        }
    }

    attributes {
        /// Is this tree item expanded?
        expanded(bool)
        /// Is this tree item selected?
        selected(bool)
    }

    custom_events {
        selected-change
        expanded-change
    }
}

We should probably tweak the syntax to make it more Rust-like if it's going to be made public. expanded(bool) should probably become expanded: bool for example.

Defining Web Components

The plan is just to declare custom html elements, so moxie-dom users can use third party web components. This section is just an exploration of what defining custom html elements might look like, so we can see how it fits in with declaring them.

See here for instructions on using custom elements in Javascript. In summary, we call customElements.define('my-custom-element', MyCustomElement) to register the element. MyCustomElement has a static observedAttributes method that returns an array of attribute names. MyCustomElement also has some lifecycle methods, connectedCallback, attributeChangedCallback, disconnectedCallback and adoptedCallback.

We've already got the element name, attribute and event names from html_element. So we could add an optional define MyCustomElement to html_element!, then, given these traits:

pub trait CustomHtmlElement {
    fn name() -> &'static str;
    fn observed_attributes() -> ...;
    fn attribute_changed(self, ...);
}

pub trait OwnedHtmlElement {
    fn connected(self, ...);
    fn disconnected(self, ...);
    fn adopted(self, ...);
}

html_element! could generate:

impl CustomHtmlElement for MyCustomElement {
    fn name() { ... }
    fn observed_attributes() -> ... { ... }
    fn attribute_changed(self, ...) {
        // Dispatch to named, typed methods on `self`
    }
}

impl MyCustomElement {
    fn dispatch_my_event(self, detail) { ... }
    fn dispatch_my_other_event(self, detail) { ... }
    ...
}

Then a user would define attribute update methods and implement OwnedHtmlElement.

anp commented 3 years ago

@anp: you mentioned that html_element had some warts that you'd like to consider before making it public. I found issue #240. Was there anything else?

Nothing comes to mind right now, although I think your point about how to write expected(bool as expected: bool is a good one.

declaring custom events

Would the user need to define their own event types for these, would we generate opaque event type wrappers for them?

I like the basic direction of using a trait to define a custom element within moxie, and I agree we don't need to sort it out to be able to make use of existing web components :D.

simon-bourne commented 3 years ago

@anp: you mentioned that html_element had some warts that you'd like to consider before making it public. I found issue #240. Was there anything else?

Nothing comes to mind right now, although I think your point about how to write expected(bool as expected: bool is a good one.

I was also thinking commas between elements, but no strong preference either way.

declaring custom events

Would the user need to define their own event types for these, would we generate opaque event type wrappers for them?

I was thinking the user would supply their own event type with a constructor that accepted a Javascript CustomEvent. We need to give access to the event target as that's defined by the web component, but this allows the user to make the Javascript event opaque downstream. For example, if the event was selected-event, the user would define something like:

#[derive(Constructor)]
struct SelectedEvent{
    // User can decide how they want to represent their event
}

impl SelectedEvent {
    pub fn new(event: web_sys::CustomEvent) { ... }

    // User can provide whatever methods they want.
}

and we'd generate a call to SelectedEvent::new.

anp commented 3 years ago

What about requiring SelectedEvent: From<web_sys::CustomEvent>?