Repository: DioxusLabs/dioxus (language: rust)

Issue:
document::Script async is not enabled.
**Problem**

document::Script async is not enabled.

<!-- A clear and concise description of what the bug is. -->

**Steps To Reproduce**

Steps to reproduce the behavior:

- Code for src/main.rs 

  ```rust
  rsx! {
      Script {
          r#async: true,
          src: "https://www.googletagmanager.com/gtag/js?id=G-ABCD",
      }
      Script {
          r#async: true,
          src: asset!("/assets/gtag.js"),
          r#type: "text/javascript",
      }
  }
  ```

- Execute `dx serve` command
- Check the html head tag.

  ```html
  <script src="https://www.googletagmanager.com/gtag/js?id=G-ABCD"></script>
  <script type="text/javascript" src="/assets/gtag-c45b1667d1b92d03.js"></script>
  ```
  
**Expected behavior**

```html
<script src="https://www.googletagmanager.com/gtag/js?id=G-ABCD" async="true"></script>
<script type="text/javascript" src="/assets/gtag-c45b1667d1b92d03.js" async="true"></script>
```
<!-- A clear and concise description of what you expected to happen. -->

**Screenshots**

<!-- If applicable, add screenshots to help explain your problem. -->

**Environment:**
 - Dioxus version:  <!-- e.g. v0.17, `master` --> 0.6.1
 - Rust version:    <!-- e.g. 1.43.0, `nightly` --> rustc 1.84.0
 - OS info:         <!-- e.g. MacOS --> Linux
 - App platform:    <!-- e.g. `web`, `desktop` --> web

**Questionnaire**
<!-- If you feel up to the challenge, please uncomment one of the lines below: -->

<!-- I'm interested in fixing this myself but don't know where to start -->
<!-- I would like to fix and I have a solution -->
<!-- I don't have time to fix this right now, but maybe later -->


Discussion / hints:

