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:
1 parent
a490a94ae5
commit
176e67e5b7
65 files changed
+1703
-495
No files matched your search
@@ -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>>,
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
@@ -0,0 +1 @@
|
||||
[10372071913661173523, 8375185156499858125, 4813754958077120784]
|
||||
@@ -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 @@
|
||||
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};
|
||||
@@ -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};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# please dont accidentally run tsc and commit your js in this dir.
|
||||
*.js
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,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> {}
|
||||
}
|
||||
Reference in new issue
Block a user