diff --git a/README.md b/README.md index d1ec98019..65cd08263 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ So... Dioxus is great, but why won't it work for me? ## Contributing - Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing). - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). -- Join the discord and ask questions! +- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions! diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 3189086b4..ae1bd2109 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -18,7 +18,7 @@ use crate::{innerlude::VNode, ScopeState}; /// A concrete type provider for closures that build [`VNode`] structures. /// -/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over +/// This struct wraps lazy structs that build [`VNode`] trees. Normally, we cannot perform a blanket implementation over /// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching. /// /// diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index cbd7b75f5..0f35f4ae6 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -38,6 +38,10 @@ salvo = { version = "0.44.1", optional = true, features = ["ws"] } once_cell = "1.17.1" async-trait = "0.1.71" +# rocket +rocket = { version = "0.5.0", optional = true } +rocket_ws = { version = "0.1.0", optional = true } + # actix is ... complicated? # actix-files = { version = "0.6.2", optional = true } # actix-web = { version = "4.2.1", optional = true } @@ -49,13 +53,16 @@ tokio = { workspace = true, features = ["full"] } dioxus = { workspace = true } warp = "0.3.3" axum = { version = "0.6.1", features = ["ws"] } -salvo = { version = "0.44.1", features = ["affix", "ws"] } +# salvo = { version = "0.44.1", features = ["affix", "ws"] } +rocket = "0.5.0" +rocket_ws = "0.1.0" tower = "0.4.13" [features] default = ["hot-reload"] # actix = ["actix-files", "actix-web", "actix-ws"] hot-reload = ["dioxus-hot-reload"] +rocket = ["dep:rocket", "dep:rocket_ws"] [[example]] name = "axum" @@ -68,3 +75,7 @@ required-features = ["salvo"] [[example]] name = "warp" required-features = ["warp"] + +[[example]] +name = "rocket" +required-features = ["rocket"] diff --git a/packages/liveview/README.md b/packages/liveview/README.md index cbae3c53a..13af3c36f 100644 --- a/packages/liveview/README.md +++ b/packages/liveview/README.md @@ -28,6 +28,7 @@ The current backend frameworks supported include: - Axum - Warp - Salvo +- Rocket Dioxus-LiveView exports some primitives to wire up an app into an existing backend framework. diff --git a/packages/liveview/examples/rocket.rs b/packages/liveview/examples/rocket.rs new file mode 100644 index 000000000..8256db7f8 --- /dev/null +++ b/packages/liveview/examples/rocket.rs @@ -0,0 +1,76 @@ +#[macro_use] +extern crate rocket; + +use dioxus::prelude::*; +use dioxus_liveview::LiveViewPool; +use rocket::response::content::RawHtml; +use rocket::{Config, Rocket, State}; +use rocket_ws::{Channel, WebSocket}; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello Rocket! {num}" + button { onclick: move |_| num += 1, "Increment" } + } + }) +} + +fn index_page_with_glue(glue: &str) -> RawHtml { + RawHtml(format!( + r#" + + + Dioxus LiveView with Rocket +
+ {glue} + + "#, + glue = glue + )) +} + +#[get("/")] +async fn index(config: &Config) -> RawHtml { + index_page_with_glue(&dioxus_liveview::interpreter_glue(&format!( + "ws://{addr}:{port}/ws", + addr = config.address, + port = config.port, + ))) +} + +#[get("/as-path")] +async fn as_path() -> RawHtml { + index_page_with_glue(&dioxus_liveview::interpreter_glue("/ws")) +} + +#[get("/ws")] +fn ws(ws: WebSocket, pool: &State) -> Channel<'static> { + let pool = pool.inner().to_owned(); + + ws.channel(move |stream| { + Box::pin(async move { + let _ = pool + .launch(dioxus_liveview::rocket_socket(stream), app) + .await; + Ok(()) + }) + }) +} + +#[tokio::main] +async fn main() { + let view = dioxus_liveview::LiveViewPool::new(); + + Rocket::build() + .manage(view) + .mount("/", routes![index, as_path, ws]) + .ignite() + .await + .expect("Failed to ignite rocket") + .launch() + .await + .expect("Failed to launch rocket"); +} diff --git a/packages/liveview/src/adapters/rocket_adapter.rs b/packages/liveview/src/adapters/rocket_adapter.rs new file mode 100644 index 000000000..8a9840bf2 --- /dev/null +++ b/packages/liveview/src/adapters/rocket_adapter.rs @@ -0,0 +1,25 @@ +use crate::{LiveViewError, LiveViewSocket}; +use rocket::futures::{SinkExt, StreamExt}; +use rocket_ws::{result::Error, stream::DuplexStream, Message}; + +/// Convert a rocket websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the rocket web framework +pub fn rocket_socket(stream: DuplexStream) -> impl LiveViewSocket { + stream + .map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) +} + +fn transform_rx(message: Result) -> Result, LiveViewError> { + message + .map_err(|_| LiveViewError::SendingFailed)? + .into_text() + .map(|s| s.into_bytes()) + .map_err(|_| LiveViewError::SendingFailed) +} + +async fn transform_tx(message: Vec) -> Result { + Ok(Message::Text(String::from_utf8_lossy(&message).to_string())) +} diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 6a2768fd5..40d6186dc 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -18,6 +18,11 @@ pub mod adapters { #[cfg(feature = "salvo")] pub use salvo_adapter::*; + + #[cfg(feature = "rocket")] + pub mod rocket_adapter; + #[cfg(feature = "rocket")] + pub use rocket_adapter::*; } pub use adapters::*; diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 9b0d9e7bb..31c7f33f5 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -106,7 +106,10 @@ impl Renderer { if self.pre_render { if let AttributeValue::Listener(_) = &attr.value { - accumulated_listeners.push(attr.name); + // The onmounted event doesn't need a DOM listener + if attr.name != "onmounted" { + accumulated_listeners.push(attr.name); + } } } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index bcf2065ec..c535d961b 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -255,17 +255,21 @@ impl WebsysDom { i.flush(); for id in to_mount { - let node = get_node(id.0 as u32); - if let Some(element) = node.dyn_ref::() { - let data: MountedData = element.into(); - let data = Rc::new(data); - let _ = self.event_channel.unbounded_send(UiEvent { - name: "mounted".to_string(), - bubbles: false, - element: id, - data, - }); - } + self.send_mount_event(id); + } + } + + pub(crate) fn send_mount_event(&self, id: ElementId) { + let node = get_node(id.0 as u32); + if let Some(element) = node.dyn_ref::() { + let data: MountedData = element.into(); + let data = Rc::new(data); + let _ = self.event_channel.unbounded_send(UiEvent { + name: "mounted".to_string(), + bubbles: false, + element: id, + data, + }); } } } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 3d8ecf03f..90959a517 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -253,13 +253,6 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop pin_mut!(work); #[cfg(all(feature = "hot_reload", debug_assertions))] - // futures_util::select! { - // _ = work => (None, None), - // new_template = hotreload_rx.next() => { - // (None, new_template) - // } - // evt = rx.next() => - // } match select(work, select(hotreload_rx.next(), rx.next())).await { Either::Left((_, _)) => (None, None), Either::Right((Either::Left((new_template, _)), _)) => (None, new_template), diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 9fa20e56a..4905d1e24 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -14,12 +14,17 @@ impl WebsysDom { pub fn rehydrate(&mut self, dom: &VirtualDom) -> Result<(), RehydrationError> { let root_scope = dom.base_scope(); let mut ids = Vec::new(); + let mut to_mount = Vec::new(); // Recursively rehydrate the dom from the VirtualDom - self.rehydrate_scope(root_scope, dom, &mut ids)?; + self.rehydrate_scope(root_scope, dom, &mut ids, &mut to_mount)?; dioxus_interpreter_js::hydrate(ids); + for id in to_mount { + self.send_mount_event(id); + } + Ok(()) } @@ -28,12 +33,13 @@ impl WebsysDom { scope: &ScopeState, dom: &VirtualDom, ids: &mut Vec, + to_mount: &mut Vec, ) -> Result<(), RehydrationError> { let vnode = match scope.root_node() { dioxus_core::RenderReturn::Ready(ready) => ready, _ => return Err(VNodeNotInitialized), }; - self.rehydrate_vnode(dom, vnode, ids) + self.rehydrate_vnode(dom, vnode, ids, to_mount) } fn rehydrate_vnode( @@ -41,6 +47,7 @@ impl WebsysDom { dom: &VirtualDom, vnode: &VNode, ids: &mut Vec, + to_mount: &mut Vec, ) -> Result<(), RehydrationError> { for (i, root) in vnode.template.get().roots.iter().enumerate() { self.rehydrate_template_node( @@ -48,6 +55,7 @@ impl WebsysDom { vnode, root, ids, + to_mount, Some(*vnode.root_ids.borrow().get(i).ok_or(VNodeNotInitialized)?), )?; } @@ -60,6 +68,7 @@ impl WebsysDom { vnode: &VNode, node: &TemplateNode, ids: &mut Vec, + to_mount: &mut Vec, root_id: Option, ) -> Result<(), RehydrationError> { tracing::trace!("rehydrate template node: {:?}", node); @@ -73,6 +82,11 @@ impl WebsysDom { let attribute = &vnode.dynamic_attrs[*id]; let id = attribute.mounted_element(); mounted_id = Some(id); + if let dioxus_core::AttributeValue::Listener(_) = attribute.value { + if attribute.name == "onmounted" { + to_mount.push(id); + } + } } } if let Some(id) = mounted_id { @@ -80,12 +94,12 @@ impl WebsysDom { } if !children.is_empty() { for child in *children { - self.rehydrate_template_node(dom, vnode, child, ids, None)?; + self.rehydrate_template_node(dom, vnode, child, ids, to_mount, None)?; } } } TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => { - self.rehydrate_dynamic_node(dom, &vnode.dynamic_nodes[*id], ids)?; + self.rehydrate_dynamic_node(dom, &vnode.dynamic_nodes[*id], ids, to_mount)?; } _ => {} } @@ -97,6 +111,7 @@ impl WebsysDom { dom: &VirtualDom, dynamic: &DynamicNode, ids: &mut Vec, + to_mount: &mut Vec, ) -> Result<(), RehydrationError> { tracing::trace!("rehydrate dynamic node: {:?}", dynamic); match dynamic { @@ -108,11 +123,16 @@ impl WebsysDom { } dioxus_core::DynamicNode::Component(comp) => { let scope = comp.mounted_scope().ok_or(VNodeNotInitialized)?; - self.rehydrate_scope(dom.get_scope(scope).ok_or(VNodeNotInitialized)?, dom, ids)?; + self.rehydrate_scope( + dom.get_scope(scope).ok_or(VNodeNotInitialized)?, + dom, + ids, + to_mount, + )?; } dioxus_core::DynamicNode::Fragment(fragment) => { for vnode in *fragment { - self.rehydrate_vnode(dom, vnode, ids)?; + self.rehydrate_vnode(dom, vnode, ids, to_mount)?; } } }