From aa15d00b1e18850ca17e38ae29add12d6a946d6b Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Thu, 13 Jan 2022 12:16:17 +0800 Subject: [PATCH 01/33] docs: add `zh-cn` readme --- README.md | 8 ++ notes/README/ZH_CN.md | 165 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 notes/README/ZH_CN.md diff --git a/README.md b/README.md index 04d01dead..bb0ebcdc9 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,14 @@ +
+

+ English + | + Chinese +

+
+
Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust. diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md new file mode 100644 index 000000000..6e22092d5 --- /dev/null +++ b/notes/README/ZH_CN.md @@ -0,0 +1,165 @@ +
+

🌗🚀 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~~)的项目! + +## 为什么使用 Rust 开发 + +TypeScript 是一个不错的 JavaScript 拓展集,但它仍然算是 JavaScript。 + +TS 代码运行效率不高,而且有大量的配置项。 + +相比之下,Dioxus 使用用 Rust 编写将大大的提高效能。 + +使用 Rust 开发,我们能获得: + +- 所有库的静态类型支持。 +- 变量的默认不变性。 +- 一个简单且直观的模块管理系统。 +- 集成在内部的文档系统。 +- 先进的模式匹配系统。 +- 简洁、高效、可组合的迭代器。 +- 内联内置的 单元测试 / 集成测试。 +- 一流的异常处理系统。 +- 强大且健全的标准库。 +- 灵活的 `宏` 系统。 +- 使用 `crates.io` 管理包。 + +Dioxus 能为开发者提供的: + +- 正确且安全的使用不可变数据结构。 +- 安全的错误处理结果。 +- 拥有原生移动端的性能。 +- 直接访问系统层IO。 + +Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让你的前端团队更有信心在更短的时间内做出重大改变。 + +### 不建议使用 Dioxus 的情况? + +您不该在这些情况下使用 Dioxus : + +- 你不喜欢 React Hooks 的前端开发方案。 +- 你需要一个 `no-std` 的渲染器。 +- 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。 + + +## 协议 + +这个项目使用 [MIT 协议]. + +[MIT 协议]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE \ No newline at end of file From 55b84fd9bdce08119a39de909192035e8ae9d73e Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Thu, 13 Jan 2022 12:30:05 +0800 Subject: [PATCH 02/33] docs: commit docs --- README.md | 2 +- notes/README/ZH_CN.md | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bb0ebcdc9..588e1f873 100644 --- a/README.md +++ b/README.md @@ -116,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? diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 6e22092d5..eaf364c80 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -75,7 +75,9 @@ fn app(cx: Scope) -> Element { } ``` -Dioxus 可用于制作 网页程序、桌面应用、静态站点、移动端应用。Dioxus 为不同的平台都提供了很好的开发文档。 +Dioxus 可用于制作 网页程序、桌面应用、静态站点、移动端应用。 + +Dioxus 为不同的平台都提供了很好的开发文档。 如果你会使用 React ,那 Dioxus 对你来说会很简单。 @@ -89,13 +91,13 @@ Dioxus 可用于制作 网页程序、桌面应用、静态站点、移动端应 ### 示例 -所有例子都是 桌面应用 程序,请使用 `cargo run --example XYZ` 运行这些例子。 +本项目中的所有例子都是 `桌面应用` 程序,请使用 `cargo run --example XYZ` 运行这些例子。 ``` cargo run --example EXAMPLE ``` -## 开始学习 +## 进入学习 @@ -118,7 +120,7 @@ cargo run --example EXAMPLE 查看 [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) 查看更多有趣(~~NiuBi~~)的项目! -## 为什么使用 Rust 开发 +## 为什么使用 Dioxus 和 Rust ? TypeScript 是一个不错的 JavaScript 拓展集,但它仍然算是 JavaScript。 @@ -128,7 +130,7 @@ TS 代码运行效率不高,而且有大量的配置项。 使用 Rust 开发,我们能获得: -- 所有库的静态类型支持。 +- 库的静态类型支持。 - 变量的默认不变性。 - 一个简单且直观的模块管理系统。 - 集成在内部的文档系统。 @@ -145,7 +147,7 @@ Dioxus 能为开发者提供的: - 正确且安全的使用不可变数据结构。 - 安全的错误处理结果。 - 拥有原生移动端的性能。 -- 直接访问系统层IO。 +- 直接访问系统的IO层。 Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让你的前端团队更有信心在更短的时间内做出重大改变。 @@ -153,9 +155,10 @@ Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但 您不该在这些情况下使用 Dioxus : -- 你不喜欢 React Hooks 的前端开发方案。 -- 你需要一个 `no-std` 的渲染器。 +- 您不喜欢 React Hooks 的前端开发方案。 +- 您需要一个 `no-std` 的渲染器。 - 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。 +- 您需要一个 `Send + Sync` UI 解决方案(目前不支持)。 ## 协议 From 5d85eb643862e6ddd2b4cb7d3eb7e973750c3b89 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Thu, 13 Jan 2022 18:16:55 +0800 Subject: [PATCH 03/33] fix: edit `git-repository-url` --- docs/guide/book.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 75032da885d3e8d568531783b0f3005072077aa6 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Thu, 13 Jan 2022 22:30:18 +0800 Subject: [PATCH 04/33] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 588e1f873..b7d167711 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@

