Skip to content

Commit

Permalink
Implementation of portals (#2147)
Browse files Browse the repository at this point in the history
* initial poc implementation of portals

* add new_before

* add portals example

* add shadow dom example

* add english website documentation
  • Loading branch information
WorldSEnder authored Nov 16, 2021
1 parent a5c343d commit aa9b9b2
Show file tree
Hide file tree
Showing 14 changed files with 411 additions and 4 deletions.
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

0 comments on commit aa9b9b2

Please sign in to comment.