mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: add Portal component (#1820)
This commit is contained in:
parent
c080c2cbca
commit
4251f6c0f4
12 changed files with 226 additions and 6 deletions
|
@ -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
|
||||
|
||||
|
|
15
examples/portal/Cargo.toml
Normal file
15
examples/portal/Cargo.toml
Normal file
|
@ -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"
|
5
examples/portal/Makefile.toml
Normal file
5
examples/portal/Makefile.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
7
examples/portal/README.md
Normal file
7
examples/portal/README.md
Normal file
|
@ -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.
|
9
examples/portal/index.html
Normal file
9
examples/portal/index.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
2
examples/portal/rust-toolchain.toml
Normal file
2
examples/portal/rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "nightly"
|
34
examples/portal/src/lib.rs
Normal file
34
examples/portal/src/lib.rs
Normal file
|
@ -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! {
|
||||
<div>
|
||||
<button id="btn-show" on:click=move |_| set_show_overlay(true)>
|
||||
Show Overlay
|
||||
</button>
|
||||
|
||||
<Show when=show_overlay fallback=|| ()>
|
||||
<div>Show</div>
|
||||
<Portal mount=document().get_element_by_id("app").unwrap()>
|
||||
<div style="position: fixed; z-index: 10; width: 100vw; height: 100vh; top: 0; left: 0; background: rgba(0, 0, 0, 0.8); color: white;">
|
||||
<p>This is in the body element</p>
|
||||
<button id="btn-hide" on:click=move |_| set_show_overlay(false)>
|
||||
Close Overlay
|
||||
</button>
|
||||
<button id="btn-toggle" on:click=move |_| set_show_inside_overlay(!show_inside_overlay())>
|
||||
Toggle inner
|
||||
</button>
|
||||
|
||||
<Show when=show_inside_overlay fallback=|| view! { "Hidden" }>
|
||||
Visible
|
||||
</Show>
|
||||
</div>
|
||||
</Portal>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
}
|
15
examples/portal/src/main.rs
Normal file
15
examples/portal/src/main.rs
Normal file
|
@ -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! { <App/> },
|
||||
)
|
||||
}
|
60
examples/portal/tests/web.rs
Normal file
60
examples/portal/tests/web.rs
Normal file
|
@ -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! { <App/> });
|
||||
|
||||
let show_button = document
|
||||
.get_element_by_id("btn-show")
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlButtonElement>();
|
||||
|
||||
show_button.click();
|
||||
|
||||
// next_tick().await;
|
||||
|
||||
// check HTML
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<!-- <App> --><div><button id=\"btn-show\">\n Show Overlay\n </button><!-- <Show> --><!-- <DynChild> --><!-- <> --><div>Show</div><!-- <Portal> --><!-- <() /> --><!-- </Portal> --><!-- </> --><!-- </DynChild> --><!-- </Show> --></div><!-- </App> --><div><!-- <> --><div style=\"position: fixed; z-index: 10; width: 100vw; height: 100vh; top: 0; left: 0; background: rgba(0, 0, 0, 0.8); color: white;\"><p>This is in the body element</p><button id=\"btn-hide\">\n Close Overlay\n </button><button id=\"btn-toggle\">\n Toggle inner\n </button><!-- <Show> --><!-- <DynChild> -->Hidden<!-- </DynChild> --><!-- </Show> --></div><!-- </> --></div>"
|
||||
);
|
||||
|
||||
let toggle_button = document
|
||||
.get_element_by_id("btn-toggle")
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlButtonElement>();
|
||||
|
||||
toggle_button.click();
|
||||
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<!-- <App> --><div><button id=\"btn-show\">\n Show Overlay\n </button><!-- <Show> --><!-- <DynChild> --><!-- <> --><div>Show</div><!-- <Portal> --><!-- <() /> --><!-- </Portal> --><!-- </> --><!-- </DynChild> --><!-- </Show> --></div><!-- </App> --><div><!-- <> --><div style=\"position: fixed; z-index: 10; width: 100vw; height: 100vh; top: 0; left: 0; background: rgba(0, 0, 0, 0.8); color: white;\"><p>This is in the body element</p><button id=\"btn-hide\">\n Close Overlay\n </button><button id=\"btn-toggle\">\n Toggle inner\n </button><!-- <Show> --><!-- <DynChild> --><!-- <> -->\n Visible\n <!-- </> --><!-- </DynChild> --><!-- </Show> --></div><!-- </> --></div>"
|
||||
);
|
||||
|
||||
let hide_button = document
|
||||
.get_element_by_id("btn-hide")
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlButtonElement>();
|
||||
|
||||
hide_button.click();
|
||||
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<!-- <App> --><div><button id=\"btn-show\">\n Show \
|
||||
Overlay\n </button><!-- <Show> --><!-- <DynChild> --><!-- \
|
||||
<() /> --><!-- </DynChild> --><!-- </Show> --></div><!-- </App> -->"
|
||||
);
|
||||
}
|
|
@ -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 = [
|
||||
|
|
|
@ -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;
|
||||
|
|
74
leptos/src/portal.rs
Normal file
74
leptos/src/portal.rs
Normal file
|
@ -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 `<div>` unless `is_svg` is `true` in which case it's wrappend in a `<g>`.
|
||||
/// 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<web_sys::Element>,
|
||||
/// 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;
|
||||
}}
|
||||
}
|
Loading…
Reference in a new issue