From 4251f6c0f482cccc892ed456354e6e2b505e73b7 Mon Sep 17 00:00:00 2001 From: Marc-Stefan Cassola Date: Mon, 9 Oct 2023 21:18:52 +0100 Subject: [PATCH] feat: add Portal component (#1820) --- examples/README.md | 2 +- examples/portal/Cargo.toml | 15 ++++++ examples/portal/Makefile.toml | 5 ++ examples/portal/README.md | 7 +++ examples/portal/index.html | 9 ++++ examples/portal/rust-toolchain.toml | 2 + examples/portal/src/lib.rs | 34 +++++++++++++ examples/portal/src/main.rs | 15 ++++++ examples/portal/tests/web.rs | 60 +++++++++++++++++++++++ leptos/Cargo.toml | 7 +-- leptos/src/lib.rs | 2 + leptos/src/portal.rs | 74 +++++++++++++++++++++++++++++ 12 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 examples/portal/Cargo.toml create mode 100644 examples/portal/Makefile.toml create mode 100644 examples/portal/README.md create mode 100644 examples/portal/index.html create mode 100644 examples/portal/rust-toolchain.toml create mode 100644 examples/portal/src/lib.rs create mode 100644 examples/portal/src/main.rs create mode 100644 examples/portal/tests/web.rs create mode 100644 leptos/src/portal.rs diff --git a/examples/README.md b/examples/README.md index 71dc8d37c3..a81ad0f6ca 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,7 +6,7 @@ The examples in this directory are all built and tested against the current `mai To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch but not the current release. -To see the examples as they were at the time of the `0.4.9` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.4.9/examples). +To see the examples as they were at the time of the `0.5.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.5.0/examples). ## Cargo Make diff --git a/examples/portal/Cargo.toml b/examples/portal/Cargo.toml new file mode 100644 index 0000000000..c4ed7f6412 --- /dev/null +++ b/examples/portal/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "portal" +version = "0.1.0" +edition = "2021" + +[dependencies] +leptos = { path = "../../leptos", features = ["csr", "nightly"] } +log = "0.4" +console_log = "1" +console_error_panic_hook = "0.1.7" + +[dev-dependencies] +wasm-bindgen-test = "0.3.0" +wasm-bindgen = "0.2" +web-sys = "0.3" \ No newline at end of file diff --git a/examples/portal/Makefile.toml b/examples/portal/Makefile.toml new file mode 100644 index 0000000000..7c753497be --- /dev/null +++ b/examples/portal/Makefile.toml @@ -0,0 +1,5 @@ +extend = [ + { path = "../cargo-make/main.toml" }, + { path = "../cargo-make/wasm-test.toml" }, + { path = "../cargo-make/trunk_server.toml" }, +] diff --git a/examples/portal/README.md b/examples/portal/README.md new file mode 100644 index 0000000000..287d85ea33 --- /dev/null +++ b/examples/portal/README.md @@ -0,0 +1,7 @@ +# Leptos Portal Example + +This example showcases a basic leptos app with a portal. + +## Getting Started + +See the [Examples README](../README.md) for setup and run instructions. diff --git a/examples/portal/index.html b/examples/portal/index.html new file mode 100644 index 0000000000..3e27c6b353 --- /dev/null +++ b/examples/portal/index.html @@ -0,0 +1,9 @@ + + + + + + +
+ + \ No newline at end of file diff --git a/examples/portal/rust-toolchain.toml b/examples/portal/rust-toolchain.toml new file mode 100644 index 0000000000..5d56faf9ae --- /dev/null +++ b/examples/portal/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/examples/portal/src/lib.rs b/examples/portal/src/lib.rs new file mode 100644 index 0000000000..989167ff4e --- /dev/null +++ b/examples/portal/src/lib.rs @@ -0,0 +1,34 @@ +use leptos::*; + +#[component] +pub fn App() -> impl IntoView { + let (show_overlay, set_show_overlay) = create_signal(false); + let (show_inside_overlay, set_show_inside_overlay) = create_signal(false); + + view! { +
+ + + +
Show
+ +
+

This is in the body element

+ + + + + Visible + +
+
+
+
+ } +} diff --git a/examples/portal/src/main.rs b/examples/portal/src/main.rs new file mode 100644 index 0000000000..fea7d89ebb --- /dev/null +++ b/examples/portal/src/main.rs @@ -0,0 +1,15 @@ +use leptos::*; +use portal::App; +use wasm_bindgen::JsCast; + +fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + mount_to( + leptos::document() + .get_element_by_id("app") + .unwrap() + .unchecked_into(), + || view! { }, + ) +} diff --git a/examples/portal/tests/web.rs b/examples/portal/tests/web.rs new file mode 100644 index 0000000000..38b06766d1 --- /dev/null +++ b/examples/portal/tests/web.rs @@ -0,0 +1,60 @@ +use wasm_bindgen::JsCast; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); +use leptos::*; +use portal::App; +use web_sys::HtmlButtonElement; + +#[wasm_bindgen_test] +fn portal() { + let document = leptos::document(); + let body = document.body().unwrap(); + + let div = document.create_element("div").unwrap(); + div.set_id("app"); + let _ = body.append_child(&div); + + mount_to(div.clone().unchecked_into(), || view! { }); + + let show_button = document + .get_element_by_id("btn-show") + .unwrap() + .unchecked_into::(); + + show_button.click(); + + // next_tick().await; + + // check HTML + assert_eq!( + div.inner_html(), + "
Show