Fix patch:
diff --git a/packages/document/src/elements/link.rs b/packages/document/src/elements/link.rs
index 15bf8ac320..215bd8a8a4 100644
--- a/packages/document/src/elements/link.rs
+++ b/packages/document/src/elements/link.rs
@@ -28,6 +28,7 @@ impl LinkProps {
     /// Get all the attributes for the link tag
     pub fn attributes(&self) -> Vec<(&'static str, String)> {
         let mut attributes = Vec::new();
+        extend_attributes(&mut attributes, &self.additional_attributes);
         if let Some(rel) = &self.rel {
             attributes.push(("rel", rel.clone()));
         }
diff --git a/packages/document/src/elements/meta.rs b/packages/document/src/elements/meta.rs
index 695718eb09..f799653534 100644
--- a/packages/document/src/elements/meta.rs
+++ b/packages/document/src/elements/meta.rs
@@ -19,6 +19,7 @@ impl MetaProps {
     /// Get all the attributes for the meta tag
     pub fn attributes(&self) -> Vec<(&'static str, String)> {
         let mut attributes = Vec::new();
+        extend_attributes(&mut attributes, &self.additional_attributes);
         if let Some(property) = &self.property {
             attributes.push(("property", property.clone()));
         }
diff --git a/packages/document/src/elements/mod.rs b/packages/document/src/elements/mod.rs
index e9fbbc4a46..05753b230c 100644
--- a/packages/document/src/elements/mod.rs
+++ b/packages/document/src/elements/mod.rs
@@ -124,3 +124,26 @@ impl DeduplicationContext {
         }
     }
 }
+
+/// Extend a list of string attributes with a list of dioxus attribute
+pub(crate) fn extend_attributes(
+    attributes: &mut Vec<(&'static str, String)>,
+    additional_attributes: &[Attribute],
+) {
+    for additional_attribute in additional_attributes {
+        let attribute_value_as_string = match &additional_attribute.value {
+            dioxus_core::AttributeValue::Text(v) => v.to_string(),
+            dioxus_core::AttributeValue::Float(v) => v.to_string(),
+            dioxus_core::AttributeValue::Int(v) => v.to_string(),
+            dioxus_core::AttributeValue::Bool(v) => v.to_string(),
+            dioxus_core::AttributeValue::Listener(_) | dioxus_core::AttributeValue::Any(_) => {
+                tracing::error!("document::* elements do not support event listeners or any value attributes. Expected displayable attribute, found {:?}", additional_attribute.value);
+                continue;
+            }
+            dioxus_core::AttributeValue::None => {
+                continue;
+            }
+        };
+        attributes.push((additional_attribute.name, attribute_value_as_string));
+    }
+}
diff --git a/packages/document/src/elements/script.rs b/packages/document/src/elements/script.rs
index c7b8d1044a..5737b1dc11 100644
--- a/packages/document/src/elements/script.rs
+++ b/packages/document/src/elements/script.rs
@@ -25,6 +25,7 @@ impl ScriptProps {
     /// Get all the attributes for the script tag
     pub fn attributes(&self) -> Vec<(&'static str, String)> {
         let mut attributes = Vec::new();
+        extend_attributes(&mut attributes, &self.additional_attributes);
         if let Some(defer) = &self.defer {
             attributes.push(("defer", defer.to_string()));
         }
diff --git a/packages/document/src/elements/style.rs b/packages/document/src/elements/style.rs
index b924291211..bd85e799b8 100644
--- a/packages/document/src/elements/style.rs
+++ b/packages/document/src/elements/style.rs
@@ -20,6 +20,7 @@ impl StyleProps {
     /// Get all the attributes for the style tag
     pub fn attributes(&self) -> Vec<(&'static str, String)> {
         let mut attributes = Vec::new();
+        extend_attributes(&mut attributes, &self.additional_attributes);
         if let Some(href) = &self.href {
             attributes.push(("href", href.clone()));
         }
diff --git a/packages/fullstack/src/document/server.rs b/packages/fullstack/src/document/server.rs
index dacc752176..067dc1889a 100644
--- a/packages/fullstack/src/document/server.rs
+++ b/packages/fullstack/src/document/server.rs
@@ -88,7 +88,7 @@ impl Document for ServerDocument {
                 http_equiv: props.http_equiv,
                 content: props.content,
                 property: props.property,
-                ..props.additional_attributes
+                ..props.additional_attributes,
             }
         });
     }
@@ -142,6 +142,7 @@ impl Document for ServerDocument {
                 integrity: props.integrity,
                 r#type: props.r#type,
                 blocking: props.blocking,
+                ..props.additional_attributes,
             }
         })
     }


Test patch:
diff --git a/packages/playwright-tests/fullstack.spec.js b/packages/playwright-tests/fullstack.spec.js
index 2e981d7c9e..7732b98a00 100644
--- a/packages/playwright-tests/fullstack.spec.js
+++ b/packages/playwright-tests/fullstack.spec.js
@@ -37,3 +37,34 @@ test("hydration", async ({ page }) => {
   const mountedDiv = page.locator("div.onmounted-div");
   await expect(mountedDiv).toHaveText("onmounted was called 1 times");
 });
+
+test("document elements", async ({ page }) => {
+  await page.goto("http://localhost:9999");
+  // wait until the meta element is mounted
+  const meta = page.locator("meta#meta-head[name='testing']");
+  await meta.waitFor({ state: "attached" });
+  await expect(meta).toHaveAttribute("data", "dioxus-meta-element");
+
+  const link = page.locator("link#link-head[rel='stylesheet']");
+  await link.waitFor({ state: "attached" });
+  await expect(link).toHaveAttribute(
+    "href",
+    "https://fonts.googleapis.com/css?family=Roboto+Mono"
+  );
+
+  const stylesheet = page.locator("link#stylesheet-head[rel='stylesheet']");
+  await stylesheet.waitFor({ state: "attached" });
+  await expect(stylesheet).toHaveAttribute(
+    "href",
+    "https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic"
+  );
+
+  const script = page.locator("script#script-head");
+  await script.waitFor({ state: "attached" });
+  await expect(script).toHaveAttribute("async", "true");
+
+  const style = page.locator("style#style-head");
+  await style.waitFor({ state: "attached" });
+  const main = page.locator("#main");
+  await expect(main).toHaveCSS("font-family", "Roboto");
+});
diff --git a/packages/playwright-tests/fullstack/src/main.rs b/packages/playwright-tests/fullstack/src/main.rs
index dba72c9390..1b2feba66b 100644
--- a/packages/playwright-tests/fullstack/src/main.rs
+++ b/packages/playwright-tests/fullstack/src/main.rs
@@ -41,6 +41,7 @@ fn app() -> Element {
             Errors {}
         }
         OnMounted {}
+        DocumentElements {}
     }
 }
 
