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:
1 parent
2b219c826c
commit
519ec9d294
91 files changed
+1113
-1044
No files matched your search
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "dioxus-document"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { workspace = true }
|
||||
dioxus-core-types = { workspace = true }
|
||||
dioxus-core-macro = { workspace = true }
|
||||
dioxus-html = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
futures-channel = { workspace = true }
|
||||
futures-util.workspace = true
|
||||
generational-box.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
lazy-js-bundle = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { workspace = true }
|
||||
@@ -0,0 +1 @@
|
||||
// this script is included as an asset!() for a test
|
||||
@@ -0,0 +1 @@
|
||||
/* this stylesheet is included as an asset!() for a test */
|
||||
@@ -0,0 +1,7 @@
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
# 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 `Eval` 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 = document::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 `Eval` 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 = document::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).unwrap();
|
||||
}
|
||||
},
|
||||
"Log Count"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sending data from JavaScript
|
||||
|
||||
The `Eval` 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 `Eval::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 = document::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: i32 = 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
|
||||
// document::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 = document::eval(SCRIPT).await;
|
||||
println!("Count is {:?}", count);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
id: "my-element",
|
||||
"data-count": "123",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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)
|
||||
- [document::Link](crate::document::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
|
||||
document::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
|
||||
document::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
|
||||
document::Meta { name: "description", content: "My Page" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,156 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A context for the document
|
||||
pub type DocumentContext = Arc<dyn Document>;
|
||||
|
||||
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.
|
||||
///
|
||||
/// Provides things like a history API, a title, a way to run JS, and some other basics/essentials used
|
||||
/// by nearly every platform.
|
||||
///
|
||||
/// An integration with some kind of navigation history.
|
||||
///
|
||||
/// Depending on your use case, your implementation may deviate from the described procedure. This
|
||||
/// is fine, as long as both `current_route` and `current_query` match the described format.
|
||||
///
|
||||
/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
|
||||
/// The described behaviors are designed to mimic a web browser, which most users should already
|
||||
/// know. Deviations might confuse them.
|
||||
pub trait Document: 'static {
|
||||
/// Run `eval` against this document, returning an [`Eval`] that can be used to await the result.
|
||||
fn eval(&self, js: String) -> Eval;
|
||||
|
||||
/// Set the title of the document
|
||||
fn set_title(&self, title: String) {
|
||||
self.eval(format!("document.title = {title:?};"));
|
||||
}
|
||||
|
||||
/// Create a new element in the head
|
||||
fn create_head_element(
|
||||
&self,
|
||||
name: &str,
|
||||
attributes: &[(&str, String)],
|
||||
contents: Option<String>,
|
||||
) {
|
||||
self.eval(create_element_in_head(name, attributes, contents));
|
||||
}
|
||||
|
||||
/// Create a new meta tag in the head
|
||||
fn create_meta(&self, props: MetaProps) {
|
||||
let attributes = props.attributes();
|
||||
self.create_head_element("meta", &attributes, None);
|
||||
}
|
||||
|
||||
/// Create a new script tag in the head
|
||||
fn create_script(&self, props: ScriptProps) {
|
||||
let attributes = props.attributes();
|
||||
match (&props.src, props.script_contents()) {
|
||||
// The script has inline contents, render it as a script tag
|
||||
(_, Ok(contents)) => self.create_head_element("script", &attributes, Some(contents)),
|
||||
// The script has a src, render it as a script tag without a body
|
||||
(Some(_), _) => self.create_head_element("script", &attributes, None),
|
||||
// The script has neither contents nor src, log an error
|
||||
(None, Err(err)) => err.log("Script"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new style tag in the head
|
||||
fn create_style(&self, props: StyleProps) {
|
||||
let mut attributes = props.attributes();
|
||||
match (&props.href, props.style_contents()) {
|
||||
// The style has inline contents, render it as a style tag
|
||||
(_, Ok(contents)) => self.create_head_element("style", &attributes, Some(contents)),
|
||||
// The style has a src, render it as a link tag
|
||||
(Some(_), _) => {
|
||||
attributes.push(("type", "text/css".into()));
|
||||
self.create_head_element("link", &attributes, None)
|
||||
}
|
||||
// The style has neither contents nor src, log an error
|
||||
(None, Err(err)) => err.log("Style"),
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a new link tag in the head
|
||||
fn create_link(&self, props: LinkProps) {
|
||||
let attributes = props.attributes();
|
||||
self.create_head_element("link", &attributes, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// A document that does nothing
|
||||
#[derive(Default)]
|
||||
pub struct NoOpDocument;
|
||||
|
||||
impl Document for NoOpDocument {
|
||||
fn eval(&self, _: String) -> Eval {
|
||||
let owner = generational_box::Owner::default();
|
||||
let boxed = owner.insert(Box::new(NoOpEvaluator {}) as Box<dyn Evaluator + 'static>);
|
||||
Eval::new(boxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// An evaluator that does nothing
|
||||
#[derive(Default)]
|
||||
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 std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
|
||||
std::task::Poll::Ready(Err(EvalError::Unsupported))
|
||||
}
|
||||
|
||||
fn poll_join(
|
||||
&mut self,
|
||||
_context: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
|
||||
std::task::Poll::Ready(Err(EvalError::Unsupported))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
use super::*;
|
||||
use crate::document;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
#[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
|
||||
/// document::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()
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct LinkContext(DeduplicationContext);
|
||||
|
||||
fn should_insert_link(href: &str) -> bool {
|
||||
get_or_insert_root_context::<LinkContext>()
|
||||
.0
|
||||
.should_insert(href)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use super::*;
|
||||
use crate::document;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
#[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
|
||||
/// document::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]
|
||||
#[doc(alias = "<meta>")]
|
||||
pub fn Meta(props: MetaProps) -> Element {
|
||||
use_update_warning(&props, "Meta {}");
|
||||
|
||||
use_hook(|| {
|
||||
let document = document();
|
||||
document.create_meta(props);
|
||||
});
|
||||
|
||||
VNode::empty()
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
#![doc = include_str!("../../docs/head.md")]
|
||||
|
||||
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
||||
|
||||
use dioxus_core::{prelude::*, DynamicNode};
|
||||
use dioxus_core_macro::*;
|
||||
|
||||
mod link;
|
||||
pub use link::*;
|
||||
mod meta;
|
||||
pub use meta::*;
|
||||
mod script;
|
||||
pub use script::*;
|
||||
mod style;
|
||||
pub use style::*;
|
||||
mod title;
|
||||
pub use title::*;
|
||||
|
||||
/// 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),
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
use super::*;
|
||||
use crate::document;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
#[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
|
||||
/// document::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()
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct ScriptContext(DeduplicationContext);
|
||||
|
||||
fn should_insert_script(src: &str) -> bool {
|
||||
get_or_insert_root_context::<ScriptContext>()
|
||||
.0
|
||||
.should_insert(src)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use super::*;
|
||||
use crate::document;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
#[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
|
||||
/// document::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()
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct StyleContext(DeduplicationContext);
|
||||
|
||||
fn should_insert_style(href: &str) -> bool {
|
||||
get_or_insert_root_context::<StyleContext>()
|
||||
.0
|
||||
.should_insert(href)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use crate::document;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[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
|
||||
/// document::Title { "My Page" }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component]
|
||||
#[doc(alias = "<title>")]
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// 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),
|
||||
|
||||
/// Represents an error serializing or deserializing the result of an eval
|
||||
Serialization(serde_json::Error),
|
||||
}
|
||||
|
||||
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"),
|
||||
EvalError::Serialization(_) => write!(f, "EvalError::Serialization - there was an error trying to serialize or deserialize the result of an eval"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for EvalError {}
|
||||
@@ -0,0 +1,74 @@
|
||||
#![doc = include_str!("../docs/eval.md")]
|
||||
|
||||
use crate::error::EvalError;
|
||||
use generational_box::GenerationalBox;
|
||||
use std::future::{poll_fn, Future, IntoFuture};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
#[doc = include_str!("../docs/eval.md")]
|
||||
pub struct Eval {
|
||||
evaluator: GenerationalBox<Box<dyn Evaluator>>,
|
||||
}
|
||||
|
||||
impl Eval {
|
||||
/// Create this eval from a dynamic evaluator
|
||||
pub fn new(evaluator: GenerationalBox<Box<dyn Evaluator + 'static>>) -> Self {
|
||||
Self { evaluator }
|
||||
}
|
||||
|
||||
/// Wait until the javascript task is finished and return the result
|
||||
pub async fn join<T: serde::de::DeserializeOwned>(self) -> Result<T, EvalError> {
|
||||
let json_value = poll_fn(|cx| match self.evaluator.try_write() {
|
||||
Ok(mut evaluator) => evaluator.poll_join(cx),
|
||||
Err(_) => Poll::Ready(Err(EvalError::Finished)),
|
||||
})
|
||||
.await?;
|
||||
serde_json::from_value(json_value).map_err(EvalError::Serialization)
|
||||
}
|
||||
|
||||
/// Send a message to the javascript task
|
||||
pub fn send(&self, data: impl serde::Serialize) -> Result<(), EvalError> {
|
||||
match self.evaluator.try_read() {
|
||||
Ok(evaluator) => {
|
||||
evaluator.send(serde_json::to_value(data).map_err(EvalError::Serialization)?)
|
||||
}
|
||||
Err(_) => Err(EvalError::Finished),
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a message from the javascript task
|
||||
pub async fn recv<T: serde::de::DeserializeOwned>(&mut self) -> Result<T, EvalError> {
|
||||
let json_value = poll_fn(|cx| match self.evaluator.try_write() {
|
||||
Ok(mut evaluator) => evaluator.poll_recv(cx),
|
||||
Err(_) => Poll::Ready(Err(EvalError::Finished)),
|
||||
})
|
||||
.await?;
|
||||
serde_json::from_value(json_value).map_err(EvalError::Serialization)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFuture for Eval {
|
||||
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().into_future())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[206827801705263822, 8375185156499858125]
|
||||
@@ -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;
|
||||
@@ -0,0 +1,31 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
mod document;
|
||||
mod elements;
|
||||
mod error;
|
||||
mod eval;
|
||||
|
||||
pub use document::*;
|
||||
pub use elements::*;
|
||||
pub use error::*;
|
||||
pub use eval::*;
|
||||
|
||||
/// 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> {
|
||||
match dioxus_core::prelude::try_consume_context::<Rc<dyn Document>>() {
|
||||
Some(document) => document,
|
||||
None => {
|
||||
tracing::error!(
|
||||
"Unable to find a document in the renderer. Using the default no-op document."
|
||||
);
|
||||
Rc::new(NoOpDocument)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate some javascript in the current document
|
||||
#[doc = include_str!("../docs/eval.md")]
|
||||
#[doc(alias = "javascript")]
|
||||
pub fn eval(script: &str) -> Eval {
|
||||
document().eval(script.to_string())
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# please dont accidentally run tsc and commit your js in this dir.
|
||||
*.js
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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>;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"lib": [
|
||||
"ES2015",
|
||||
"DOM",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"noImplicitAny": true,
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user