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

Two-way bindings between Webview and Rust Yew #135

Open
antebandov opened this issue Jan 23, 2020 · 19 comments
Open

Two-way bindings between Webview and Rust Yew #135

antebandov opened this issue Jan 23, 2020 · 19 comments

Comments

@antebandov
Copy link

How can i call the invoke_handler from a Yew application? I want to communicate between Yew and Webview. I already tried adding javascript to Yew through stdweb and then calling external.invoke("foo"). But this doesn't work.

@mbuscemi
Copy link

I have not yet tried stdweb. I went with wasm_bindgen. It compiles in Rust, but errors out in JS at runtime. This happens even when just including wasm_bindgen and not invoking any functions (source code). I'm curious if there's a suggested path forward for this.

@mbuscemi
Copy link

external.invoke definitely works via stdweb from Yew. I'll post an example soon.

@mbuscemi
Copy link

mbuscemi commented Apr 15, 2020

@ante-dev @Boscop Here is an example of successfully executing external.invoke from Rust Yew, which in turn activates the WebView layer.

I'm still trying to work out how to get messages going the other direction. One thing I tried was to inline stdweb js! macro into the view HTML. That did not compile. My next thought was to do a #[js_export] on a model function. That did not compile either, as self is apparently not allowed in JS exports. Not sure what to explore next.

@Boscop
Copy link
Owner

Boscop commented Apr 15, 2020

@mbuscemi In your frontend (in your App::create method), you can register an event handler on document for a custom event that sends a Msg to your app.
Then from the backend you execute js that dispatches this event on document.

https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
https://javascript.info/dispatch-events#bubbling-example

Let me know how it goes :)

@mbuscemi
Copy link

mbuscemi commented Apr 15, 2020

@Boscop If I use link.callback in App::create, does that translate to an event handler registered on the document, or is there a different syntax for that?

@mbuscemi
Copy link

It looks like virtual_dom::Listener::attach is probably what I want, but I can't find any examples on how to get access to a virtual dom element. https://docs.rs/yew-stdweb/0.14.0/yew_stdweb/virtual_dom/trait.Listener.html

@mbuscemi
Copy link

@Boscop Here is where I'm at this morning. I've set up a js! block in my App::create to do the document.addEventListener. Unfortunately, my callback isn't being passed in succesfully. I get this error:

error[E0277]: the trait bound `stdweb::private::Newtype<_, yew::callback::Callback<std::string::String>>: stdweb::private::JsSerializeOwned` is not satisfied
  --> src/lib.rs:42:21
   |
42 |           let value = js! {
   |  _____________________^
43 | |             var callback = @{set_file_callback};
44 | |             return document.addEventListener("set_file", content => alert(content));
45 | |         };
   | |_________^ the trait `stdweb::private::JsSerializeOwned` is not implemented for `stdweb::private::Newtype<_, yew::callback::Callback<std::string::String>>`
   |
   = help: the following implementations were found:
             <stdweb::private::Newtype<(stdweb::webcore::serialization::FunctionTag, ()), F> as stdweb::private::JsSerializeOwned>
             <stdweb::private::Newtype<(stdweb::webcore::serialization::FunctionTag, ()), std::option::Option<F>> as stdweb::private::JsSerializeOwned>
             <stdweb::private::Newtype<(stdweb::webcore::serialization::FunctionTag, ()), std::option::Option<stdweb::Mut<F>>> as stdweb::private::JsSerializeOwned>
             <stdweb::private::Newtype<(stdweb::webcore::serialization::FunctionTag, ()), std::option::Option<stdweb::Once<F>>> as stdweb::private::JsSerializeOwned>
           and 81 others
   = note: required by `stdweb::private::JsSerializeOwned::into_js_owned`
   = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

I found this article, but applying the ReferenceType derive to my Msg enum generates:

error: proc-macro derive panicked
  --> src/lib.rs:28:39
   |
28 | #[derive(Clone, Debug, PartialEq, Eq, ReferenceType)]
   |                                       ^^^^^^^^^^^^^
   |
   = help: message: Only tuple structures are supported!

So, presuming this is how I'm supposed to set up the event listener, I'm unclear how to proceed.

@mbuscemi
Copy link

I've arrived at a working example of bi-directional communication.

I explored using document and add_event_listener on the Rust side extensively. There doesn't seem to be an implementation of CustomEvent in stdweb, and so the type of the event to pass into add_event_listener proved to be an impassable hurdle.

However, I was able to get this working by setting up a js! block in my App::create function that registers the event on document (source). I modified my call from the Webview layer (source), and it all worked. I read on this ticket that I need to drop variables I register in js! blocks, so I added that to my App::destroy.

I'll check with the maintainer of stdweb if they have any interest in an implementation of CustomEvent. I'd be happy to contribute it, and it would make this example a lot less error prone. As it stands, a slight typo in a JS variable name leads to an obscure error.

That said, as per the topic of this ticket, two-way communication between Webview and Yew achieved.

@Boscop
Copy link
Owner

Boscop commented Apr 17, 2020

Nice!

Btw:

self.event_listener = js! { 
	var set_file_callback = @{js_set_file_callback}; 
	return event => set_file_callback(event.detail.contents); 
};

And dropping would be like this: https://github.com/Boscop/yew-geolocation/blob/63dec0618393eda60becf8978a212c854f0ed5be/src/lib.rs#L133-L158

So you could just store a Value in your model, that is a js object like { listener: event => set_file_callback(event.detail.contents), callback: set_file_callback } (returned by that js block that registers the listener) so this contains everything you need to clean up.

@mbuscemi
Copy link

