switch to a Document trait and introduce Script/Head/Style/Meta components (#2635)

* switch to a Document trait and introduce Script/Head/Style/Meta components

* Fix desktop title

* Insert special elements into the head during the inital SSR render

* Make all head component attributes optional

* hydrate head elements

* improve the server streaming head warning

* Document fullstack head hydration approach

* deduplicate head elements by href

* move Link into head::Link

* document head components

* add meta and title examples

* Fix a few doc examples

* fix formatting

* Add title to playwright tests

* serde is optional on web, but it is enabled by hydrate

* remove leftover console log
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-07-17 20:54:03 -05:00
1 parent a490a94ae5
commit 176e67e5b7
65 files changed
+1703 -495

No files matched your search

+10 -3
View File
@@ -11,12 +11,15 @@ keywords = ["dom", "ui", "gui", "react"]
[dependencies]
dioxus-core = { workspace = true }
dioxus-core-macro = { workspace = true }
dioxus-rsx = { workspace = true, optional = true }
dioxus-html-internal-macro = { workspace = true }
dioxus-hooks = { workspace = true }
generational-box = { workspace = true }
serde = { version = "1", features = ["derive"], optional = true }
serde_repr = { version = "0.1", optional = true }
wasm-bindgen = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
js-sys = { version = "0.3.56", optional = true }
euclid = "0.22.7"
enumset = "1.1.2"
@@ -51,14 +54,18 @@ features = [
"CompositionEvent",
]
[build-dependencies]
lazy-js-bundle = { workspace = true }
[dev-dependencies]
serde_json = "1"
dioxus = { workspace = true }
dioxus-web = { workspace = true }
tokio = { workspace = true, features = ["time"] }
manganis = { workspace = true }
[features]
default = ["serialize", "mounted", "eval", "file-engine"]
default = ["serialize", "mounted", "document", "file-engine"]
serialize = [
"dep:serde",
"dep:serde_json",
@@ -76,7 +83,7 @@ mounted = [
"web-sys?/ScrollBehavior",
"web-sys?/HtmlElement",
]
eval = [
document = [
"dep:serde",
"dep:serde_json"
]
@@ -87,7 +94,7 @@ file-engine = [
"web-sys?/FileList",
"web-sys?/FileReader"
]
wasm-bind = ["dep:web-sys", "dep:wasm-bindgen"]
wasm-bind = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"]
native-bind = ["dep:tokio", "file-engine"]
hot-reload-context = ["dep:dioxus-rsx", "dioxus-rsx/hot_reload_traits"]
html-to-rsx = []
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+9
View File
@@ -0,0 +1,9 @@
fn main() {
// If any TS files change, re-run the build script
lazy_js_bundle::LazyTypeScriptBindings::new()
.with_watching("./src/ts")
.with_binding("./src/ts/eval.ts", "./src/js/eval.js")
.with_binding("./src/ts/native_eval.ts", "./src/js/native_eval.js")
.with_binding("./src/ts/head.ts", "./src/js/head.js")
.run();
}
+59
View File
@@ -0,0 +1,59 @@
# Modifying the Head
Dioxus includes a series of components that render into the head of the page:
- [Title](crate::Title)
- [Meta](crate::Meta)
- [head::Link](crate::head::Link)
- [Script](crate::Script)
- [Style](crate::Style)
Each of these components can be used to add extra information to the head of the page. For example, you can use the `Title` component to set the title of the page, or the `Meta` component to add extra metadata to the page.
## Limitations
Components that render into the head of the page do have a few key limitations:
- With the exception of the `Title` component, all components that render into the head cannot be modified after the first time they are rendered.
- Components that render into the head will not be removed even after the component is removed from the tree.
## Example
```rust, no_run
# use dioxus::prelude::*;
fn RedirectToDioxusHomepageWithoutJS() -> Element {
rsx! {
// You can use the meta component to render a meta tag into the head of the page
// This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
Meta {
http_equiv: "refresh",
content: "10;url=https://dioxuslabs.com",
}
}
}
```
## Fullstack Rendering
Head components are compatible with fullstack rendering, but only head components that are rendered in the initial render (before suspense boundaries resolve) will be rendered into the head.
If you have any important metadata that you want to render into the head, make sure to render it outside of any pending suspense boundaries.
```rust, no_run
# use dioxus::prelude::*;
# #[component]
# fn LoadData(children: Element) -> Element { unimplemented!() }
fn App() -> Element {
rsx! {
// This will render in SSR
Title { "My Page" }
SuspenseBoundary {
fallback: |_| rsx! { "Loading..." },
LoadData {
// This will only be rendered on the client after hydration so it may not be visible to search engines
Meta { name: "description", content: "My Page" }
}
}
}
}
```
+50
View File
@@ -0,0 +1,50 @@
/// Code for the Dioxus channel used to communicate between the dioxus and javascript code
#[cfg(feature = "native-bind")]
pub const NATIVE_EVAL_JS: &str = include_str!("../js/native_eval.js");
#[cfg(feature = "wasm-bind")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub struct JSOwner {
_owner: Box<dyn std::any::Any>,
}
#[cfg(feature = "wasm-bind")]
impl JSOwner {
pub fn new(owner: impl std::any::Any) -> Self {
Self {
_owner: Box::new(owner),
}
}
}
#[cfg(feature = "wasm-bind")]
#[wasm_bindgen::prelude::wasm_bindgen(module = "/src/js/eval.js")]
extern "C" {
pub type WebDioxusChannel;
#[wasm_bindgen(constructor)]
pub fn new(owner: JSOwner) -> WebDioxusChannel;
#[wasm_bindgen(method, js_name = "rustSend")]
pub fn rust_send(this: &WebDioxusChannel, value: wasm_bindgen::JsValue);
#[wasm_bindgen(method, js_name = "rustRecv")]
pub async fn rust_recv(this: &WebDioxusChannel) -> wasm_bindgen::JsValue;
#[wasm_bindgen(method)]
pub fn send(this: &WebDioxusChannel, value: wasm_bindgen::JsValue);
#[wasm_bindgen(method)]
pub async fn recv(this: &WebDioxusChannel) -> wasm_bindgen::JsValue;
#[wasm_bindgen(method)]
pub fn weak(this: &WebDioxusChannel) -> WeakDioxusChannel;
pub type WeakDioxusChannel;
#[wasm_bindgen(method, js_name = "rustSend")]
pub fn rust_send(this: &WeakDioxusChannel, value: wasm_bindgen::JsValue);
#[wasm_bindgen(method, js_name = "rustRecv")]
pub async fn rust_recv(this: &WeakDioxusChannel) -> wasm_bindgen::JsValue;
}
@@ -1,18 +1,14 @@
#![allow(clippy::await_holding_refcell_ref)]
#![doc = include_str!("../docs/eval.md")]
#![doc = include_str!("../../docs/eval.md")]
use dioxus_core::prelude::*;
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
use generational_box::GenerationalBox;
use std::future::{poll_fn, Future, IntoFuture};
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
/// A struct that implements EvalProvider is sent through [`ScopeState`]'s provide_context function
/// so that [`eval`] can provide a platform agnostic interface for evaluating JavaScript code.
pub trait EvalProvider {
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>>;
}
use super::document;
/// The platform's evaluator.
pub trait Evaluator {
@@ -42,55 +38,22 @@ type EvalCreator = Rc<dyn Fn(&str) -> UseEval>;
/// has access to most, if not all of your application data.**
#[must_use]
pub fn eval_provider() -> EvalCreator {
let eval_provider = consume_context::<Rc<dyn EvalProvider>>();
let eval_provider = document();
Rc::new(move |script: &str| UseEval::new(eval_provider.new_evaluator(script.to_string())))
as Rc<dyn Fn(&str) -> UseEval>
}
#[doc = include_str!("../docs/eval.md")]
#[doc = include_str!("../../docs/eval.md")]
#[doc(alias = "javascript")]
pub fn eval(script: &str) -> UseEval {
let eval_provider = dioxus_core::prelude::try_consume_context::<Rc<dyn EvalProvider>>()
// Create a dummy provider that always hiccups when trying to evaluate
// That way, we can still compile and run the code without a real provider
.unwrap_or_else(|| {
struct DummyProvider;
impl EvalProvider for DummyProvider {
fn new_evaluator(&self, _js: String) -> GenerationalBox<Box<dyn Evaluator>> {
tracing::error!("Eval is not supported on this platform. If you are using dioxus fullstack, you can wrap your code with `client! {{}}` to only include the code that runs eval in the client bundle.");
UnsyncStorage::owner().insert(Box::new(DummyEvaluator))
}
}
struct DummyEvaluator;
impl Evaluator for DummyEvaluator {
fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
Err(EvalError::Unsupported)
}
fn poll_recv(
&mut self,
_context: &mut Context<'_>,
) -> Poll<Result<serde_json::Value, EvalError>> {
Poll::Ready(Err(EvalError::Unsupported))
}
fn poll_join(
&mut self,
_context: &mut Context<'_>,
) -> Poll<Result<serde_json::Value, EvalError>> {
Poll::Ready(Err(EvalError::Unsupported))
}
}
Rc::new(DummyProvider) as Rc<dyn EvalProvider>
});
UseEval::new(eval_provider.new_evaluator(script.to_string()))
let document = use_hook(document);
UseEval::new(document.new_evaluator(script.to_string()))
}
/// A wrapper around the target platform's evaluator that lets you send and receive data from JavaScript spawned by [`eval`].
///
#[doc = include_str!("../docs/eval.md")]
#[doc = include_str!("../../docs/eval.md")]
#[derive(Clone, Copy)]
pub struct UseEval {
evaluator: GenerationalBox<Box<dyn Evaluator>>,
+520
View File
@@ -0,0 +1,520 @@
#![doc = include_str!("../../docs/head.md")]
use std::{cell::RefCell, collections::HashSet, rc::Rc};
use dioxus_core::{prelude::*, DynamicNode};
use dioxus_core_macro::*;
/// Warn the user if they try to change props on a element that is injected into the head
#[allow(unused)]
fn use_update_warning<T: PartialEq + Clone + 'static>(value: &T, name: &'static str) {
#[cfg(debug_assertions)]
{
let cloned_value = value.clone();
let initial = use_hook(move || value.clone());
if initial != cloned_value {
tracing::warn!("Changing the props of `{name}` is not supported ");
}
}
}
fn extract_single_text_node(children: &Element, component: &str) -> Option<String> {
let vnode = match children {
Element::Ok(vnode) => vnode,
Element::Err(err) => {
tracing::error!("Error while rendering {component}: {err}");
return None;
}
};
// The title's children must be in one of two forms:
// 1. rsx! { "static text" }
// 2. rsx! { "title: {dynamic_text}" }
match vnode.template.get() {
// rsx! { "static text" }
Template {
roots: &[TemplateNode::Text { text }],
node_paths: &[],
attr_paths: &[],
..
} => Some(text.to_string()),
// rsx! { "title: {dynamic_text}" }
Template {
roots: &[TemplateNode::Dynamic { id }],
node_paths: &[&[0]],
attr_paths: &[],
..
} => {
let node = &vnode.dynamic_nodes[id];
match node {
DynamicNode::Text(text) => Some(text.value.clone()),
_ => {
tracing::error!("Error while rendering {component}: The children of {component} must be a single text node. It cannot be a component, if statement, loop, or a fragment");
None
}
}
}
_ => {
tracing::error!(
"Error while rendering title: The children of title must be a single text node"
);
None
}
}
}
#[derive(Clone, Props, PartialEq)]
pub struct TitleProps {
/// The contents of the title tag. The children must be a single text node.
children: Element,
}
/// Render the title of the page. On web renderers, this will set the [title](crate::elements::title) in the head. On desktop, it will set the window title.
///
/// Unlike most head components, the Title can be modified after the first render. Only the latest update to the title will be reflected if multiple title components are rendered.
///
///
/// The children of the title component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the title will not be updated.
///
/// # Example
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// fn App() -> Element {
/// rsx! {
/// // You can use the Title component to render a title tag into the head of the page or window
/// Title { "My Page" }
/// }
/// }
/// ```
#[component]
pub fn Title(props: TitleProps) -> Element {
let children = props.children;
let Some(text) = extract_single_text_node(&children, "Title") else {
return rsx! {};
};
// Update the title as it changes. NOTE: We don't use use_effect here because we need this to run on the server
let document = use_hook(document);
let last_text = use_hook(|| {
// Set the title initially
document.set_title(text.clone());
Rc::new(RefCell::new(text.clone()))
});
// If the text changes, update the title
let mut last_text = last_text.borrow_mut();
if text != *last_text {
document.set_title(text.clone());
*last_text = text;
}
rsx! {}
}
/// Props for the [`Meta`] component
#[derive(Clone, Props, PartialEq)]
pub struct MetaProps {
pub property: Option<String>,
pub name: Option<String>,
pub charset: Option<String>,
pub http_equiv: Option<String>,
pub content: Option<String>,
}
impl MetaProps {
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
let mut attributes = Vec::new();
if let Some(property) = &self.property {
attributes.push(("property", property.clone()));
}
if let Some(name) = &self.name {
attributes.push(("name", name.clone()));
}
if let Some(charset) = &self.charset {
attributes.push(("charset", charset.clone()));
}
if let Some(http_equiv) = &self.http_equiv {
attributes.push(("http-equiv", http_equiv.clone()));
}
if let Some(content) = &self.content {
attributes.push(("content", content.clone()));
}
attributes
}
}
/// Render a [`meta`](crate::elements::meta) tag into the head of the page.
///
/// # Example
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// fn RedirectToDioxusHomepageWithoutJS() -> Element {
/// rsx! {
/// // You can use the meta component to render a meta tag into the head of the page
/// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
/// Meta {
/// http_equiv: "refresh",
/// content: "10;url=https://dioxuslabs.com",
/// }
/// }
/// }
/// ```
///
/// <div class="warning">
///
/// Any updates to the props after the first render will not be reflected in the head.
///
/// </div>
#[component]
pub fn Meta(props: MetaProps) -> Element {
use_update_warning(&props, "Meta {}");
use_hook(|| {
let document = document();
document.create_meta(props);
});
rsx! {}
}
#[derive(Clone, Props, PartialEq)]
pub struct ScriptProps {
/// The contents of the script tag. If present, the children must be a single text node.
pub children: Element,
/// Scripts are deduplicated by their src attribute
pub src: Option<String>,
pub defer: Option<bool>,
pub crossorigin: Option<String>,
pub fetchpriority: Option<String>,
pub integrity: Option<String>,
pub nomodule: Option<bool>,
pub nonce: Option<String>,
pub referrerpolicy: Option<String>,
pub r#type: Option<String>,
}
impl ScriptProps {
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
let mut attributes = Vec::new();
if let Some(defer) = &self.defer {
attributes.push(("defer", defer.to_string()));
}
if let Some(crossorigin) = &self.crossorigin {
attributes.push(("crossorigin", crossorigin.clone()));
}
if let Some(fetchpriority) = &self.fetchpriority {
attributes.push(("fetchpriority", fetchpriority.clone()));
}
if let Some(integrity) = &self.integrity {
attributes.push(("integrity", integrity.clone()));
}
if let Some(nomodule) = &self.nomodule {
attributes.push(("nomodule", nomodule.to_string()));
}
if let Some(nonce) = &self.nonce {
attributes.push(("nonce", nonce.clone()));
}
if let Some(referrerpolicy) = &self.referrerpolicy {
attributes.push(("referrerpolicy", referrerpolicy.clone()));
}
if let Some(r#type) = &self.r#type {
attributes.push(("type", r#type.clone()));
}
attributes
}
pub fn script_contents(&self) -> Option<String> {
extract_single_text_node(&self.children, "Script")
}
}
/// Render a [`script`](crate::elements::script) tag into the head of the page.
///
///
/// If present, the children of the script component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the script will not be added.
///
///
/// Any scripts you add will be deduplicated by their `src` attribute (if present).
///
/// # Example
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// fn LoadScript() -> Element {
/// rsx! {
/// // You can use the Script component to render a script tag into the head of the page
/// Script {
/// src: manganis::mg!(file("./assets/script.js")),
/// }
/// }
/// }
/// ```
///
/// <div class="warning">
///
/// Any updates to the props after the first render will not be reflected in the head.
///
/// </div>
#[component]
pub fn Script(props: ScriptProps) -> Element {
use_update_warning(&props, "Script {}");
use_hook(|| {
if let Some(src) = &props.src {
if !should_insert_script(src) {
return;
}
}
let document = document();
document.create_script(props);
});
rsx! {}
}
#[derive(Clone, Props, PartialEq)]
pub struct StyleProps {
/// Styles are deduplicated by their href attribute
pub href: Option<String>,
pub media: Option<String>,
pub nonce: Option<String>,
pub title: Option<String>,
/// The contents of the style tag. If present, the children must be a single text node.
pub children: Element,
}
impl StyleProps {
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
let mut attributes = Vec::new();
if let Some(href) = &self.href {
attributes.push(("href", href.clone()));
}
if let Some(media) = &self.media {
attributes.push(("media", media.clone()));
}
if let Some(nonce) = &self.nonce {
attributes.push(("nonce", nonce.clone()));
}
if let Some(title) = &self.title {
attributes.push(("title", title.clone()));
}
attributes
}
pub fn style_contents(&self) -> Option<String> {
extract_single_text_node(&self.children, "Title")
}
}
/// Render a [`style`](crate::elements::style) tag into the head of the page.
///
/// If present, the children of the style component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the style will not be added.
///
/// # Example
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// fn RedBackground() -> Element {
/// rsx! {
/// // You can use the style component to render a style tag into the head of the page
/// // This style tag will set the background color of the page to red
/// Style {
/// r#"
/// body {{
/// background-color: red;
/// }}
/// "#,
/// }
/// }
/// }
/// ```
///
/// <div class="warning">
///
/// Any updates to the props after the first render will not be reflected in the head.
///
/// </div>
#[component]
pub fn Style(props: StyleProps) -> Element {
use_update_warning(&props, "Style {}");
use_hook(|| {
if let Some(href) = &props.href {
if !should_insert_style(href) {
return;
}
}
let document = document();
document.create_style(props);
});
rsx! {}
}
use super::*;
#[derive(Clone, Props, PartialEq)]
pub struct LinkProps {
pub rel: Option<String>,
pub media: Option<String>,
pub title: Option<String>,
pub disabled: Option<bool>,
pub r#as: Option<String>,
pub sizes: Option<String>,
/// Links are deduplicated by their href attribute
pub href: Option<String>,
pub crossorigin: Option<String>,
pub referrerpolicy: Option<String>,
pub fetchpriority: Option<String>,
pub hreflang: Option<String>,
pub integrity: Option<String>,
pub r#type: Option<String>,
pub blocking: Option<String>,
}
impl LinkProps {
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
let mut attributes = Vec::new();
if let Some(rel) = &self.rel {
attributes.push(("rel", rel.clone()));
}
if let Some(media) = &self.media {
attributes.push(("media", media.clone()));
}
if let Some(title) = &self.title {
attributes.push(("title", title.clone()));
}
if let Some(disabled) = &self.disabled {
attributes.push(("disabled", disabled.to_string()));
}
if let Some(r#as) = &self.r#as {
attributes.push(("as", r#as.clone()));
}
if let Some(sizes) = &self.sizes {
attributes.push(("sizes", sizes.clone()));
}
if let Some(href) = &self.href {
attributes.push(("href", href.clone()));
}
if let Some(crossorigin) = &self.crossorigin {
attributes.push(("crossOrigin", crossorigin.clone()));
}
if let Some(referrerpolicy) = &self.referrerpolicy {
attributes.push(("referrerPolicy", referrerpolicy.clone()));
}
if let Some(fetchpriority) = &self.fetchpriority {
attributes.push(("fetchPriority", fetchpriority.clone()));
}
if let Some(hreflang) = &self.hreflang {
attributes.push(("hrefLang", hreflang.clone()));
}
if let Some(integrity) = &self.integrity {
attributes.push(("integrity", integrity.clone()));
}
if let Some(r#type) = &self.r#type {
attributes.push(("type", r#type.clone()));
}
if let Some(blocking) = &self.blocking {
attributes.push(("blocking", blocking.clone()));
}
attributes
}
}
/// Render a [`link`](crate::elements::link) tag into the head of the page.
///
/// > The [Link](https://docs.rs/dioxus-router/latest/dioxus_router/components/fn.Link.html) component in dioxus router and this component are completely different.
/// > This component links resources in the head of the page, while the router component creates clickable links in the body of the page.
///
/// # Example
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// fn RedBackground() -> Element {
/// rsx! {
/// // You can use the meta component to render a meta tag into the head of the page
/// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
/// head::Link {
/// href: manganis::mg!(file("./assets/style.css")),
/// rel: "stylesheet",
/// }
/// }
/// }
/// ```
///
/// <div class="warning">
///
/// Any updates to the props after the first render will not be reflected in the head.
///
/// </div>
#[doc(alias = "<link>")]
#[component]
pub fn Link(props: LinkProps) -> Element {
use_update_warning(&props, "Link {}");
use_hook(|| {
if let Some(href) = &props.href {
if !should_insert_link(href) {
return;
}
}
let document = document();
document.create_link(props);
});
rsx! {}
}
fn get_or_insert_root_context<T: Default + Clone + 'static>() -> T {
match ScopeId::ROOT.has_context::<T>() {
Some(context) => context,
None => {
let context = T::default();
ScopeId::ROOT.provide_context(context.clone());
context
}
}
}
#[derive(Default, Clone)]
struct LinkContext(DeduplicationContext);
fn should_insert_link(href: &str) -> bool {
get_or_insert_root_context::<LinkContext>()
.0
.should_insert(href)
}
#[derive(Default, Clone)]
struct ScriptContext(DeduplicationContext);
fn should_insert_script(src: &str) -> bool {
get_or_insert_root_context::<ScriptContext>()
.0
.should_insert(src)
}
#[derive(Default, Clone)]
struct StyleContext(DeduplicationContext);
fn should_insert_style(href: &str) -> bool {
get_or_insert_root_context::<StyleContext>()
.0
.should_insert(href)
}
#[derive(Default, Clone)]
struct DeduplicationContext(Rc<RefCell<HashSet<String>>>);
impl DeduplicationContext {
fn should_insert(&self, href: &str) -> bool {
let mut set = self.0.borrow_mut();
let present = set.contains(href);
if !present {
set.insert(href.to_string());
true
} else {
false
}
}
}
+114
View File
@@ -0,0 +1,114 @@
// API inspired by Reacts implementation of head only elements. We use components here instead of elements to simplify internals.
use std::{
rc::Rc,
task::{Context, Poll},
};
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
mod bindings;
#[allow(unused)]
pub use bindings::*;
mod eval;
pub use eval::*;
pub mod head;
pub use head::{Meta, MetaProps, Script, ScriptProps, Style, StyleProps, Title, TitleProps};
fn format_attributes(attributes: &[(&str, String)]) -> String {
let mut formatted = String::from("[");
for (key, value) in attributes {
formatted.push_str(&format!("[{key:?}, {value:?}],"));
}
if formatted.ends_with(',') {
formatted.pop();
}
formatted.push(']');
formatted
}
fn create_element_in_head(
tag: &str,
attributes: &[(&str, String)],
children: Option<String>,
) -> String {
let helpers = include_str!("../js/head.js");
let attributes = format_attributes(attributes);
let children = children
.map(|c| format!("\"{c}\""))
.unwrap_or("null".to_string());
format!(r#"{helpers};window.createElementInHead("{tag}", {attributes}, {children});"#)
}
/// A provider for document-related functionality. By default most methods are driven through [`eval`].
pub trait Document {
/// Create a new evaluator for the document that evaluates JavaScript and facilitates communication between JavaScript and Rust.
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>>;
/// Set the title of the document
fn set_title(&self, title: String) {
self.new_evaluator(format!("document.title = {title:?};"));
}
fn create_meta(&self, props: MetaProps) {
let attributes = props.attributes();
let js = create_element_in_head("meta", &attributes, None);
self.new_evaluator(js);
}
fn create_script(&self, props: ScriptProps) {
let attributes = props.attributes();
let js = create_element_in_head("script", &attributes, props.script_contents());
self.new_evaluator(js);
}
fn create_style(&self, props: StyleProps) {
let attributes = props.attributes();
let js = create_element_in_head("style", &attributes, props.style_contents());
self.new_evaluator(js);
}
fn create_link(&self, props: head::LinkProps) {
let attributes = props.attributes();
let js = create_element_in_head("link", &attributes, None);
self.new_evaluator(js);
}
}
/// The default No-Op document
pub struct NoOpDocument;
impl Document for NoOpDocument {
fn new_evaluator(&self, _js: String) -> GenerationalBox<Box<dyn Evaluator>> {
tracing::error!("Eval is not supported on this platform. If you are using dioxus fullstack, you can wrap your code with `client! {{}}` to only include the code that runs eval in the client bundle.");
UnsyncStorage::owner().insert(Box::new(NoOpEvaluator))
}
}
struct NoOpEvaluator;
impl Evaluator for NoOpEvaluator {
fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
Err(EvalError::Unsupported)
}
fn poll_recv(
&mut self,
_context: &mut Context<'_>,
) -> Poll<Result<serde_json::Value, EvalError>> {
Poll::Ready(Err(EvalError::Unsupported))
}
fn poll_join(
&mut self,
_context: &mut Context<'_>,
) -> Poll<Result<serde_json::Value, EvalError>> {
Poll::Ready(Err(EvalError::Unsupported))
}
}
/// Get the document provider for the current platform or a no-op provider if the platform doesn't document functionality.
pub fn document() -> Rc<dyn Document> {
dioxus_core::prelude::try_consume_context::<Rc<dyn Document>>()
// Create a NoOp provider that always logs an error when trying to evaluate
// That way, we can still compile and run the code without a real provider
.unwrap_or_else(|| Rc::new(NoOpDocument) as Rc<dyn Document>)
}
+8
View File
@@ -655,6 +655,11 @@ builder_constructors! {
title: String DEFAULT, // FIXME
r#type: Mime "type",
integrity: String DEFAULT,
disabled: Bool DEFAULT,
referrerpolicy: ReferrerPolicy DEFAULT,
fetchpriority: FetchPriority DEFAULT,
blocking: Blocking DEFAULT,
r#as: As "as",
};
/// Build a
@@ -665,6 +670,7 @@ builder_constructors! {
content: String DEFAULT,
http_equiv: String "http-equiv",
name: Metadata DEFAULT,
property: Metadata DEFAULT,
};
/// Build a
@@ -1261,6 +1267,8 @@ builder_constructors! {
nonce: Nonce DEFAULT,
src: Uri DEFAULT,
text: String DEFAULT,
fetchpriority: String DEFAULT,
referrerpolicy: String DEFAULT,
r#async: Bool "async",
r#type: String "type", // TODO could be an enum
+1
View File
@@ -0,0 +1 @@
class Channel{pending;waiting;constructor(){this.pending=[],this.waiting=[]}send(data){if(this.waiting.length>0){this.waiting.shift()(data);return}this.pending.push(data)}async recv(){return new Promise((resolve,_reject)=>{if(this.pending.length>0){resolve(this.pending.shift());return}this.waiting.push(resolve)})}}class WeakDioxusChannel{inner;constructor(channel){this.inner=new WeakRef(channel)}rustSend(data){let channel=this.inner.deref();if(channel)channel.rustSend(data)}async rustRecv(){let channel=this.inner.deref();if(channel)return await channel.rustRecv()}}class DioxusChannel{weak(){return new WeakDioxusChannel(this)}}class WebDioxusChannel extends DioxusChannel{js_to_rust;rust_to_js;owner;constructor(owner){super();this.owner=owner,this.js_to_rust=new Channel,this.rust_to_js=new Channel}weak(){return new WeakDioxusChannel(this)}async recv(){return await this.rust_to_js.recv()}send(data){this.js_to_rust.send(data)}rustSend(data){this.rust_to_js.send(data)}async rustRecv(){return await this.js_to_rust.recv()}}export{WebDioxusChannel,WeakDioxusChannel,DioxusChannel,Channel};
+1
View File
@@ -0,0 +1 @@
[10372071913661173523, 8375185156499858125, 4813754958077120784]
+1
View File
@@ -0,0 +1 @@
var createElementInHead=function(tag,attributes,children){const element=document.createElement(tag);for(let[key,value]of attributes)element.setAttribute(key,value);if(children)element.appendChild(document.createTextNode(children));document.head.appendChild(element)};window.createElementInHead=createElementInHead;
+1
View File
@@ -0,0 +1 @@
class Channel{pending;waiting;constructor(){this.pending=[],this.waiting=[]}send(data){if(this.waiting.length>0){this.waiting.shift()(data);return}this.pending.push(data)}async recv(){return new Promise((resolve,_reject)=>{if(this.pending.length>0){resolve(this.pending.shift());return}this.waiting.push(resolve)})}}class WeakDioxusChannel{inner;constructor(channel){this.inner=new WeakRef(channel)}rustSend(data){let channel=this.inner.deref();if(channel)channel.rustSend(data)}async rustRecv(){let channel=this.inner.deref();if(channel)return await channel.rustRecv()}}class DioxusChannel{weak(){return new WeakDioxusChannel(this)}}class QueryParams{id;data;constructor(id,method,data){this.id=id,this.data={method,data}}}window.__msg_queues=window.__msg_queues||[];window.finalizationRegistry=window.finalizationRegistry||new FinalizationRegistry(({id})=>{window.ipc.postMessage(JSON.stringify({method:"query",params:new QueryParams(id,"drop")}))});window.getQuery=function(request_id){return window.__msg_queues[request_id]};window.createQuery=function(request_id){return new NativeDioxusChannel(request_id)};class NativeDioxusChannel extends DioxusChannel{rust_to_js;request_id;constructor(request_id){super();this.rust_to_js=new Channel,this.request_id=request_id,window.__msg_queues[request_id]=this.weak(),window.finalizationRegistry.register(this,{id:request_id})}async recv(){return await this.rust_to_js.recv()}send(data){window.ipc.postMessage(JSON.stringify({method:"query",params:new QueryParams(this.request_id,"send",data)}))}rustSend(data){this.rust_to_js.send(data)}async rustRecv(){}}export{NativeDioxusChannel};
+7 -4
View File
@@ -47,8 +47,8 @@ pub use elements::*;
pub use events::*;
pub use render_template::*;
#[cfg(feature = "eval")]
pub mod eval;
#[cfg(feature = "document")]
pub mod document;
pub mod extensions {
pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
@@ -57,9 +57,12 @@ pub mod extensions {
pub mod prelude {
pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
#[cfg(feature = "document")]
pub use crate::document::{
self, document, eval, head, Document, Meta, MetaProps, Script, ScriptProps, Style,
StyleProps, Title, TitleProps, UseEval,
};
pub use crate::elements::extensions::*;
#[cfg(feature = "eval")]
pub use crate::eval::*;
pub use crate::events::*;
pub use crate::point_interaction::*;
pub use keyboard_types::{self, Code, Key, Location, Modifiers};
+3
View File
@@ -0,0 +1,3 @@
# please dont accidentally run tsc and commit your js in this dir.
*.js
+108
View File
@@ -0,0 +1,108 @@
// Handle communication between rust and evaluating javascript
export class Channel {
pending: any[];
waiting: ((data: any) => void)[];
constructor() {
this.pending = [];
this.waiting = [];
}
send(data: any) {
// If there's a waiting callback, call it
if (this.waiting.length > 0) {
this.waiting.shift()(data);
return;
}
// Otherwise queue the data
this.pending.push(data);
}
async recv(): Promise<any> {
return new Promise((resolve, _reject) => {
// If data already exists, resolve immediately
if (this.pending.length > 0) {
resolve(this.pending.shift());
return;
}
// Otherwise queue the resolve callback
this.waiting.push(resolve);
});
}
}
export class WeakDioxusChannel {
inner: WeakRef<DioxusChannel>;
constructor(channel: DioxusChannel) {
this.inner = new WeakRef(channel);
}
// Send data from rust to javascript
rustSend(data: any) {
let channel = this.inner.deref();
if (channel) {
channel.rustSend(data);
}
}
// Receive data sent from javascript in rust
async rustRecv(): Promise<any> {
let channel = this.inner.deref();
if (channel) {
return await channel.rustRecv();
}
}
}
export abstract class DioxusChannel {
// Return a weak reference to this channel
weak(): WeakDioxusChannel {
return new WeakDioxusChannel(this);
}
// Send data from rust to javascript
abstract rustSend(data: any): void;
// Receive data sent from javascript in rust
abstract rustRecv(): Promise<any>;
}
export class WebDioxusChannel extends DioxusChannel {
js_to_rust: Channel;
rust_to_js: Channel;
owner: any;
constructor(owner: any) {
super();
this.owner = owner;
this.js_to_rust = new Channel();
this.rust_to_js = new Channel();
}
// Return a weak reference to this channel
weak(): WeakDioxusChannel {
return new WeakDioxusChannel(this);
}
// Receive message from Rust
async recv() {
return await this.rust_to_js.recv();
}
// Send message to rust.
send(data: any) {
this.js_to_rust.send(data);
}
// Send data from rust to javascript
rustSend(data: any) {
this.rust_to_js.send(data);
}
// Receive data sent from javascript in rust
async rustRecv(): Promise<any> {
return await this.js_to_rust.recv();
}
}
+19
View File
@@ -0,0 +1,19 @@
// Helper functions for working with the document head
function createElementInHead(
tag: string,
attributes: [string, string][],
children: string | null
): void {
const element = document.createElement(tag);
for (const [key, value] of attributes) {
element.setAttribute(key, value);
}
if (children) {
element.appendChild(document.createTextNode(children));
}
document.head.appendChild(element);
}
// @ts-ignore
window.createElementInHead = createElementInHead;
+86
View File
@@ -0,0 +1,86 @@
import { Channel, DioxusChannel, WeakDioxusChannel } from "./eval";
// In dioxus desktop, eval needs to use the window object to store global state because we evaluate separate snippets of javascript in the browser
declare global {
interface Window {
__msg_queues: WeakDioxusChannel[];
finalizationRegistry: FinalizationRegistry<{ id: number }>;
getQuery(request_id: number): WeakDioxusChannel;
createQuery(request_id: number): NativeDioxusChannel;
}
}
// A message that can be sent to the desktop renderer about a query
class QueryParams {
id: number;
data: { method: "drop" | "send"; data?: any };
constructor(id: number, method: "drop" | "send", data?: any) {
this.id = id;
this.data = { method, data };
}
}
window.__msg_queues = window.__msg_queues || [];
// In dioxus desktop, eval is copy so we cannot run a drop handler. Instead, the drop handler is run after the channel is garbage collected in the javascript side
window.finalizationRegistry =
window.finalizationRegistry ||
new FinalizationRegistry(({ id }) => {
// @ts-ignore - wry gives us this
window.ipc.postMessage(
JSON.stringify({
method: "query",
params: new QueryParams(id, "drop"),
})
);
});
// Get a query from the global state
window.getQuery = function (request_id: number): WeakDioxusChannel {
return window.__msg_queues[request_id];
};
// Create a new query (and insert it into the global state)
window.createQuery = function (request_id: number): NativeDioxusChannel {
return new NativeDioxusChannel(request_id);
};
export class NativeDioxusChannel extends DioxusChannel {
rust_to_js: Channel;
request_id: number;
constructor(request_id: number) {
super();
this.rust_to_js = new Channel();
this.request_id = request_id;
window.__msg_queues[request_id] = this.weak();
window.finalizationRegistry.register(this, { id: request_id });
}
// Receive message from Rust
async recv() {
return await this.rust_to_js.recv();
}
// Send message to rust.
send(data: any) {
// @ts-ignore - wry gives us this
window.ipc.postMessage(
JSON.stringify({
method: "query",
params: new QueryParams(this.request_id, "send", data),
})
);
}
// Send data from rust to javascript
rustSend(data: any) {
this.rust_to_js.send(data);
}
// Receive data sent from javascript in rust. This is a no-op in the native interpreter because the rust code runs remotely
async rustRecv(): Promise<any> {}
}
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "CommonJS",
"lib": [
"ES2015",
"DOM",
"dom",
"dom.iterable",
"ESNext"
],
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
},
"exclude": [
"**/*.spec.ts"
]
}