Move the document trait into a separate crate (#3035)

* add a default head method through eval
* remove the old document trait
* implement document for each platform
* pull out document into a dedicated crate to cut down on shared dependencies
---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2024-10-14 10:33:37 -07:00
1 parent 2b219c826c
commit 519ec9d294
91 files changed
+1113 -1044

No files matched your search

+1 -5
View File
@@ -41,7 +41,7 @@ tokio = { workspace = true, features = ["time"] }
manganis = { workspace = true }
[features]
default = ["serialize", "mounted", "document", "file_engine"]
default = ["serialize", "mounted", "file_engine"]
serialize = [
"dep:serde",
"dep:serde_json",
@@ -51,10 +51,6 @@ serialize = [
"dioxus-core/serialize"
]
mounted = []
document = [
"dep:serde",
"dep:serde_json"
]
file_engine = [
"dep:async-trait",
]
View File
Whitespace-only changes.
View File
Whitespace-only changes.
-7
View File
@@ -1,7 +0,0 @@
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/head.ts", "./src/js/head.js")
.run();
}
-125
View File
@@ -1,125 +0,0 @@
# Communicating with JavaScript
You can use the `eval` function to execute JavaScript code in your application with the desktop, mobile, web or liveview renderers. Eval takes a block of JavaScript code (that may be asynchronous) and returns a `UseEval` object that you can use to send data to the JavaScript code and receive data from it.
<div class="warning">
## Safety
Please be careful when executing JavaScript code with `eval`. You should only execute code that you trust. **This applies especially to web targets, where the JavaScript context has access to most, if not all of your application data.** Running untrusted code can lead to a [cross-site scripting](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting) (XSS) vulnerability.
</div>
```rust
use dioxus::prelude::*;
fn App() -> Element {
rsx! {
button {
onclick: move |_| async move {
// Eval is a global function you can use anywhere inside Dioxus. It will execute the given JavaScript code.
let result = eval(r#"console.log("Hello World");
return "Hello World";"#);
// You can use the `await` keyword to wait for the result of the JavaScript code.
println!("{:?}", result.await);
},
"Log Hello World"
}
}
}
```
## Sending data to JavaScript
When you execute JavaScript code with `eval`, you can pass data to it by formatting the value into the JavaScript code or sending values to the `UseEval` channel.
```rust
use dioxus::prelude::*;
fn app() -> Element {
rsx! {
button {
onclick: move |_| {
// You can pass initial data to the eval function by formatting it into the JavaScript code.
const LOOP_COUNT: usize = 10;
let eval = eval(&format!(r#"for(let i = 0; i < {LOOP_COUNT}; i++) {{
// You can receive values asynchronously with the the `await dioxus.recv()` method.
let value = await dioxus.recv();
console.log("Received", value);
}}"#));
// You can send values from rust to the JavaScript code with the `send` method on the object returned by `eval`.
for i in 0..LOOP_COUNT {
eval.send(i.into()).unwrap();
}
},
"Log Count"
}
}
}
```
## Sending data from JavaScript
The `UseEval` struct also contains methods for receiving values you send from JavaScript. You can use the `dioxus.send()` method to send values to the JavaScript code and the `UseEval::recv()` method to receive values from the JavaScript code.
```rust
use dioxus::prelude::*;
fn app() -> Element {
rsx! {
button {
onclick: move |_| async move {
// You can send values from rust to the JavaScript code by using the `send` method on the object returned by `eval`.
let mut eval = eval(r#"for(let i = 0; i < 10; i++) {
// You can send values asynchronously with the `dioxus.send()` method.
dioxus.send(i);
}"#);
// You can receive values from the JavaScript code with the `recv` method on the object returned by `eval`.
for _ in 0..10 {
let value = eval.recv().await.unwrap();
println!("Received {}", value);
}
},
"Log Count"
}
}
}
```
## Interacting with the DOM with Eval
You can also use the `eval` function to execute JavaScript code that reads or modifies the DOM. If you want to interact with the mounted DOM, you need to use `eval` inside the [`dioxus_hooks::use_effect`] hook which runs after the component has been mounted.
```rust
use dioxus::prelude::*;
const SCRIPT: &str = r#"
let element = document.getElementById("my-element");
element.innerHTML = "Hello World";
return element.getAttribute("data-count");
"#;
fn app() -> Element {
// ❌ You shouldn't run eval in the body of a component. This will run before the component has been mounted
// eval(SCRIPT);
// ✅ You should run eval inside an effect or event. This will run after the component has been mounted
use_effect(move || {
spawn(async {
let count = eval(SCRIPT).await;
println!("Count is {:?}", count);
});
});
rsx! {
div {
id: "my-element",
"data-count": "123",
}
}
}
```
-59
View File
@@ -1,59 +0,0 @@
# 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" }
}
}
}
}
```
-134
View File
@@ -1,134 +0,0 @@
#![allow(clippy::await_holding_refcell_ref)]
#![doc = include_str!("../../docs/eval.md")]
use dioxus_core::prelude::*;
use generational_box::GenerationalBox;
use std::error::Error;
use std::fmt::Display;
use std::future::{poll_fn, Future, IntoFuture};
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
use super::document;
/// The platform's evaluator.
pub trait Evaluator {
/// Sends a message to the evaluated JavaScript.
fn send(&self, data: serde_json::Value) -> Result<(), EvalError>;
/// Receive any queued messages from the evaluated JavaScript.
fn poll_recv(
&mut self,
context: &mut Context<'_>,
) -> Poll<Result<serde_json::Value, EvalError>>;
/// Gets the return value of the JavaScript
fn poll_join(
&mut self,
context: &mut Context<'_>,
) -> Poll<Result<serde_json::Value, EvalError>>;
}
type EvalCreator = Rc<dyn Fn(&str) -> UseEval>;
/// Get a struct that can execute any JavaScript.
///
/// # Safety
///
/// Please be very careful with this function. A script with too many dynamic
/// parts is practically asking for a hacker to find an XSS vulnerability in
/// it. **This applies especially to web targets, where the JavaScript context
/// has access to most, if not all of your application data.**
#[must_use]
pub fn eval_provider() -> EvalCreator {
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(alias = "javascript")]
pub fn eval(script: &str) -> UseEval {
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")]
#[derive(Clone, Copy)]
pub struct UseEval {
evaluator: GenerationalBox<Box<dyn Evaluator>>,
}
impl UseEval {
/// Creates a new UseEval
pub fn new(evaluator: GenerationalBox<Box<dyn Evaluator + 'static>>) -> Self {
Self { evaluator }
}
/// Sends a [`serde_json::Value`] to the evaluated JavaScript.
pub fn send(&self, data: serde_json::Value) -> Result<(), EvalError> {
match self.evaluator.try_read() {
Ok(evaluator) => evaluator.send(data),
Err(_) => Err(EvalError::Finished),
}
}
/// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript.
pub async fn recv(&mut self) -> Result<serde_json::Value, EvalError> {
poll_fn(|cx| match self.evaluator.try_write() {
Ok(mut evaluator) => evaluator.poll_recv(cx),
Err(_) => Poll::Ready(Err(EvalError::Finished)),
})
.await
}
/// Gets the return value of the evaluated JavaScript.
pub async fn join(self) -> Result<serde_json::Value, EvalError> {
poll_fn(|cx| match self.evaluator.try_write() {
Ok(mut evaluator) => evaluator.poll_join(cx),
Err(_) => Poll::Ready(Err(EvalError::Finished)),
})
.await
}
}
impl IntoFuture for UseEval {
type Output = Result<serde_json::Value, EvalError>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.join())
}
}
/// Represents an error when evaluating JavaScript
#[derive(Debug)]
#[non_exhaustive]
pub enum EvalError {
/// The platform does not support evaluating JavaScript.
Unsupported,
/// The provided JavaScript has already been ran.
Finished,
/// The provided JavaScript is not valid and can't be ran.
InvalidJs(String),
/// Represents an error communicating between JavaScript and Rust.
Communication(String),
}
impl Display for EvalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EvalError::Unsupported => write!(f, "EvalError::Unsupported - eval is not supported on the current platform"),
EvalError::Finished => write!(f, "EvalError::Finished - eval has already ran"),
EvalError::InvalidJs(_) => write!(f, "EvalError::InvalidJs - the provided javascript is invalid"),
EvalError::Communication(_) => write!(f, "EvalError::Communication - there was an error trying to communicate with between javascript and rust"),
}
}
}
impl Error for EvalError {}
-562
View File
@@ -1,562 +0,0 @@
#![doc = include_str!("../../docs/head.md")]
use std::{cell::RefCell, collections::HashSet, rc::Rc};
use crate as dioxus_elements;
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 ");
}
}
}
/// An error that can occur when extracting a single text node from a component
pub enum ExtractSingleTextNodeError<'a> {
/// The node contained an render error, so we can't extract the text node
RenderError(&'a RenderError),
/// There was only one child, but it wasn't a text node
NonTextNode,
/// There is multiple child nodes
NonTemplate,
}
impl ExtractSingleTextNodeError<'_> {
/// Log a warning depending on the error
pub fn log(&self, component: &str) {
match self {
ExtractSingleTextNodeError::RenderError(err) => {
tracing::error!("Error while rendering {component}: {err}");
}
ExtractSingleTextNodeError::NonTextNode => {
tracing::error!(
"Error while rendering {component}: The children of {component} must be a single text node"
);
}
ExtractSingleTextNodeError::NonTemplate => {
tracing::error!(
"Error while rendering {component}: The children of {component} must be a single text node"
);
}
}
}
}
fn extract_single_text_node(children: &Element) -> Result<String, ExtractSingleTextNodeError<'_>> {
let vnode = match children {
Element::Ok(vnode) => vnode,
Element::Err(err) => {
return Err(ExtractSingleTextNodeError::RenderError(err));
}
};
// The title's children must be in one of two forms:
// 1. rsx! { "static text" }
// 2. rsx! { "title: {dynamic_text}" }
match vnode.template {
// rsx! { "static text" }
Template {
roots: &[TemplateNode::Text { text }],
node_paths: &[],
attr_paths: &[],
..
} => Ok(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) => Ok(text.value.clone()),
_ => Err(ExtractSingleTextNodeError::NonTextNode),
}
}
_ => Err(ExtractSingleTextNodeError::NonTemplate),
}
}
#[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 text = match extract_single_text_node(&children) {
Ok(text) => text,
Err(err) => {
err.log("Title");
return VNode::empty();
}
};
// 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;
}
VNode::empty()
}
#[non_exhaustive]
/// 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>,
#[props(extends = meta, extends = GlobalAttributes)]
pub additional_attributes: Vec<Attribute>,
}
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);
});
VNode::empty()
}
#[non_exhaustive]
#[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>,
#[props(extends = script, extends = GlobalAttributes)]
pub additional_attributes: Vec<Attribute>,
}
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()));
}
if let Some(src) = &self.src {
attributes.push(("src", src.clone()));
}
attributes
}
pub fn script_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
extract_single_text_node(&self.children)
}
}
/// 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: asset!("./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);
});
VNode::empty()
}
#[non_exhaustive]
#[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,
#[props(extends = style, extends = GlobalAttributes)]
pub additional_attributes: Vec<Attribute>,
}
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) -> Result<String, ExtractSingleTextNodeError<'_>> {
extract_single_text_node(&self.children)
}
}
/// 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);
});
VNode::empty()
}
use super::*;
#[non_exhaustive]
#[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>,
#[props(extends = link, extends = GlobalAttributes)]
pub additional_attributes: Vec<Attribute>,
}
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: asset!("./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);
});
VNode::empty()
}
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
}
}
}
-162
View File
@@ -1,162 +0,0 @@
// 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};
#[allow(unused)]
mod eval;
pub use eval::*;
pub mod head;
pub use head::{Meta, MetaProps, Script, ScriptProps, Style, StyleProps, Title, TitleProps};
fn format_string_for_js(s: &str) -> String {
let escaped = s
.replace('\\', "\\\\")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('"', "\\\"");
format!("\"{escaped}\"")
}
fn format_attributes(attributes: &[(&str, String)]) -> String {
let mut formatted = String::from("[");
for (key, value) in attributes {
formatted.push_str(&format!(
"[{}, {}],",
format_string_for_js(key),
format_string_for_js(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
.as_deref()
.map(format_string_for_js)
.unwrap_or("null".to_string());
let tag = format_string_for_js(tag);
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:?};"));
}
/// Create a new meta tag
fn create_meta(&self, props: MetaProps) {
let attributes = props.attributes();
let js = create_element_in_head("meta", &attributes, None);
self.new_evaluator(js);
}
/// Create a new script tag
fn create_script(&self, props: ScriptProps) {
let attributes = props.attributes();
let js = match (&props.src, props.script_contents()) {
// The script has inline contents, render it as a script tag
(_, Ok(contents)) => create_element_in_head("script", &attributes, Some(contents)),
// The script has a src, render it as a script tag without a body
(Some(_), _) => create_element_in_head("script", &attributes, None),
// The script has neither contents nor src, log an error
(None, Err(err)) => {
err.log("Script");
return;
}
};
self.new_evaluator(js);
}
/// Create a new style tag
fn create_style(&self, props: StyleProps) {
let mut attributes = props.attributes();
let js = match (&props.href, props.style_contents()) {
// The style has inline contents, render it as a style tag
(_, Ok(contents)) => create_element_in_head("style", &attributes, Some(contents)),
// The style has a src, render it as a link tag
(Some(_), _) => {
attributes.push(("type", "text/css".into()));
create_element_in_head("link", &attributes, None)
}
// The style has neither contents nor src, log an error
(None, Err(err)) => {
err.log("Style");
return;
}
};
self.new_evaluator(js);
}
/// Create a new link tag
fn create_link(&self, props: head::LinkProps) {
let attributes = props.attributes();
let js = create_element_in_head("link", &attributes, None);
self.new_evaluator(js);
}
/// Get a reference to the document as `dyn Any`
fn as_any(&self) -> &dyn std::any::Any;
}
/// 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))
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
/// The default No-Op evaluator
pub 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>)
}
-1
View File
@@ -1 +0,0 @@
[206827801705263822, 8375185156499858125]
-1
View File
@@ -1 +0,0 @@
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;
-8
View File
@@ -41,9 +41,6 @@ pub use elements::*;
pub use events::*;
pub use render_template::*;
#[cfg(feature = "document")]
pub mod document;
pub mod extensions {
pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
pub use crate::elements::extensions::*;
@@ -51,11 +48,6 @@ 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::*;
pub use crate::events::*;
pub use crate::point_interaction::*;
-3
View File
@@ -1,3 +0,0 @@
# please dont accidentally run tsc and commit your js in this dir.
*.js
-70
View File
@@ -1,70 +0,0 @@
// 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>;
}
-19
View File
@@ -1,19 +0,0 @@
// 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;