This is in the body element

Hidden
" + ); + + let toggle_button = document + .get_element_by_id("btn-toggle") + .unwrap() + .unchecked_into::(); + + toggle_button.click(); + + assert_eq!( + div.inner_html(), + "
Show

This is in the body element

\n Visible\n
" + ); + + let hide_button = document + .get_element_by_id("btn-hide") + .unwrap() + .unchecked_into::(); + + hide_button.click(); + + assert_eq!( + div.inner_html(), + "
" + ); +} diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 113bc0f050..4a6dca7490 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -21,19 +21,18 @@ typed-builder-macro = "0.16" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } server_fn = { workspace = true } -web-sys = { version = "0.3.63", optional = true } +web-sys = { version = "0.3.63", features = ["ShadowRoot", "ShadowRootInit", "ShadowRootMode"] } wasm-bindgen = { version = "0.2", optional = true } [features] default = ["serde"] -template_macro = ["leptos_dom/web", "dep:web-sys", "dep:wasm-bindgen"] +template_macro = ["leptos_dom/web", "dep:wasm-bindgen"] csr = [ "leptos_dom/csr", "leptos_macro/csr", "leptos_reactive/csr", "leptos_server/csr", "dep:wasm-bindgen", - "dep:web-sys", ] hydrate = [ "leptos_dom/hydrate", @@ -41,7 +40,6 @@ hydrate = [ "leptos_reactive/hydrate", "leptos_server/hydrate", "dep:wasm-bindgen", - "dep:web-sys", ] default-tls = ["leptos_server/default-tls", "server_fn/default-tls"] rustls = ["leptos_server/rustls", "server_fn/rustls"] @@ -79,7 +77,6 @@ denylist = [ "template_macro", "rustls", "default-tls", - "web-sys", "wasm-bindgen", ] skip_feature_sets = [ diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index bc6ebc60ab..cdeb525d42 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -232,8 +232,10 @@ pub use wasm_bindgen; // used in islands pub use web_sys; // used in islands mod children; +mod portal; mod view_fn; pub use children::*; +pub use portal::*; pub use view_fn::*; extern crate self as leptos; diff --git a/leptos/src/portal.rs b/leptos/src/portal.rs new file mode 100644 index 0000000000..06576684bf --- /dev/null +++ b/leptos/src/portal.rs @@ -0,0 +1,74 @@ +use crate::ChildrenFn; +use cfg_if::cfg_if; +use leptos_dom::IntoView; +use leptos_macro::component; + +/// Renders components somewhere else in the DOM. +/// +/// Useful for inserting modals and tooltips outside of a cropping layout. +/// If no mount point is given, the portal is inserted in `document.body`; +/// it is wrapped in a `
` unless `is_svg` is `true` in which case it's wrappend in a ``. +/// Setting `use_shadow` to `true` places the element in a shadow root to isolate styles. +#[cfg_attr( + any(debug_assertions, feature = "ssr"), + tracing::instrument(level = "info", skip_all) +)] +#[component] +pub fn Portal( + /// Target element where the children will be appended + #[prop(into, optional)] + mount: Option, + /// Whether to use a shadow DOM inside `mount`. Defaults to `false`. + #[prop(optional)] + use_shadow: bool, + /// When using SVG this has to be set to `true`. Defaults to `false`. + #[prop(optional)] + is_svg: bool, + /// The children to teleport into the `mount` element + children: ChildrenFn, +) -> impl IntoView { + cfg_if! { if #[cfg(all(target_arch = "wasm32", any(feature = "hydrate", feature = "csr")))] { + use leptos_dom::{document, Mountable}; + use leptos_reactive::{create_render_effect, on_cleanup}; + use wasm_bindgen::JsCast; + + let mount = mount + .unwrap_or_else(|| document().body().expect("body to exist").unchecked_into()); + + create_render_effect(move |_| { + let tag = if is_svg { "g" } else { "div" }; + + let container = document() + .create_element(tag) + .expect("element creation to work"); + + let render_root = if use_shadow { + container + .attach_shadow(&web_sys::ShadowRootInit::new( + web_sys::ShadowRootMode::Open, + )) + .map(|root| root.unchecked_into()) + .unwrap_or(container.clone()) + } else { + container.clone() + }; + + let _ = render_root.append_child(&children().into_view().get_mountable_node()); + + let _ = mount.append_child(&container); + + on_cleanup({ + let mount = mount.clone(); + + move || { + let _ = mount.remove_child(&container); + } + }) + }); + } else { + let _ = mount; + let _ = use_shadow; + let _ = is_svg; + let _ = children; + }} +}