@@ -117,3 +118,19 @@ pub fn ThrowsError() -> Element {
         "success"
     }
 }
+
+/// This component tests the document::* elements pre-rendered on the server
+#[component]
+fn DocumentElements() -> Element {
+    rsx! {
+        document::Meta { id: "meta-head", name: "testing", data: "dioxus-meta-element" }
+        document::Link {
+            id: "link-head",
+            rel: "stylesheet",
+            href: "https://fonts.googleapis.com/css?family=Roboto+Mono"
+        }
+        document::Stylesheet { id: "stylesheet-head", href: "https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic" }
+        document::Script { id: "script-head", async: true, "console.log('hello world');" }
+        document::Style { id: "style-head", "body {{ font-family: 'Roboto'; }}" }
+    }
+}
diff --git a/packages/playwright-tests/web.spec.js b/packages/playwright-tests/web.spec.js
index d5cac90cb7..dca040fcb4 100644
--- a/packages/playwright-tests/web.spec.js
+++ b/packages/playwright-tests/web.spec.js
@@ -124,3 +124,34 @@ test("web-sys closure", async ({ page }) => {
   await page.keyboard.press("Enter");
   await expect(scrollDiv).toHaveText("the keydown event was triggered");
 });
+
+test("document elements", async ({ page }) => {
+  await page.goto("http://localhost:9999");
+  // wait until the meta element is mounted
+  const meta = page.locator("meta#meta-head[name='testing']");
+  await meta.waitFor({ state: "attached" });
+  await expect(meta).toHaveAttribute("data", "dioxus-meta-element");
+
+  const link = page.locator("link#link-head[rel='stylesheet']");
+  await link.waitFor({ state: "attached" });
+  await expect(link).toHaveAttribute(
+    "href",
+    "https://fonts.googleapis.com/css?family=Roboto+Mono"
+  );
+
+  const stylesheet = page.locator("link#stylesheet-head[rel='stylesheet']");
+  await stylesheet.waitFor({ state: "attached" });
+  await expect(stylesheet).toHaveAttribute(
+    "href",
+    "https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic"
+  );
+
+  const script = page.locator("script#script-head");
+  await script.waitFor({ state: "attached" });
+  await expect(script).toHaveAttribute("async", "true");
+
+  const style = page.locator("style#style-head");
+  await style.waitFor({ state: "attached" });
+  const main = page.locator("#main");
+  await expect(main).toHaveCSS("font-family", "Roboto");
+});
diff --git a/packages/playwright-tests/web/src/main.rs b/packages/playwright-tests/web/src/main.rs
index 58163ebd78..e90c858b5d 100644
--- a/packages/playwright-tests/web/src/main.rs
+++ b/packages/playwright-tests/web/src/main.rs
@@ -55,6 +55,7 @@ fn app() -> Element {
         PreventDefault {}
         OnMounted {}
         WebSysClosure {}
+        DocumentElements {}
     }
 }
 
@@ -131,6 +132,22 @@ fn WebSysClosure() -> Element {
     }
 }
 
+/// This component tests the document::* elements
+#[component]
+fn DocumentElements() -> Element {
+    rsx! {
+        document::Meta { id: "meta-head", name: "testing", data: "dioxus-meta-element" }
+        document::Link {
+            id: "link-head",
+            rel: "stylesheet",
+            href: "https://fonts.googleapis.com/css?family=Roboto+Mono"
+        }
+        document::Stylesheet { id: "stylesheet-head", href: "https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic" }
+        document::Script { id: "script-head", async: true, "console.log('hello world');" }
+        document::Style { id: "style-head", "body {{ font-family: 'Roboto'; }}" }
+    }
+}
+
 fn main() {
     tracing_wasm::set_as_global_default_with_config(
         tracing_wasm::WASMLayerConfigBuilder::default()