diff --git a/README.md b/README.md index 04d01dead..641f5a8b8 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,14 @@ +
+

+ English + | + 中文 +

+
+
Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust. @@ -108,7 +116,7 @@ cargo run --example EXAMPLE | [![File Explorer](https://github.com/DioxusLabs/example-projects/raw/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![E-commerce Example](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) | -See the awesome-dioxus page for a curated list of content in the Dioxus Ecosystem. +See the [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) page for a curated list of content in the Dioxus Ecosystem. ## Why Dioxus and why Rust? @@ -151,7 +159,7 @@ You shouldn't use Dioxus if: ### Comparison with other Rust UI frameworks Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**. -- [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead of React-hooks, no borrowed props, no SSR. +- [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead of React-hooks, no borrowed props, supports SSR (no hydration). - [Percy](https://github.com/chinedufn/percy): Supports SSR but less emphasis on state management and event handling. - [Sycamore](https://github.com/sycamore-rs/sycamore): VDOM-less using fine-grained reactivity, but lacking in ergonomics. - [Dominator](https://github.com/Pauan/rust-dominator): Signal-based zero-cost alternative, less emphasis on community and docs. diff --git a/docs/guide/book.toml b/docs/guide/book.toml index 48ead3936..eabbe6f2b 100644 --- a/docs/guide/book.toml +++ b/docs/guide/book.toml @@ -10,8 +10,8 @@ edition = "2018" [output.html] mathjax-support = true site-url = "/mdBook/" -git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide" -edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}" +git-repository-url = "https://github.com/DioxusLabs/dioxus/edit/master/docs/guide" +edit-url-template = "https://github.com/DioxusLabs/dioxus/edit/master/docs/guide/{path}" [output.html.playground] editable = true diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 3b35ae174..b2f8429ee 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -61,7 +61,7 @@ fn app(cx: Scope) -> Element { #[inline_props] fn Breed(cx: Scope, breed: String) -> Element { - #[derive(serde::Deserialize)] + #[derive(serde::Deserialize, Debug)] struct DogApi { message: String, } @@ -72,6 +72,12 @@ fn Breed(cx: Scope, breed: String) -> Element { reqwest::get(endpoint).await.unwrap().json::().await }); + let breed_name = use_state(&cx, || breed.clone()); + if breed_name.get() != breed { + breed_name.set(breed.clone()); + fut.restart(); + } + cx.render(match fut.value() { Some(Ok(resp)) => rsx! { button { diff --git a/examples/link.rs b/examples/link.rs new file mode 100644 index 000000000..41b010e8d --- /dev/null +++ b/examples/link.rs @@ -0,0 +1,28 @@ +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! ( + div { + p { + a { + href: "http://dioxuslabs.com/", + "default link" + } + } + p { + a { + href: "http://dioxuslabs.com/", + prevent_default: "onclick", + onclick: |_| { + println!("Hello Dioxus"); + }, + "custom event link", + } + } + } + )) +} diff --git a/examples/optional_props.rs b/examples/optional_props.rs index 47ccbeac0..9485a2aeb 100644 --- a/examples/optional_props.rs +++ b/examples/optional_props.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + //! Example: README.md showcase //! //! The example from the README.md. @@ -14,6 +16,7 @@ fn app(cx: Scope) -> Element { a: "asd".to_string(), c: Some("asd".to_string()), d: "asd".to_string(), + e: "asd".to_string(), } }) } @@ -30,8 +33,19 @@ struct ButtonProps { #[props(default, strip_option)] d: Option, + + #[props(optional)] + e: Option, } fn Button(cx: Scope) -> Element { - todo!() + cx.render(rsx! { + button { + "{cx.props.a}" + "{cx.props.b:?}" + "{cx.props.c:?}" + "{cx.props.d:?}" + "{cx.props.e:?}" + } + }) } diff --git a/examples/router.rs b/examples/router.rs index bd6d51881..121514423 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -2,6 +2,7 @@ use dioxus::prelude::*; use dioxus::router::{Link, Route, Router}; +use serde::Deserialize; fn main() { dioxus::desktop::launch(app); @@ -30,7 +31,7 @@ fn app(cx: Scope) -> Element { } fn BlogPost(cx: Scope) -> Element { - let post = dioxus::router::use_route(&cx).last_segment()?; + let post = dioxus::router::use_route(&cx).last_segment(); cx.render(rsx! { div { @@ -40,9 +41,14 @@ fn BlogPost(cx: Scope) -> Element { }) } +#[derive(Deserialize)] +struct Query { + bold: bool, +} + fn User(cx: Scope) -> Element { - let post = dioxus::router::use_route(&cx).last_segment()?; - let bold = dioxus::router::use_route(&cx).param::("bold"); + let post = dioxus::router::use_route(&cx).last_segment(); + let query = dioxus::router::use_route(&cx).query::(); cx.render(rsx! { div { diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md new file mode 100644 index 000000000..614774595 --- /dev/null +++ b/notes/README/ZH_CN.md @@ -0,0 +1,168 @@ +
+

🌗🚀 Dioxus

+

+ Frontend that scales. +

+
+ +
+ + + Crates.io version + + + + Download + + + + docs.rs docs + + + + CI status + +
+ +
+ + + Awesome Page + + + + Awesome Page + +
+ + +
+

+ 官网 + | + 手册 + | + 示例 +

+
+ +
+

+ English + | + 中文 +

+
+ + +
+ +Dioxus 是一个可移植、高性能的框架,用于在 Rust 中构建跨平台的用户界面。 + +```rust +fn app(cx: Scope) -> Element { + let mut count = use_state(&cx, || 0); + + cx.render(rsx!( + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + )) +} +``` + +Dioxus 可用于制作 网页程序、桌面应用、静态站点、移动端应用。 + +Dioxus 为不同的平台都提供了很好的开发文档。 + +如果你会使用 React ,那 Dioxus 对你来说会很简单。 + +### 项目特点: +- 对桌面应用的原生支持。 +- 强大的状态管理工具。 +- 支持所有 HTML 标签,监听器和事件。 +- 超高的内存使用率,稳定的组件分配器。 +- 多通道异步调度器,超强的异步支持。 +- 更多信息请查阅: [版本发布文档](https://dioxuslabs.com/blog/introducing-dioxus/). + +### 示例 + +本项目中的所有例子都是 `桌面应用` 程序,请使用 `cargo run --example XYZ` 运行这些例子。 + +``` +cargo run --example EXAMPLE +``` + +## 进入学习 + + + + + + + + + + +
教程网页端桌面端SSR移动端状态管理
+ + +## Dioxus 项目 + +| 文件浏览器 (桌面应用) | WiFi 扫描器 (桌面应用) | Todo管理 (所有平台) | 商城系统 (SSR/liveview) | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [![File Explorer](https://github.com/DioxusLabs/example-projects/raw/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![E-commerce Example](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) | + + +查看 [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) 查看更多有趣(~~NiuBi~~)的项目! + +## 为什么使用 Dioxus 和 Rust ? + +TypeScript 是一个不错的 JavaScript 拓展集,但它仍然算是 JavaScript。 + +TS 代码运行效率不高,而且有大量的配置项。 + +相比之下,Dioxus 使用 Rust 编写将大大的提高效能。 + +使用 Rust 开发,我们能获得: + +- 静态类型支持。 +- 变量默认不变性。 +- 简单直观的模块系统。 +- 内部集成的文档系统。 +- 先进的模式匹配系统。 +- 简洁、高效、强大的迭代器。 +- 内置的 单元测试 / 集成测试。 +- 优秀的异常处理系统。 +- 强大且健全的标准库。 +- 灵活的 `宏` 系统。 +- 使用 `crates.io` 管理包。 + +Dioxus 能为开发者提供的: + +- 安全使用数据结构。 +- 安全的错误处理结果。 +- 拥有原生移动端的性能。 +- 直接访问系统的IO层。 + +Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让团队能在更短的时间内做出强大功能。 + +### 不建议使用 Dioxus 的情况? + +您不该在这些情况下使用 Dioxus : + +- 您不喜欢类似 React 的开发风格。 +- 您需要一个 `no-std` 的渲染器。 +- 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。 +- 您需要一个 `Send + Sync` UI 解决方案(目前不支持)。 + + +## 协议 + +这个项目使用 [MIT 协议]. + +[MIT 协议]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index f5443272b..d569e711a 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -364,6 +364,14 @@ mod field_info { Some(syn::parse(quote!(Default::default()).into()).unwrap()); Ok(()) } + + "optional" => { + self.default = + Some(syn::parse(quote!(Default::default()).into()).unwrap()); + self.strip_option = true; + Ok(()) + } + _ => { macro_rules! handle_fields { ( $( $flag:expr, $field:ident, $already:expr; )* ) => { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index d662b4998..80de92917 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -351,31 +351,25 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// } /// /// ``` +#[derive(Default)] pub struct EventHandler<'bump, T = ()> { - pub callback: &'bump RefCell>>, + pub callback: RefCell>>, } impl EventHandler<'_, T> { + /// Call this event handler with the appropriate event type pub fn call(&self, event: T) { if let Some(callback) = self.callback.borrow_mut().as_mut() { callback(event); } } + /// Forcibly drop the internal handler callback, releasing memory pub fn release(&self) { self.callback.replace(None); } } -impl Copy for EventHandler<'_, T> {} -impl Clone for EventHandler<'_, T> { - fn clone(&self) -> Self { - Self { - callback: self.callback, - } - } -} - /// Virtual Components for custom user-defined components /// Only supports the functional syntax pub struct VComponent<'src> { @@ -677,7 +671,7 @@ impl<'a> NodeFactory<'a> { pub fn event_handler(self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> { let handler: &mut dyn FnMut(T) = self.bump.alloc(f); let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; - let callback = self.bump.alloc(RefCell::new(Some(caller))); + let callback = RefCell::new(Some(caller)); EventHandler { callback } } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 65511470e..5b8f527c5 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -198,12 +198,9 @@ impl ScopeArena { // run the hooks (which hold an &mut Reference) // recursively call ensure_drop_safety on all children items.borrowed_props.drain(..).for_each(|comp| { - let scope_id = comp - .scope - .get() - .expect("VComponents should be associated with a valid Scope"); - - self.ensure_drop_safety(scope_id); + if let Some(scope_id) = comp.scope.get() { + self.ensure_drop_safety(scope_id); + } drop(comp.props.take()); }); diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 8a0e5e8f5..eb9425c1f 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -29,6 +29,7 @@ tokio = { version = "1.12.0", features = [ ], optional = true, default-features = false } dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"} dioxus-html = { path = "../html", features = ["serialize"], version ="^0.1.4"} +webbrowser = "0.5.5" [features] default = ["tokio_runtime"] diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 02e1dca82..802e7441c 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -187,6 +187,20 @@ pub fn launch_with_props( is_ready.store(true, std::sync::atomic::Ordering::Relaxed); let _ = proxy.send_event(UserWindowEvent::Update); } + "browser_open" => { + let data = req.params.unwrap(); + log::trace!("Open browser: {:?}", data); + if let Some(arr) = data.as_array() { + if let Some(temp) = arr[0].as_object() { + if temp.contains_key("href") { + let url = temp.get("href").unwrap().as_str().unwrap(); + if let Err(e) = webbrowser::open(url) { + log::error!("Open Browser error: {:?}", e); + } + } + } + } + } _ => {} } None diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 634696bf4..6a1b671ab 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -31,6 +31,9 @@ pub struct LinkProps<'a> { #[props(default, strip_option)] id: Option<&'a str>, + #[props(default, strip_option)] + title: Option<&'a str>, + children: Element<'a>, #[props(default)] @@ -38,17 +41,25 @@ pub struct LinkProps<'a> { } pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { - let service = cx.consume_context::()?; - cx.render(rsx! { - a { - href: "{cx.props.to}", - class: format_args!("{}", cx.props.class.unwrap_or("")), - id: format_args!("{}", cx.props.id.unwrap_or("")), + log::debug!("render Link to {}", cx.props.to); + if let Some(service) = cx.consume_context::() { + return cx.render(rsx! { + a { + href: "{cx.props.to}", + class: format_args!("{}", cx.props.class.unwrap_or("")), + id: format_args!("{}", cx.props.id.unwrap_or("")), + title: format_args!("{}", cx.props.title.unwrap_or("")), - prevent_default: "onclick", - onclick: move |_| service.push_route(cx.props.to), + prevent_default: "onclick", + onclick: move |_| service.push_route(cx.props.to), - &cx.props.children - } - }) + &cx.props.children + } + }); + } + log::warn!( + "Attempted to create a Link to {} outside of a Router context", + cx.props.to, + ); + None } diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index 2d9a270b8..129953eea 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -30,6 +30,7 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { Some(ctx) => ctx.total_route.to_string(), None => cx.props.to.to_string(), }; + log::trace!("total route for {} is {}", cx.props.to, total_route); // provide our route context let route_context = cx.provide_context(RouteContext { diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 85f0a62a3..3d9f0c83a 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -12,7 +12,7 @@ pub struct RouterProps<'a> { children: Element<'a>, #[props(default, strip_option)] - onchange: Option<&'a Fn(&'a str)>, + onchange: Option<&'a dyn Fn(&'a str)>, } #[allow(non_snake_case)] diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 8307ed1f0..97dd6c048 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,30 +1,83 @@ use dioxus_core::ScopeState; +use gloo::history::{HistoryResult, Location}; +use serde::de::DeserializeOwned; +use std::{rc::Rc, str::FromStr}; -pub struct UseRoute<'a> { - cur_route: String, - cx: &'a ScopeState, +use crate::RouterService; + +/// This struct provides is a wrapper around the internal router +/// implementation, with methods for getting information about the current +/// route. +pub struct UseRoute { + router: Rc, } -impl<'a> UseRoute<'a> { - /// Parse the query part of the URL - pub fn param(&self, param: &str) -> Option<&T> { - todo!() +impl UseRoute { + /// This method simply calls the [`Location::query`] method. + pub fn query(&self) -> HistoryResult + where + T: DeserializeOwned, + { + self.current_location().query::() } - pub fn nth_segment(&self, n: usize) -> Option<&str> { - todo!() + /// Returns the nth segment in the path. Paths that end with a slash have + /// the slash removed before determining the segments. If the path has + /// fewer segments than `n` then this method returns `None`. + pub fn nth_segment(&self, n: usize) -> Option { + let mut segments = self.path_segments(); + let len = segments.len(); + if len - 1 < n { + return None; + } + Some(segments.remove(n)) } - pub fn last_segment(&self) -> Option<&'a str> { - todo!() + /// Returns the last segment in the path. Paths that end with a slash have + /// the slash removed before determining the segments. The root path, `/`, + /// will return an empty string. + pub fn last_segment(&self) -> String { + let mut segments = self.path_segments(); + segments.remove(segments.len() - 1) } - /// Parse the segments of the URL, using named parameters (defined in your router) - pub fn segment(&self, name: &str) -> Option<&T> { - todo!() + /// Get the named parameter from the path, as defined in your router. The + /// value will be parsed into the type specified by `T` by calling + /// `value.parse::()`. This method returns `None` if the named + /// parameter does not exist in the current path. + pub fn segment(&self, name: &str) -> Option> + where + T: FromStr, + { + self.router + .current_path_params() + .get(name) + .and_then(|v| Some(v.parse::())) + } + + /// Returns the [Location] for the current route. + pub fn current_location(&self) -> Location { + self.router.current_location() + } + + fn path_segments(&self) -> Vec { + let location = self.router.current_location(); + let path = location.path(); + if path == "/" { + return vec![String::new()]; + } + let stripped = &location.path()[1..]; + stripped.split('/').map(str::to_string).collect::>() } } -pub fn use_route<'a>(cx: &'a ScopeState) -> UseRoute<'a> { - todo!() +/// This hook provides access to information about the current location in the +/// context of a [`Router`]. If this function is called outside of a `Router` +/// component it will panic. +pub fn use_route(cx: &ScopeState) -> UseRoute { + let router = cx + .consume_context::() + .expect("Cannot call use_route outside the scope of a Router component") + .clone(); + UseRoute { router } } diff --git a/packages/router/src/platform/mod.rs b/packages/router/src/platform/mod.rs index bbb0e7c18..7afc1316a 100644 --- a/packages/router/src/platform/mod.rs +++ b/packages/router/src/platform/mod.rs @@ -1,5 +1,3 @@ -use url::Url; - pub trait RouterProvider { fn get_current_route(&self) -> String; fn subscribe_to_route_changes(&self, callback: Box); diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 7f069b6ba..00642414f 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,6 +1,6 @@ -use gloo::history::{BrowserHistory, History, HistoryListener}; +use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ - cell::{Cell, RefCell}, + cell::{Cell, Ref, RefCell}, collections::HashMap, rc::Rc, }; @@ -10,10 +10,9 @@ use dioxus_core::ScopeId; pub struct RouterService { pub(crate) regen_route: Rc, history: Rc>, - registerd_routes: RefCell, slots: Rc>>, - root_found: Rc>, - cur_root: RefCell, + root_found: Rc>>, + cur_path_params: Rc>>, listener: HistoryListener, } @@ -40,48 +39,54 @@ impl RouterService { let _slots = slots.clone(); - let root_found = Rc::new(Cell::new(false)); + let root_found = Rc::new(Cell::new(None)); let regen = regen_route.clone(); let _root_found = root_found.clone(); let listener = history.listen(move || { - _root_found.set(false); + _root_found.set(None); // checking if the route is valid is cheap, so we do it - for (slot, _) in _slots.borrow_mut().iter().rev() { - log::trace!("regenerating slot {:?}", slot); + for (slot, root) in _slots.borrow_mut().iter().rev() { + log::trace!("regenerating slot {:?} for root '{}'", slot, root); regen(*slot); } }); Self { - registerd_routes: RefCell::new(RouteSlot::Routes { - partial: String::from("/"), - total: String::from("/"), - rest: Vec::new(), - }), root_found, history: Rc::new(RefCell::new(history)), regen_route, slots, - cur_root: RefCell::new(path.to_string()), + cur_path_params: Rc::new(RefCell::new(HashMap::new())), listener, } } pub fn push_route(&self, route: &str) { + log::trace!("Pushing route: {}", route); self.history.borrow_mut().push(route); } pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) { - self.slots.borrow_mut().push((scope, route)); + let clean = clean_route(route); + log::trace!("Registered route '{}' with scope id {:?}", clean, scope); + self.slots.borrow_mut().push((scope, clean)); } pub fn should_render(&self, scope: ScopeId) -> bool { - if self.root_found.get() { + log::trace!("Should render scope id {:?}?", scope); + if let Some(root_id) = self.root_found.get() { + log::trace!(" we already found a root with scope id {:?}", root_id); + if root_id == scope { + log::trace!(" yes - it's a match"); + return true; + } + log::trace!(" no - it's not a match"); return false; } let location = self.history.borrow().location(); let path = location.path(); + log::trace!(" current path is '{}'", path); let roots = self.slots.borrow(); @@ -89,15 +94,24 @@ impl RouterService { // fallback logic match root { - Some((_id, route)) => { - if route == path { - self.root_found.set(true); + Some((id, route)) => { + log::trace!( + " matched given scope id {:?} with route root '{}'", + scope, + route, + ); + if let Some(params) = route_matches_path(route, path) { + log::trace!(" and it matches the current path '{}'", path); + self.root_found.set(Some(*id)); + *self.cur_path_params.borrow_mut() = params; true } else { if route == "" { - self.root_found.set(true); + log::trace!(" and the route is the root, so we will use that without a better match"); + self.root_found.set(Some(*id)); true } else { + log::trace!(" and the route '{}' is not the root nor does it match the current path", route); false } } @@ -105,6 +119,70 @@ impl RouterService { None => false, } } + + pub fn current_location(&self) -> Location { + self.history.borrow().location().clone() + } + + pub fn current_path_params(&self) -> Ref> { + self.cur_path_params.borrow() + } +} + +fn clean_route(route: String) -> String { + if route.as_str() == "/" { + return route; + } + route.trim_end_matches('/').to_string() +} + +fn clean_path(path: &str) -> &str { + if path == "/" { + return path; + } + path.trim_end_matches('/') +} + +fn route_matches_path(route: &str, path: &str) -> Option> { + let route_pieces = route.split('/').collect::>(); + let path_pieces = clean_path(path).split('/').collect::>(); + + log::trace!( + " checking route pieces {:?} vs path pieces {:?}", + route_pieces, + path_pieces, + ); + + if route_pieces.len() != path_pieces.len() { + log::trace!(" the routes are different lengths"); + return None; + } + + let mut matches = HashMap::new(); + for (i, r) in route_pieces.iter().enumerate() { + log::trace!(" checking route piece '{}' vs path", r); + // If this is a parameter then it matches as long as there's + // _any_thing in that spot in the path. + if r.starts_with(':') { + log::trace!( + " route piece '{}' starts with a colon so it matches anything", + r, + ); + let param = &r[1..]; + matches.insert(param.to_string(), path_pieces[i].to_string()); + continue; + } + log::trace!( + " route piece '{}' must be an exact match for path piece '{}'", + r, + path_pieces[i], + ); + if path_pieces[i] != *r { + return None; + } + } + + Some(matches) } pub struct RouterCfg { diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index cc4202886..9b0c2ebfb 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -43,13 +43,7 @@ extern "C" { pub fn CreatePlaceholder(this: &Interpreter, root: u64); #[wasm_bindgen(method)] - pub fn NewEventListener( - this: &Interpreter, - name: &str, - scope: usize, - root: u64, - handler: &Function, - ); + pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function); #[wasm_bindgen(method)] pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index c4bd6a6e5..b0b4dd035 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -21,7 +21,7 @@ pub struct WebsysDom { pub(crate) root: Element, - handler: Closure, + pub handler: Closure, } impl WebsysDom { @@ -74,15 +74,12 @@ impl WebsysDom { } DomEdit::CreatePlaceholder { root } => self.interpreter.CreatePlaceholder(root), DomEdit::NewEventListener { - event_name, - scope, - root, + event_name, root, .. } => { - // let handler: &Function = self.handler.as_ref().unchecked_ref(); - self.interpreter - .NewEventListener(event_name, scope.0, root, handler); + self.interpreter.NewEventListener(event_name, root, handler); } + DomEdit::RemoveEventListener { root, event } => { self.interpreter.RemoveEventListener(root, event) } @@ -104,6 +101,7 @@ impl WebsysDom { pub struct DioxusWebsysEvent(web_sys::Event); // safety: currently the web is not multithreaded and our VirtualDom exists on the same thread +#[allow(clippy::non_send_fields_in_send_ty)] unsafe impl Send for DioxusWebsysEvent {} unsafe impl Sync for DioxusWebsysEvent {} diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index b62cba753..387baab59 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -121,6 +121,14 @@ impl WebsysDom { self.rehydrate_single(nodes, place, dom, child, &mut last_node_was_text)?; } + for listener in vel.listeners { + self.interpreter.NewEventListener( + listener.event, + listener.mounted_node.get().unwrap().as_u64(), + self.handler.as_ref().unchecked_ref(), + ); + } + place.pop(); nodes.pop();