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

Implementation of portals #2147

Merged
merged 5 commits into from
Nov 16, 2021
Merged
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ members = [
"examples/multi_thread",
"examples/nested_list",
"examples/node_refs",
"examples/portals",
"examples/pub_sub",
"examples/router",
"examples/store",
Expand Down
23 changes: 23 additions & 0 deletions examples/portals/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "portals"
version = "0.1.0"
authors = ["Martin Molzer <[email protected]>"]
edition = "2018"
license = "MIT OR Apache-2.0"

[dependencies]
yew = { path = "../../packages/yew" }
gloo-utils = "0.1"
wasm-bindgen = "0.2"

[dependencies.web-sys]
version = "0.3"
features = [
"Document",
"Element",
"Node",
"HtmlHeadElement",
"ShadowRoot",
"ShadowRootInit",
"ShadowRootMode",
]
10 changes: 10 additions & 0 deletions examples/portals/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Portals Example

[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fportals)](https://examples.yew.rs/portals)

This example renders elements into out-of-tree nodes with the help of portals.

## Concepts

- Manually creating `Html` without the `html!` macro.
- Using `web-sys` to manipulate the DOM.
9 changes: 9 additions & 0 deletions examples/portals/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Yew • Portals</title>
</head>

<body></body>
</html>
106 changes: 106 additions & 0 deletions examples/portals/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use wasm_bindgen::JsCast;
use web_sys::{Element, ShadowRootInit, ShadowRootMode};
use yew::{create_portal, html, Children, Component, Context, Html, NodeRef, Properties};

#[derive(Properties, PartialEq)]
pub struct ShadowDOMProps {
#[prop_or_default]
pub children: Children,
}

pub struct ShadowDOMHost {
host_ref: NodeRef,
inner_host: Option<Element>,
}

impl Component for ShadowDOMHost {
type Message = ();
type Properties = ShadowDOMProps;

fn create(_: &Context<Self>) -> Self {
Self {
host_ref: NodeRef::default(),
inner_host: None,
}
}

fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
if first_render {
let shadow_root = self
.host_ref
.get()
.expect("rendered host")
.unchecked_into::<Element>()
.attach_shadow(&ShadowRootInit::new(ShadowRootMode::Closed))
.expect("installing shadow root succeeds");
let inner_host = gloo_utils::document()
.create_element("div")
.expect("can create inner wrapper");
shadow_root
.append_child(&inner_host)
.expect("can attach inner host");
self.inner_host = Some(inner_host);
ctx.link().send_message(());
}
}

fn update(&mut self, _: &Context<Self>, _: Self::Message) -> bool {
true
}

fn view(&self, ctx: &Context<Self>) -> Html {
let contents = if let Some(ref inner_host) = self.inner_host {
create_portal(
html! {
{for ctx.props().children.iter()}
},
inner_host.clone(),
)
} else {
html! { <></> }
};
html! {
<div ref={self.host_ref.clone()}>
{contents}
</div>
}
}
}

pub struct Model {
pub style_html: Html,
}

impl Component for Model {
type Message = ();
type Properties = ();

fn create(_ctx: &Context<Self>) -> Self {
let document_head = gloo_utils::document()
.head()
.expect("head element to be present");
let style_html = create_portal(
html! {
<style>{"p { color: red; }"}</style>
},
document_head.into(),
);
Self { style_html }
}

fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<>
{self.style_html.clone()}
<p>{"This paragraph is colored red, and its style is mounted into "}<pre>{"document.head"}</pre>{" with a portal"}</p>
<ShadowDOMHost>
<p>{"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}</p>
</ShadowDOMHost>
</>
}
}
}

