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

Add custom event support #1991

Closed
wants to merge 17 commits into from
Closed

Conversation

mc1098
Copy link
Contributor

@mc1098 mc1098 commented Aug 8, 2021

Adds assigning listener syntax:

let click = Callback::from(|_| ());
html! {
    <div 
        on:{click}
        on:click={Callback::from(|_| ())
    />
}

If / when we support adding event listeners onto Components directly it will distinguish
when an event listener is being assigned to a Component instead of being passed down as a prop:

// WON'T COMPILE TODAY (for anyone skimming through looking at examples)

// pretend I defined `click` and `onchange` correctly :)
<MyComp 
    // event handler being used on component
    on:{click}
    // callback being passed down to the component for internal use
    {onchange}
/>

Add custom event support

Custom event support comes from implementing the StaticEvent trait:

pub trait StaticEvent {

   type Event: JsCast + AsRef<web_sys::Event> + 'static;
   
   fn event_name() -> &'static str;
}

On it's own this would allow a user implement the trait with the Event
type being the wrapped web_sys event.

struct MyCustomEvent(web_sys::Event);

impl StaticEvent for MyCustomEvent {
    type Event = web_sys::Event;
    
    fn event_name() -> &'static str {
        "custom"
    }
}

html! {
    <div
        // here callback gets a web_sys::Event and not the MyCustomEvent type
        // but the user could just wrap the type MyCustomEvent(e) then call 
        // any useful functions. Not bad...but we can do better :)
        on:MyCustomEvent={Callback::from(|e: web_sys::Event| ())}
    />
}

A similar pattern is used to implement the events supported by yew, but with unit based structs as we just want the raw web_sys event passed in.
This allows us to treat supported and custom events in the same way and reduces the need for multiple on[event] modules.
This also means that if Yew wanted to provide it's own events somewhen then we could easily utilise the custom_event macro to make NewTypes around the raw web_sys events and this would work the same internally.

When implementing the trait like this if a user wanted to use a function on the NewType they would
have to wrap the event type in that NewType first, which doesn't seem very ergonomic.
It is possible for a user to use Self as the Event type but then they have
to implement JsCast on that type, which is not trivial even when wrapping a JsCast type.

To help with the boiler plate, this PR also adds the custom_event attribute macro which
can be applied to NewTypes that wrap a type that implements JsCast + AsRef<web_sys::Event>, this includes any
type imported in a wasm_bindgen extern block that extends Event.

The custom_event macro has one attribute which is used to define the "event name"
if this name is representable in Rust then a shorthand can be used:

Shorthand

// bind ident to event name `custard` 
#[custom_event(custard)]
// Newtype here

Normal

#[custom_event(bizarre_event = "    represent ''' this in Rust")]
// Newtype here

The ident defined in the attribute can then be used in the same way as other events:

// each callback would be accepting the NewType that the custom_event macro
// was used on!

// can still use the shorthand in html too if the variable matches!
let bizarre_event = Callback::from(|_| ());
html! {
    <div
        on:custard={Callback::from(|_| ())}
        on:{bizarre_event}
    />
}

Putting it all together:

#[custom_event(custom)]
struct MyCustomEvent(web_sys::Event);

// The shorthand works here when the variable name matches the type name
html! {
    let custom = Callback::from(|e: MyCustomEvent| ());
    <div
        on:{custom}
        // you can still use the struct name if you prefer!
        on:MyCustomEvent={Callback::from(|e: MyCustomEvent| ())}
    />
}

When implementing the static_event_impl macro I added optional comments and they display really well in the html macro for VSCode, I've only added a comment about the formdata event:
image
This could be done as is with the modules but I think it's a nice touch so have added it here as I was using a new macro_rules macro anyways.

I have changed the syntax and implementation a few times over getting to this point
but I don't think it's in bad shape (hopefully ;P) and look forward to hearing what
people think :)

Typed Custom Event

I think it's worth just going over the example that triggered the linked issue and how a solution
might look with this change. It was the ability to listen to the "MDCSnackbar:closing" event and others like it on
imported web components (in the example the mwc-snackbar).

How this example looks today:
Snackbar in material-yew

A possible solution with this PR:

use wasm_bindgen::prelude::*;

// DetailsReason implements JsCast when imported in by wasm_bindgen
#[wasm_bindgen]
extern "C" {
    #[derive(Debug)]
    #[wasm_bindgen(extends = Object)]
    type DetailsReason;

    #[wasm_bindgen(method, getter)]
    fn reason(this: &DetailsReason) -> Option<String>;
}

use yew::custom_event;

#[custom_event(snackbarclosing = "MDCSnackbar:closing")]
struct SnackBarClosingEvent(web_sys::CustomEvent);