English | - Chinese + 中文

From 6bd7e983c4eadd31501d0e30d9558f1f684721ae Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 13 Jan 2022 11:25:18 -0500 Subject: [PATCH 05/33] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b7d167711..641f5a8b8 100644 --- a/README.md +++ b/README.md @@ -159,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. From e22ba5b1e52fe00e08e0b6ac0abae29b6068b8f0 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 8 Jan 2022 13:44:53 -0600 Subject: [PATCH 06/33] Add title prop to Link Every element can have a title, but it's particularly useful on links, so I think making it an explicit option is worthwhile. --- packages/router/src/components/link.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 634696bf4..dc807aa1a 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)] @@ -44,6 +47,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { 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), From d367e0f89f1b31484bde42efaf10c3531aed1d64 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 8 Jan 2022 13:53:45 -0600 Subject: [PATCH 07/33] Fix typo in RouterService struct's "registered_routes" field name This was "registerd_routes", missing an "e". --- packages/router/src/service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 7f069b6ba..ae7c1ff05 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -10,7 +10,7 @@ use dioxus_core::ScopeId; pub struct RouterService { pub(crate) regen_route: Rc, history: Rc>, - registerd_routes: RefCell, + registered_routes: RefCell, slots: Rc>>, root_found: Rc>, cur_root: RefCell, @@ -53,7 +53,7 @@ impl RouterService { }); Self { - registerd_routes: RefCell::new(RouteSlot::Routes { + registered_routes: RefCell::new(RouteSlot::Routes { partial: String::from("/"), total: String::from("/"), rest: Vec::new(), From 3a5b417ad1639a31e9aac9f41b05d8b3f074b128 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 8 Jan 2022 13:55:13 -0600 Subject: [PATCH 08/33] Add more trace messages to the RouterService code I was trying to debug some issues with my routes and this additional tracing was quite helpful. --- packages/router/src/components/route.rs | 1 + packages/router/src/service.rs | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) 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/service.rs b/packages/router/src/service.rs index ae7c1ff05..7eddeb69d 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -46,8 +46,8 @@ impl RouterService { let listener = history.listen(move || { _root_found.set(false); // 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); } }); @@ -68,20 +68,25 @@ impl RouterService { } 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) { + log::trace!("Registered route '{}' with scope id {:?}", route, scope); self.slots.borrow_mut().push((scope, route)); } pub fn should_render(&self, scope: ScopeId) -> bool { + log::trace!("Should render scope id {:?}?", scope); if self.root_found.get() { + log::trace!(" no - because root_found is true"); return false; } let location = self.history.borrow().location(); let path = location.path(); + log::trace!(" current path is '{}'", path); let roots = self.slots.borrow(); @@ -90,14 +95,22 @@ impl RouterService { // fallback logic match root { Some((_id, route)) => { + log::trace!( + " matched given scope id {:?} with route root '{}'", + scope, + route, + ); if route == path { + log::trace!(" and it matches the current path '{}'", path); self.root_found.set(true); true } else { if route == "" { + log::trace!(" and the route is the root, so we will use that without a better match"); self.root_found.set(true); true } else { + log::trace!(" and the route '{}' is not the root nor does it match the current path", route); false } } From 3c6142fb9d8b5f715adf0fb30c2f3534b9ecd923 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sun, 9 Jan 2022 23:44:37 -0600 Subject: [PATCH 09/33] Commit WIP on router There are a few different changes in here that probably need to be picked apart. I'm sure much of this is wrong. * Fix missing `dyn` that compiler complained about in router.rs * Make UseRoute store a `Rc` rather than a string so we can get information out of the router like current location. * Implement `UseRoute`'s nth_segment and last_segment methods. I changed the return type to a String because of the above. * Remove some unused imports in platform/mod.rs and service.rs * Implement the `use_route` fn. It panics if called outside a Router { } (I think). I think that makes sense. * Add a `current_location` method to `RouterService` that returns the current location. I needed this both for the `UseRoute` implementation and _also_ so I could get at this in my webapp code. I think having some way to get this will be useful for others, whether or not this exact API is used. In my case, I want to compare the current path to the `to` path of a `Link` so I can use a different class for that `Link` if it is the currently active page. --- packages/router/src/components/router.rs | 2 +- packages/router/src/hooks/use_route.rs | 40 ++++++++++++++++++++---- packages/router/src/platform/mod.rs | 2 -- packages/router/src/service.rs | 7 +++-- 4 files changed, 40 insertions(+), 11 deletions(-) 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..bf76d0975 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,7 +1,11 @@ use dioxus_core::ScopeState; +use gloo::history::Location; +use std::rc::Rc; + +use crate::RouterService; pub struct UseRoute<'a> { - cur_route: String, + router: Rc, cx: &'a ScopeState, } @@ -11,20 +15,44 @@ impl<'a> UseRoute<'a> { todo!() } - pub fn nth_segment(&self, n: usize) -> Option<&str> { - todo!() + 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!() + pub fn last_segment(&self) -> Option { + let mut segments = self.path_segments(); + let len = segments.len(); + if len == 0 { + return None; + } + Some(segments.remove(len - 1)) } /// Parse the segments of the URL, using named parameters (defined in your router) pub fn segment(&self, name: &str) -> Option<&T> { todo!() } + + pub fn current_location(&self) -> Location { + self.router.current_location() + } + + fn path_segments(&self) -> Vec { + let location = self.router.current_location(); + let stripped = &location.path()[1..]; + stripped.split('/').map(str::to_string).collect::>() + } } pub fn use_route<'a>(cx: &'a ScopeState) -> UseRoute<'a> { - todo!() + let router = cx + .consume_context::() + .expect("Cannot call use_route outside the scope of a Router component") + .clone(); + UseRoute { router, cx } } 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 7eddeb69d..c0b383e81 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,7 +1,6 @@ -use gloo::history::{BrowserHistory, History, HistoryListener}; +use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ cell::{Cell, RefCell}, - collections::HashMap, rc::Rc, }; @@ -118,6 +117,10 @@ impl RouterService { None => false, } } + + pub fn current_location(&self) -> Location { + self.history.borrow().location().clone() + } } pub struct RouterCfg { From f8a7e1cd8255fd7d0116384247e0e305bb73bf3d Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Mon, 10 Jan 2022 11:30:26 -0600 Subject: [PATCH 10/33] Implement router matching for path parameters We don't want to have the router just always match paths as exact strings. If a path contains a parameter like "/thing/:id" then the ":id" portion of the route should match _any_ string, not a literal ":id". --- packages/router/src/service.rs | 60 ++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index c0b383e81..9525ac44c 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -72,8 +72,9 @@ impl RouterService { } pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) { - log::trace!("Registered route '{}' with scope id {:?}", route, scope); - 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 { @@ -99,7 +100,7 @@ impl RouterService { scope, route, ); - if route == path { + if route_matches_path(route, path) { log::trace!(" and it matches the current path '{}'", path); self.root_found.set(true); true @@ -123,6 +124,59 @@ impl RouterService { } } +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) -> bool { + 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 false; + } + + 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, + ); + continue; + } + log::trace!( + " route piece '{}' must be an exact match for path piece '{}'", + r, + path_pieces[i], + ); + if path_pieces[i] != *r { + return false; + } + } + + return true; +} + pub struct RouterCfg { initial_route: String, } From c9408da7310423b1676fab6d41635f9a8000d89e Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 11 Jan 2022 10:47:17 -0600 Subject: [PATCH 11/33] Implement UseRoute segment method This relies on the RouterService to capture path params when it does path matching. --- packages/router/src/hooks/use_route.rs | 17 ++++++++++---- packages/router/src/service.rs | 31 ++++++++++++++------------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index bf76d0975..5ed056f65 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,6 +1,6 @@ use dioxus_core::ScopeState; use gloo::history::Location; -use std::rc::Rc; +use std::{rc::Rc, str::FromStr}; use crate::RouterService; @@ -33,9 +33,18 @@ impl<'a> UseRoute<'a> { Some(segments.remove(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::())) } pub fn current_location(&self) -> Location { diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 9525ac44c..2fac8509b 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,6 +1,7 @@ use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ - cell::{Cell, RefCell}, + cell::{Cell, Ref, RefCell}, + collections::HashMap, rc::Rc, }; @@ -9,10 +10,9 @@ use dioxus_core::ScopeId; pub struct RouterService { pub(crate) regen_route: Rc, history: Rc>, - registered_routes: RefCell, slots: Rc>>, root_found: Rc>, - cur_root: RefCell, + cur_path_params: Rc>>, listener: HistoryListener, } @@ -52,16 +52,11 @@ impl RouterService { }); Self { - registered_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, } } @@ -100,9 +95,10 @@ impl RouterService { scope, route, ); - if route_matches_path(route, path) { + if let Some(params) = route_matches_path(route, path) { log::trace!(" and it matches the current path '{}'", path); self.root_found.set(true); + *self.cur_path_params.borrow_mut() = params; true } else { if route == "" { @@ -122,6 +118,10 @@ impl RouterService { 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 { @@ -138,7 +138,7 @@ fn clean_path(path: &str) -> &str { path.trim_end_matches('/') } -fn route_matches_path(route: &str, path: &str) -> bool { +fn route_matches_path(route: &str, path: &str) -> Option> { let route_pieces = route.split('/').collect::>(); let path_pieces = clean_path(path).split('/').collect::>(); @@ -150,9 +150,10 @@ fn route_matches_path(route: &str, path: &str) -> bool { if route_pieces.len() != path_pieces.len() { log::trace!(" the routes are different lengths"); - return false; + 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 @@ -162,6 +163,8 @@ fn route_matches_path(route: &str, path: &str) -> bool { " 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!( @@ -170,11 +173,11 @@ fn route_matches_path(route: &str, path: &str) -> bool { path_pieces[i], ); if path_pieces[i] != *r { - return false; + return None; } } - return true; + Some(matches) } pub struct RouterCfg { From e06eac1ce5c6bb0a6680482574d82b16a141a626 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 11 Jan 2022 11:27:02 -0600 Subject: [PATCH 12/33] More WIP router implementation --- examples/router.rs | 12 +++++-- packages/router/src/hooks/use_route.rs | 46 +++++++++++++++++--------- 2 files changed, 40 insertions(+), 18 deletions(-) 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/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 5ed056f65..97dd6c048 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,20 +1,29 @@ use dioxus_core::ScopeState; -use gloo::history::Location; +use gloo::history::{HistoryResult, Location}; +use serde::de::DeserializeOwned; use std::{rc::Rc, str::FromStr}; use crate::RouterService; -pub struct UseRoute<'a> { +/// 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, - cx: &'a ScopeState, } -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::() } + /// 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(); @@ -24,13 +33,12 @@ impl<'a> UseRoute<'a> { Some(segments.remove(n)) } - pub fn last_segment(&self) -> Option { + /// 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(); - let len = segments.len(); - if len == 0 { - return None; - } - Some(segments.remove(len - 1)) + segments.remove(segments.len() - 1) } /// Get the named parameter from the path, as defined in your router. The @@ -47,21 +55,29 @@ impl<'a> UseRoute<'a> { .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> { +/// 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, cx } + UseRoute { router } } From 7dd7944487d369e3c002991753487e869bfdc4a1 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Fri, 14 Jan 2022 09:56:46 +0800 Subject: [PATCH 13/33] Update ZH_CN.md --- notes/README/ZH_CN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index eaf364c80..dfc5b3e89 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -61,7 +61,7 @@
-Dioxus 是一个可移植、高性能的框架,用于在 Rust 中建跨平台的用户界面。 +Dioxus 是一个可移植、高性能的框架,用于在 Rust 中构建跨平台的用户界面。 ```rust fn app(cx: Scope) -> Element { @@ -165,4 +165,4 @@ Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但 这个项目使用 [MIT 协议]. -[MIT 协议]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE \ No newline at end of file +[MIT 协议]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE From 81c094ed29fcdc5c6099492dd6ab09a59b79252c Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 14 Jan 2022 14:55:12 -0600 Subject: [PATCH 14/33] Fix handling of re-renders in the Router Previously the router just stored a `root_found` boolean after it picked a route. But on re-render it would just always return false from `should_render` if this was true. This just aborted routing after a future resolved (or anything else that triggered a re-render). Now we store the matching ScopeId and check that against our routes in a re-render so we actually do re-render the matching route. --- packages/router/src/service.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 2fac8509b..00642414f 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -11,7 +11,7 @@ pub struct RouterService { pub(crate) regen_route: Rc, history: Rc>, slots: Rc>>, - root_found: Rc>, + root_found: Rc>>, cur_path_params: Rc>>, listener: HistoryListener, } @@ -39,11 +39,11 @@ 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, root) in _slots.borrow_mut().iter().rev() { log::trace!("regenerating slot {:?} for root '{}'", slot, root); @@ -74,8 +74,13 @@ impl RouterService { pub fn should_render(&self, scope: ScopeId) -> bool { log::trace!("Should render scope id {:?}?", scope); - if self.root_found.get() { - log::trace!(" no - because root_found is true"); + 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; } @@ -89,7 +94,7 @@ impl RouterService { // fallback logic match root { - Some((_id, route)) => { + Some((id, route)) => { log::trace!( " matched given scope id {:?} with route root '{}'", scope, @@ -97,13 +102,13 @@ impl RouterService { ); if let Some(params) = route_matches_path(route, path) { log::trace!(" and it matches the current path '{}'", path); - self.root_found.set(true); + self.root_found.set(Some(*id)); *self.cur_path_params.borrow_mut() = params; true } else { if route == "" { log::trace!(" and the route is the root, so we will use that without a better match"); - self.root_found.set(true); + 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); From 9b282d877ba0fe2463687cde7ea899b7f8510425 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 14 Jan 2022 15:40:01 -0600 Subject: [PATCH 15/33] Don't expect all components to have a scope in ScopeArena.ensure_drop_safety Not all components will be mounted when using a Router, so we cannot assume all components have a scope. --- packages/core/src/scopes.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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()); }); From e47ead5347ce778935f8f2127fcb14f520eda0ce Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 15 Jan 2022 20:17:48 -0500 Subject: [PATCH 16/33] fix: allow eventhandler to derive default --- packages/core/src/nodes.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index d662b4998..93d4c8b10 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -351,8 +351,9 @@ 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> { @@ -367,15 +368,6 @@ impl EventHandler<'_, T> { } } -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 +669,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 } } } From 036a0ff49a7dade0e04c9c07071a1ff49133ee24 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 16 Jan 2022 15:13:31 -0500 Subject: [PATCH 17/33] docs: add comments for the Handler --- packages/core/src/nodes.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 93d4c8b10..80de92917 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -357,12 +357,14 @@ pub struct EventHandler<'bump, T = ()> { } 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); } From 47bc4e4a44a7d08d4f42102d13f0766d9d6bf358 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 16 Jan 2022 15:27:41 -0500 Subject: [PATCH 18/33] feat: add "optional" flag for props --- examples/optional_props.rs | 16 +++++++++++++++- packages/core-macro/src/props/mod.rs | 8 ++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) 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/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; )* ) => { From 5badcd830cdc1d19faab8fb516baaebc09168c62 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Mon, 17 Jan 2022 23:44:16 +0800 Subject: [PATCH 19/33] Update ZH_CN.md --- notes/README/ZH_CN.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index dfc5b3e89..680409e88 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -82,7 +82,7 @@ Dioxus 为不同的平台都提供了很好的开发文档。 如果你会使用 React ,那 Dioxus 对你来说会很简单。 ### 项目特点: -- 桌面程序在本地运行。 +- 对桌面应用的原生支持。 - 强大的状态管理工具。 - 支持所有 HTML 标签,监听器和事件。 - 超高的内存使用率,稳定的组件分配器。 @@ -126,30 +126,30 @@ TypeScript 是一个不错的 JavaScript 拓展集,但它仍然算是 JavaScri TS 代码运行效率不高,而且有大量的配置项。 -相比之下,Dioxus 使用用 Rust 编写将大大的提高效能。 +相比之下,Dioxus 使用 Rust 编写将大大的提高效能。 使用 Rust 开发,我们能获得: -- 库的静态类型支持。 -- 变量的默认不变性。 -- 一个简单且直观的模块管理系统。 -- 集成在内部的文档系统。 +- 静态类型支持。 +- 变量默认不变性。 +- 简单直观的模块系统。 +- 内部集成的文档系统。 - 先进的模式匹配系统。 -- 简洁、高效、可组合的迭代器。 -- 内联内置的 单元测试 / 集成测试。 -- 一流的异常处理系统。 +- 简洁、高效、强大的迭代器。 +- 内置的 单元测试 / 集成测试。 +- 优秀的异常处理系统。 - 强大且健全的标准库。 - 灵活的 `宏` 系统。 - 使用 `crates.io` 管理包。 Dioxus 能为开发者提供的: -- 正确且安全的使用不可变数据结构。 +- 安全使用数据结构。 - 安全的错误处理结果。 - 拥有原生移动端的性能。 - 直接访问系统的IO层。 -Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让你的前端团队更有信心在更短的时间内做出重大改变。 +Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让团队能在更短的时间内做出强大功能。 ### 不建议使用 Dioxus 的情况? From 27bdef304083e8826b2dddbef5c0a0136f3898b1 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Mon, 17 Jan 2022 23:47:38 +0800 Subject: [PATCH 20/33] Update ZH_CN.md --- notes/README/ZH_CN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 680409e88..614774595 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -86,7 +86,7 @@ Dioxus 为不同的平台都提供了很好的开发文档。 - 强大的状态管理工具。 - 支持所有 HTML 标签,监听器和事件。 - 超高的内存使用率,稳定的组件分配器。 -- 多通道异步调动器,超强的异步支持。 +- 多通道异步调度器,超强的异步支持。 - 更多信息请查阅: [版本发布文档](https://dioxuslabs.com/blog/introducing-dioxus/). ### 示例 @@ -155,7 +155,7 @@ Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但 您不该在这些情况下使用 Dioxus : -- 您不喜欢 React Hooks 的前端开发方案。 +- 您不喜欢类似 React 的开发风格。 - 您需要一个 `no-std` 的渲染器。 - 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。 - 您需要一个 `Send + Sync` UI 解决方案(目前不支持)。 From 64080588d02c54a1d380116cbecdd17de16d2392 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 14 Jan 2022 15:44:18 -0600 Subject: [PATCH 21/33] Add a warning when Link it called outside of a Router context --- packages/router/src/components/link.rs | 31 ++++++++++++++++---------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index dc807aa1a..6a1b671ab 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -41,18 +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("")), - title: format_args!("{}", cx.props.title.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 } From a0f60152bc7e5866f114ed469809ce8be70d17d4 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:19:12 +0800 Subject: [PATCH 22/33] feat: link open in browser --- examples/link.rs | 27 +++++++++++++++++++++++++++ packages/desktop/Cargo.toml | 1 + packages/desktop/src/index.js | 25 ++++++++++++++++++++++++- packages/desktop/src/lib.rs | 21 +++++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 examples/link.rs diff --git a/examples/link.rs b/examples/link.rs new file mode 100644 index 000000000..bed14f8a2 --- /dev/null +++ b/examples/link.rs @@ -0,0 +1,27 @@ +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/", + onclick: |_| { + println!("Hello Dioxus"); + }, + "custom event link", + } + } + } + )) +} 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/index.js b/packages/desktop/src/index.js index 88cf9fcdc..b7de5bf0d 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -374,7 +374,7 @@ class Interpreter { } SetAttribute(edit) { - // console.log("setting attr", edit); + console.log("setting attr", edit); const name = edit.field; const value = edit.value; const ns = edit.ns; @@ -399,6 +399,29 @@ class Interpreter { break; case "dangerous_inner_html": node.innerHTML = value; + break; + case "href": + + if (node.tagName == "A") { + // open the tag in browser + node.setAttribute("browser-href", value); + node.setAttribute("href", "#"); + + node.addEventListener("click", function(event) { + + const target = event.target; + const real_id = target.getAttribute(`dioxus-id`); + + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: target.getAttribute("browser-href"), + }) + }) + + } else { + node.setAttribute(name, value); + } + break; default: // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0b6044761..546db27c1 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -187,6 +187,27 @@ 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 data.is_array() { + let arr = data.as_array().unwrap(); + if arr[0].is_object() { + let temp = arr[0] + .as_object() + .unwrap(); + if temp.contains_key("href") { + let url = temp + .get("href").unwrap() + .as_str().unwrap(); + match webbrowser::open(url) { + Ok(_) => {}, + Err(e) => log::trace!("Open browser error: {:?}", e), + } + } + } + } + } _ => {} } None From c737c424b05ad8453e8770a14a0d210fb0c7c2fe Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:19:58 +0800 Subject: [PATCH 23/33] feat: link open in browser --- packages/desktop/src/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index b7de5bf0d..d15fc39b0 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -374,7 +374,9 @@ class Interpreter { } SetAttribute(edit) { - console.log("setting attr", edit); + + // console.log("setting attr", edit); + const name = edit.field; const value = edit.value; const ns = edit.ns; From 5233ee97d9314f7f0e0bdf05c56d2a9e4201a596 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:20:36 +0800 Subject: [PATCH 24/33] fix: format code --- examples/link.rs | 2 +- packages/desktop/src/lib.rs | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/link.rs b/examples/link.rs index bed14f8a2..da4b5b459 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -6,7 +6,7 @@ fn main() { fn app(cx: Scope) -> Element { cx.render(rsx! ( - div { + div { p { a { href: "http://dioxuslabs.com/", diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 546db27c1..0f451bd0d 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -193,16 +193,14 @@ pub fn launch_with_props( if data.is_array() { let arr = data.as_array().unwrap(); if arr[0].is_object() { - let temp = arr[0] - .as_object() - .unwrap(); + let temp = arr[0].as_object().unwrap(); if temp.contains_key("href") { - let url = temp - .get("href").unwrap() - .as_str().unwrap(); + let url = temp.get("href").unwrap().as_str().unwrap(); match webbrowser::open(url) { - Ok(_) => {}, - Err(e) => log::trace!("Open browser error: {:?}", e), + Ok(_) => {} + Err(e) => { + log::trace!("Open browser error: {:?}", e) + } } } } From 9e04ce5342850d2e0a01dde169807d6f6eb16566 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:32:27 +0800 Subject: [PATCH 25/33] feat: `open_browser` bool attribute --- examples/link.rs | 1 + packages/desktop/src/index.js | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/link.rs b/examples/link.rs index da4b5b459..49921f98a 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -16,6 +16,7 @@ fn app(cx: Scope) -> Element { p { a { href: "http://dioxuslabs.com/", + "browser-open": "false", onclick: |_| { println!("Hello Dioxus"); }, diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index d15fc39b0..c141f56d4 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -414,10 +414,15 @@ class Interpreter { const target = event.target; const real_id = target.getAttribute(`dioxus-id`); - rpc.call("browser_open", { - mounted_dom_id: parseInt(real_id), - href: target.getAttribute("browser-href"), - }) + if (target.getAttribute("browser-open") != "false") { + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: target.getAttribute("browser-href"), + }); + } else { + window.location = target.getAttribute("browser-href"); + } + }) } else { From f006f50317f4b75fac353bc988db057a281ba7f8 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 14:52:14 +0800 Subject: [PATCH 26/33] feat: move `rpc` to handler --- examples/link.rs | 2 +- packages/desktop/src/index.js | 43 ++++++++++++----------------------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/examples/link.rs b/examples/link.rs index 49921f98a..41b010e8d 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -16,7 +16,7 @@ fn app(cx: Scope) -> Element { p { a { href: "http://dioxuslabs.com/", - "browser-open": "false", + prevent_default: "onclick", onclick: |_| { println!("Hello Dioxus"); }, diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index c141f56d4..0bd767581 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -343,6 +343,7 @@ class Interpreter { this.listeners[event_name] = true; this.root.addEventListener(event_name, (event) => { + const target = event.target; const real_id = target.getAttribute(`dioxus-id`); @@ -356,6 +357,20 @@ class Interpreter { event.preventDefault(); } + console.log(event); + if (event.type == "click") { + event.preventDefault(); + if (should_prevent_default !== `onclick`) { + console.log(event.target.getAttribute("href")); + if(element.tagName == "A") { + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: event.target.getAttribute("href") + }) + } + } + } + if (real_id == null) { return; } @@ -401,34 +416,6 @@ class Interpreter { break; case "dangerous_inner_html": node.innerHTML = value; - break; - case "href": - - if (node.tagName == "A") { - // open the tag in browser - node.setAttribute("browser-href", value); - node.setAttribute("href", "#"); - - node.addEventListener("click", function(event) { - - const target = event.target; - const real_id = target.getAttribute(`dioxus-id`); - - if (target.getAttribute("browser-open") != "false") { - rpc.call("browser_open", { - mounted_dom_id: parseInt(real_id), - href: target.getAttribute("browser-href"), - }); - } else { - window.location = target.getAttribute("browser-href"); - } - - }) - - } else { - node.setAttribute(name, value); - } - break; default: // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 From 62b637f8b0eaf616c49461fa23b9251a79abc147 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 14:54:48 +0800 Subject: [PATCH 27/33] fix: error pattern --- packages/desktop/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0f451bd0d..38c85052a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -196,11 +196,8 @@ pub fn launch_with_props( let temp = arr[0].as_object().unwrap(); if temp.contains_key("href") { let url = temp.get("href").unwrap().as_str().unwrap(); - match webbrowser::open(url) { - Ok(_) => {} - Err(e) => { - log::trace!("Open browser error: {:?}", e) - } + if let Err(e) = webbrowser::open(url) { + log::error!("Open Browser error: {:?}", e); } } } From 46fd6ac3450ca5ebf9aecb2d59a5a92b2a68bdd0 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 14:57:41 +0800 Subject: [PATCH 28/33] feat: link open in browser --- packages/desktop/src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 38c85052a..8526a4a93 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -190,10 +190,8 @@ pub fn launch_with_props( "browser_open" => { let data = req.params.unwrap(); log::trace!("Open browser: {:?}", data); - if data.is_array() { - let arr = data.as_array().unwrap(); - if arr[0].is_object() { - let temp = arr[0].as_object().unwrap(); + 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) { From 8089023a6c3a54957af9c9c05c9dee6088b059ef Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 15:00:49 +0800 Subject: [PATCH 29/33] fix: prevent `submit` default --- packages/desktop/src/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 0bd767581..3ab21d6ba 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -357,7 +357,10 @@ class Interpreter { event.preventDefault(); } - console.log(event); + if (event.type == "submit") { + event.preventDefault(); + } + if (event.type == "click") { event.preventDefault(); if (should_prevent_default !== `onclick`) { From 327f9015481809d8e5b9e69f26202e8d66dd198e Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 23:15:39 +0800 Subject: [PATCH 30/33] fix: check `href` null --- packages/desktop/src/index.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 3ab21d6ba..685d1c7be 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -364,12 +364,14 @@ class Interpreter { if (event.type == "click") { event.preventDefault(); if (should_prevent_default !== `onclick`) { - console.log(event.target.getAttribute("href")); if(element.tagName == "A") { - rpc.call("browser_open", { - mounted_dom_id: parseInt(real_id), - href: event.target.getAttribute("href") - }) + const href = event.target.getAttribute("href") + if (href !== "" || href !== null) { + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: event.target.getAttribute("href") + }); + } } } } From 2073b400df55f0c6d8bed7371b2313be6c064e6e Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 23:25:45 +0800 Subject: [PATCH 31/33] fix: check `href` null --- packages/desktop/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 685d1c7be..2aba572f4 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -366,7 +366,7 @@ class Interpreter { if (should_prevent_default !== `onclick`) { if(element.tagName == "A") { const href = event.target.getAttribute("href") - if (href !== "" || href !== null) { + if (href !== "" && href !== null && href !== undefined) { rpc.call("browser_open", { mounted_dom_id: parseInt(real_id), href: event.target.getAttribute("href") From 16d2074fcf9187e14b7676aa9672e8ede92224fd Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Wed, 19 Jan 2022 20:14:41 +0800 Subject: [PATCH 32/33] fix: `dog-app` breed change --- examples/dog_app.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 { From c1a5d4e11f5277e6ee644f670dc6fdd68f2202f1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 20 Jan 2022 04:01:38 -0500 Subject: [PATCH 33/33] fix: ssr + hydration event listeners --- packages/web/src/dom.rs | 20 ++++++++------------ packages/web/src/rehydrate.rs | 11 +++++++++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 364f1a1d7..219f5aa79 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -75,10 +75,8 @@ impl WebsysDom { } DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), DomEdit::NewEventListener { - event_name, - scope, - root: mounted_node_id, - } => self.new_event_listener(event_name, scope, mounted_node_id), + event_name, root, .. + } => self.new_event_listener(event_name, root), DomEdit::RemoveEventListener { event, root } => { self.remove_event_listener(event, root) @@ -172,10 +170,8 @@ impl WebsysDom { let node = self.nodes[root as usize].as_ref().unwrap(); if let Some(element) = node.dyn_ref::() { element.remove(); - } else { - if let Some(parent) = node.parent_node() { - parent.remove_child(&node).unwrap(); - } + } else if let Some(parent) = node.parent_node() { + parent.remove_child(node).unwrap(); } } @@ -227,7 +223,7 @@ impl WebsysDom { self.nodes[(id as usize)] = Some(el); } - fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { + pub fn new_event_listener(&mut self, event: &'static str, node: u64) { let event = wasm_bindgen::intern(event); // attach the correct attributes to the element @@ -235,7 +231,7 @@ impl WebsysDom { // This ensures we only ever have one handler attached to the root, but decide // dynamically when we want to call a listener. - let el = self.stack.top(); + let el = self.nodes[(node as usize)].as_ref().unwrap(); let el = el.dyn_ref::().unwrap(); @@ -278,7 +274,7 @@ impl WebsysDom { .unwrap(); // Increment the listeners - self.listeners.insert(event.into(), (1, handler)); + self.listeners.insert(event, (1, handler)); } } @@ -442,7 +438,7 @@ impl WebsysDom { anchor .parent_node() .unwrap() - .insert_before(&before, Some(&anchor)) + .insert_before(&before, Some(anchor)) .unwrap(); } else { let arr: js_sys::Array = self diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 8993dd58c..91abb4fa1 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -116,7 +116,14 @@ impl WebsysDom { // we cant have the last node be text let mut last_node_was_text = false; for child in vel.children { - self.rehydrate_single(nodes, place, dom, &child, &mut last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, child, &mut last_node_was_text)?; + } + + for listener in vel.listeners { + self.new_event_listener( + listener.event, + listener.mounted_node.get().unwrap().as_u64(), + ); } place.pop(); @@ -142,7 +149,7 @@ impl WebsysDom { VNode::Fragment(el) => { for el in el.children { - self.rehydrate_single(nodes, place, dom, &el, last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, el, last_node_was_text)?; } }