Good to know. I'm curious, as I had assumed that all js! blocks would be contained within the same JS scope, so a var in one would be accessible to all the others. Apparently that's not the case?

My mind had already being going down a similar road as to your suggestion about the model. Mostly because (having already written large Elm apps), I'm thinking about how I want to organize the code when I have twenty or thirty or more of these communication points between Webview and Yew. I'll want a way to run through a vector or slice of them and initialize/destroy them all.

By the way, I experimented with loading up a file containing double quotes, and it loaded up just fine under the current implementation. Perhaps format! is taking care of that?

Thanks for your help!

@Boscop
Copy link
Owner

Boscop commented Apr 17, 2020

I'm curious, as I had assumed that all js! blocks would be contained within the same JS scope, so a var in one would be accessible to all the others. Apparently that's not the case?

Calling js!{} is like calling eval right then and there, it can't reference symbols from other js!{} blocks, unless those defined global vars and were called before (or returned values into Rust that you then pass into the other js block).

I experimented with loading up a file containing double quotes, and it loaded up just fine under the current implementation. Perhaps format! is taking care of that?

It's because you're putting single quotes around the contents. But it would fail with single quotes in contents..

@mbuscemi
Copy link

@Boscop How does this look for an implementation of cleaning up the callbacks?

Also, you were right about the strings. The OpenFile event was failing on files containing single quote characters. I fixed that, too.

@Boscop
Copy link
Owner

Boscop commented Apr 18, 2020

@mbuscemi Yeah it makes sense to factor the event stuff out..
But if I'm not completely mistaken: Each time you bring a rust closure into a js!{} scope it will allocate on the js side, so to be able to .drop() the right one, you need to keep a reference to it around (via a Value like in yew-geolocation). As it is now, since you're bringing the closure from rust to js in Event::destroy a second time, you're only dropping that instance that just got allocated in destroy.
What I'd do here is:

            js! {
                var callback = @{js_callback};
                var name = @{name};
                var listener = event => callback(event.detail);
                document.addEventListener(name, listener);
                return {
                    callback: callback,
                    name: name,
                    listener: listener,
                };
            };

And then in Event::destroy, first call document.removeEventListener(handle.name, handle.listener); and then handle.callback.drop();

And some minor things:

  • On the backend I'd use serde_json::to_string(&contents) instead of rustc_serialize because that's been deprecated in favor of serde.
  • I'd use Into<Message> instead of this new Detail trait.
  • I'd use #[derive(TypeName)] on each event type and in Event::new I'd require D: Into<Message> + TypeName instead of this name: String arg, to be more type-safe (no way to use an event type with the wrong name).
  • I'd put all command types (currently SetFile and SetProjectPath) in an api crate that is shared between backend and frontend, and instead of duplicating the code to raise an event in a separate function for each command, I'd just have one function like pub fn send_command(webview: &mut WebView<()>, cmd: &impl Serialize). (And then derive Serialize and Deserialize on all command types (using serde).)

@mbuscemi
Copy link

mbuscemi commented Apr 20, 2020

@Boscop Awesome suggestions!

  • So, I'm pretty sure I was dropping the callback already. I saved it out a field on my event, which got saved to my Yew model, which I then used to invoke destroy. Not sure what to do differently in that regard. I did take your advice about also calling removeEventListener and saving a handle to the listener and event name.
  • I removed my dependency on rustc_serialize completely. Thanks for the heads up.
  • I really liked the idea of sharing code between the two layers. I got this going productively early this weekend with the DiskEntry struct. Very cool to make changes in one place have both layers interoperate seamlessly. I've just moved my Event and Message structs over to the shared library, and next I'm going to work on adjusting rpc.rs to call into a generic method on Event, which will remove the duplication on the backend as well.
  • I like my custom event names, but I agree that having them live at the root of the Yew layer isn't quite right. I made a constant on the Detail trait to store this, which moves the information more appropriately into the Event module, where it can now be shared between the frontend and backend. :)

@Boscop
Copy link
Owner

Boscop commented Apr 20, 2020

So, I'm pretty sure I was dropping the callback already. I saved it out a field on my event, which got saved to my Yew model, which I then used to invoke destroy.

Ah right, you had callback: Value in the model. For some reason I misread and thought you were storing the rust closure..

@mbuscemi
Copy link

Putting this here for anyone who might find this in the future: https://github.com/mbuscemi/webview-yew-minimal

@hobofan
Copy link

hobofan commented May 3, 2020

I also took a stab at it (taking a lot of inspiration from the discussed approach): https://github.com/hobofan/yew_webview_bridge (also available on crates.io)

A few notable points:

  • On the yew-side, I packaged the core logic into a service, which I think is a bit more of a idiomatic approach for yew
  • How the user wants to handle their "shared" message types is left up to them
  • The communication logic internally uses a Message wrapper type which carries a subscription_id and message_id, which allows for association of responses to their initial messages (this is handled by the service)
  • When you use .send_message from yew, it returns a MessageFuture with the response from web-view (which can be used to trigger a component update with the included send_future helper).

@ohmree
Copy link

ohmree commented Dec 25, 2020

I'd also like this.

Sadly I'm using seed and not yew so the premade solution won't work for me.

And while I was prepared to build a custom local storage backend (since localStorage doesn't work in data urls, which my embedded single page app uses) I don't really feel like delving into browser apis, seed's abstractions and where they intersect to build something that works :/

It'd be really great if the bind function from upstream could be implemented here, since at the moment there's no way for me to read stored values from my web app when it's embedded in a native wrapper and not running in a browser.

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

No branches or pull requests

5 participants