fn main() {
yew::start_app::<Model>();
}
1 change: 1 addition & 0 deletions packages/website-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ yew-router = { path = "../../packages/yew-router/" }
[dev-dependencies.web-sys]
version = "0.3"
features = [
"Document",
"Element",
"EventTarget",
"HtmlElement",
Expand Down
5 changes: 5 additions & 0 deletions packages/website-test/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ fn main() {
let pattern = format!("{}/../../website/docs/**/*.md", home);
let base = format!("{}/../../website", home);
let base = Path::new(&base).canonicalize().unwrap();
let dir_pattern = format!("{}/../../website/docs/**", home);
for dir in glob(&dir_pattern).unwrap() {
println!("cargo:rerun-if-changed={}", dir.unwrap().display());
}

let mut level = Level::default();

for entry in glob(&pattern).unwrap() {
let path = entry.unwrap();
let path = Path::new(&path).canonicalize().unwrap();
println!("cargo:rerun-if-changed={}", path.display());
let rel = path.strip_prefix(&base).unwrap();

let mut parts = vec![];
Expand Down
12 changes: 10 additions & 2 deletions packages/yew/src/html/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ pub use component::*;
pub use conversion::*;
pub use listener::*;

use crate::virtual_dom::VNode;
use crate::virtual_dom::{VNode, VPortal};
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::JsValue;
use web_sys::Node;
use web_sys::{Element, Node};

/// A type which expected as a result of `view` function implementation.
pub type Html = VNode;
Expand Down Expand Up @@ -136,6 +136,14 @@ impl NodeRef {
}
}

/// Render children into a DOM node that exists outside the hierarchy of the parent
/// component.
/// ## Relevant examples
/// - [Portals](https://github.com/yewstack/yew/tree/master/examples/portals)
pub fn create_portal(child: Html, host: Element) -> Html {
VNode::VPortal(VPortal::new(child, host))
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
3 changes: 2 additions & 1 deletion packages/yew/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,8 @@ pub mod prelude {
pub use crate::context::ContextProvider;
pub use crate::events::*;
pub use crate::html::{
Children, ChildrenWithProps, Classes, Component, Context, Html, NodeRef, Properties,
create_portal, Children, ChildrenWithProps, Classes, Component, Context, Html, NodeRef,
Properties,
};
pub use crate::macros::{classes, html, html_nested};

Expand Down
4 changes: 4 additions & 0 deletions packages/yew/src/virtual_dom/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub mod vlist;
#[doc(hidden)]
pub mod vnode;
#[doc(hidden)]
pub mod vportal;
#[doc(hidden)]
pub mod vtag;
#[doc(hidden)]
pub mod vtext;
Expand All @@ -31,6 +33,8 @@ pub use self::vlist::VList;
#[doc(inline)]
pub use self::vnode::VNode;
#[doc(inline)]
pub use self::vportal::VPortal;
#[doc(inline)]
pub use self::vtag::VTag;
#[doc(inline)]
pub use self::vtext::VText;
Expand Down
14 changes: 13 additions & 1 deletion packages/yew/src/virtual_dom/vnode.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! This module contains the implementation of abstract virtual node.

use super::{Key, VChild, VComp, VDiff, VList, VTag, VText};
use super::{Key, VChild, VComp, VDiff, VList, VPortal, VTag, VText};
use crate::html::{AnyScope, Component, NodeRef};
use gloo::console;
use std::cmp::PartialEq;
Expand All @@ -21,6 +21,8 @@ pub enum VNode {
VComp(VComp),
/// A holder for a list of other nodes.
VList(VList),
/// A portal to another part of the document
VPortal(VPortal),
/// A holder for any `Node` (necessary for replacing node).
VRef(Node),
}
Expand All @@ -33,6 +35,7 @@ impl VNode {
VNode::VRef(_) => None,
VNode::VTag(vtag) => vtag.key.clone(),
VNode::VText(_) => None,
VNode::VPortal(vportal) => vportal.node.key(),
}
}

Expand All @@ -43,6 +46,7 @@ impl VNode {
VNode::VList(vlist) => vlist.key.is_some(),
VNode::VRef(_) | VNode::VText(_) => false,
VNode::VTag(vtag) => vtag.key.is_some(),
VNode::VPortal(vportal) => vportal.node.has_key(),
}
}

Expand All @@ -58,6 +62,7 @@ impl VNode {
VNode::VComp(vcomp) => vcomp.node_ref.get(),
VNode::VList(vlist) => vlist.get(0).and_then(VNode::first_node),
VNode::VRef(node) => Some(node.clone()),
VNode::VPortal(vportal) => vportal.next_sibling(),
}
}

Expand Down Expand Up @@ -88,6 +93,7 @@ impl VNode {
.expect("VList is not mounted")
.unchecked_first_node(),
VNode::VRef(node) => node.clone(),
VNode::VPortal(_) => panic!("portals have no first node, they are empty inside"),
}
}

Expand All @@ -104,6 +110,7 @@ impl VNode {
.expect("VComp has no root vnode")
.move_before(parent, next_sibling);
}
VNode::VPortal(_) => {} // no need to move portals
_ => super::insert_node(&self.unchecked_first_node(), parent, next_sibling.as_ref()),
};
}
Expand All @@ -122,6 +129,7 @@ impl VDiff for VNode {
console::warn!("Node not found to remove VRef");
}
}
VNode::VPortal(ref mut vportal) => vportal.detach(parent),
}
}

Expand Down Expand Up @@ -155,6 +163,9 @@ impl VDiff for VNode {
super::insert_node(node, parent, next_sibling.get().as_ref());
NodeRef::new(node.clone())
}
VNode::VPortal(ref mut vportal) => {
vportal.apply(parent_scope, parent, next_sibling, ancestor)
}
}
}
}
Expand Down Expand Up @@ -225,6 +236,7 @@ impl fmt::Debug for VNode {
VNode::VComp(ref vcomp) => vcomp.fmt(f),
VNode::VList(ref vlist) => vlist.fmt(f),
VNode::VRef(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)),
VNode::VPortal(ref vportal) => vportal.fmt(f),
}
}
}
Expand Down
Loading