Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Portal #1820

Merged
merged 11 commits into from
Oct 9, 2023
Merged

Portal #1820

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"] }
maccesch marked this conversation as resolved.
Show resolved Hide resolved
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;
}}
}
Loading