impl SnackBarClosingEvent {
    // web_sys::CustomEven` has a `detail` method which returns a JsValue, which is not very 
    // user friendly. As we have wrapped the CustomEvent in our new type we can now define our own
    // detail method that returns our imported type.
    pub fn detail(&self) -> DetailsReason {
        // JsCast::unchecked_into to jump from JsValue to DetailsReason
        self.0.detail().unchecked_into()
    }

    // You may just want to skip the detail call and provide a direct method to the reason
    pub fn reason(&self) -> Option<String> {
        self.0.detail().unchecked_into::<DetailsReason>().reason()
    } 
}

// Then inside the SnackBar Component a Callback can be used like any other supported event

use yew::prelude::*;

#[derive(Properties)]
pub struct SnackBarProps {
    #[prop_or_default]
    pub onclosing: Callback<Option<String>>,
    // rest omitted for clarity
}

impl Component for MatSnackBar {

    // create, update, etc omitted for clarity

    fn view(&self) -> Html {
        let snackbarclosing = self.props.onclosing.reform(|e: SnackBarClosingEvent| {
            e.detail().reason()
        });
        html! {
            <mwc-snackbar
                on:{snackbarclosing}
                // rest omitted
            >
                { self.props.children.clone() }
            </mwc-snackbar>
        }
    }
}

This Component could make the SnackBarClosingEvent and it's snackbarclosing alias public and
change the onclosing property to accept this event. This approach means the event can be defined
with the Component and imported by the user :)

Fixes #1777

Checklist

  • I have run cargo make pr-flow
  • I have reviewed my own code
  • I have added tests

@github-actions
Copy link

github-actions bot commented Aug 8, 2021

Visit the preview URL for this PR (updated for commit 7dc56d9):

https://yew-rs--pr1991-on-custom-event-dd4nt6dy.web.app

(expires Thu, 02 Sep 2021 21:32:36 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@mc1098 mc1098 mentioned this pull request Aug 15, 2021
13 tasks
@mc1098 mc1098 force-pushed the on-custom-event branch 2 times, most recently from 849b37d to 8ed4aad Compare August 23, 2021 14:52
mc1098 added 17 commits August 26, 2021 18:36
Uses a similar syntax to Svelte `on:click`. Yew will continue to check
standard global event handlers and provide the respective `web_sys`
event.

For custom events the handler is registered for the event after `on:`
syntax, therefore `on:custard` will register a handler for `custard`
events. Custom event handlers will receive the `CustomEvent` type which
is a new type for `web_sys::Event` which is used to indicate that the
user is specifying a custom event handler. This should help prevent some
typo's as users expecting a specific type will get an error that Yew was
expecting the callback to be of type `Callback<CustomEvent>` where they
might have expected `Callback<MouseEvent>`.

The shorthand property assignment still works but the shorthand name
required is the name of the event and this has to be used in braces
after the listener prefix: `on:{click}`.
Removes stringy syntax to listener syntax ie: on:"click" - custom events
now have to be explicitly defined using the CustomEventHandler trait.

Added custom_event attribute macro to help reduce the boiler plate -
using this new approach the custom event can be defined once and used
wherever required (a nice benefit over the literal string syntax!)

Add example of using imported wasm_bindgen type as these implement
JsCast which means that can be easily wrapped in a NewType with the
custom_event macro for custom events.
`CustomEventHandler` trait has been changed to `StaticEvent` which better
reflects it's purpose - to statically describe an event.

`StaticEvent` can be implemented easily to the supported standard events which
allows for only one `Wrapper` and `Listener` implementation to be used to support
custom and standard events.

Added a doc comment for the lack of `web_sys::FormDataEvent` - this doc
comment for IDE users will be visible when using the type in the `html!`
macro.
Change to single Wrapper & Listener impl effected output.
Use new event listener syntax
Change `CustomEventHandler` to `StaticEvent`.
Updated the traits implemented by the custom_event macro.
Tweaked some wording and formatting.
Implementing Deref automatically prevents users from providing a more
strongly typed alternatives.

This will be common when wrapping `web_sys::CustomEvent`, as a user
could provide a `detail` method on the event which returns a specific
type and not just a JsValue.
@mc1098 mc1098 closed this Aug 28, 2021
@ranile
Copy link
Member

ranile commented Aug 28, 2021

Why close this?

@mc1098
Copy link
Contributor Author

mc1098 commented Aug 28, 2021

Why close this?

Didn't seem to get much interest and the on: listener syntax is probably a touch pre-emptive before (#1533 / #1779) is resolved.

#1542 added ListenerKind enum which blocks being able to register custom events using &'static str, I could add some other variant which takes the str or some other method for registering a custom listener but wasn't sure it was worth keeping this approach going.

Also as you suggested in the linked issue an API on NodeRef might be a better alternative considering the above and would probably require far less macro magic and wouldn't need the transmute that I sneaked in here :D

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

Successfully merging this pull request may close these issues.

No way to listen to custom events exposed by custom elements
2 participants