Skip to content

Commit

Permalink
feat: add Portal component (#1820)
Browse files Browse the repository at this point in the history
  • Loading branch information
maccesch authored Oct 9, 2023
1 parent c080c2c commit 4251f6c
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 6 deletions.
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions examples/portal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions examples/portal/Makefile.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]
7 changes: 7 additions & 0 deletions examples/portal/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions examples/portal/index.html
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions examples/portal/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"
34 changes: 34 additions & 0 deletions examples/portal/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 15 additions & 0 deletions examples/portal/src/main.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 60 additions & 0 deletions examples/portal/tests/web.rs
Original file line number Diff line number Diff line change
@@ -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> -->"
);
}
7 changes: 2 additions & 5 deletions leptos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,25 @@ 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",
"leptos_macro/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"]
Expand Down Expand Up @@ -79,7 +77,6 @@ denylist = [
"template_macro",
"rustls",
"default-tls",
"web-sys",
"wasm-bindgen",
]
skip_feature_sets = [
Expand Down
2 changes: 2 additions & 0 deletions leptos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
74 changes: 74 additions & 0 deletions leptos/src/portal.rs
Original file line number Diff line number Diff line change
@@ -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;
}}
}

0 comments on commit 4251f6c

Please sign in to comment.