Open antebandov opened 4 years ago
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.
external.invoke
definitely works via stdweb from Yew. I'll post an example soon.
@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.
@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 :)
@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?
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
@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.
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.
Nice!
Btw:
In general you probably want to serialize your string as json here: https://github.com/mbuscemi/hedgehog/blob/d6bd3b14ff9a058593954c71d6dfd4a9b4f1e2da/src/rpc.rs#L6 It could contain quotes etc.
I'm pretty sure this will not work:
https://github.com/mbuscemi/hedgehog/blob/d6bd3b14ff9a058593954c71d6dfd4a9b4f1e2da/frontend/src/lib.rs#L59
set_file_callback
is not in scope there.
Also, if you want to clean up properly, you should call removeEventListener
.
To be able to pass the listener, you'd need to store it in your model, like
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.
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!
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
..
@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.
@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:
serde_json::to_string(&contents)
instead of rustc_serialize
because that's been deprecated in favor of serde
.Into<Message>
instead of this new Detail
trait.#[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).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).)@Boscop Awesome suggestions!
removeEventListener
and saving a handle to the listener and event name.rustc_serialize
completely. Thanks for the heads up.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.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. :)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..
Putting this here for anyone who might find this in the future: https://github.com/mbuscemi/webview-yew-minimal
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:
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).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).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.
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.