瀏覽代碼

Merge branch 'master' into jk/documet-everything

Jonathan Kelley 3 年之前
父節點
當前提交
77631bff1f

+ 11 - 0
.github/workflows/macos.yml

@@ -1,6 +1,17 @@
 name: macOS tests
 
 on:
+  push:
+    branches:
+      - master
+    paths:
+      - packages/**
+      - examples/**
+      - src/**
+      - .github/**
+      - lib.rs
+      - Cargo.toml
+
   pull_request:
     types: [opened, synchronize, reopened, ready_for_review]
     branches:

+ 11 - 0
.github/workflows/main.yml

@@ -1,6 +1,17 @@
 name: Rust CI
 
 on:
+  push:
+    branches:
+      - master
+    paths:
+      - packages/**
+      - examples/**
+      - src/**
+      - .github/**
+      - lib.rs
+      - Cargo.toml
+
   pull_request:
     types: [opened, synchronize, reopened, ready_for_review]
     branches:

+ 11 - 0
.github/workflows/windows.yml

@@ -1,6 +1,17 @@
 name: windows
 
 on:
+  push:
+    branches:
+      - master
+    paths:
+      - packages/**
+      - examples/**
+      - src/**
+      - .github/**
+      - lib.rs
+      - Cargo.toml
+
   pull_request:
     types: [opened, synchronize, reopened, ready_for_review]
     branches:

+ 4 - 2
Cargo.toml

@@ -16,8 +16,8 @@ dioxus-core = { path = "./packages/core", version = "^0.1.9" }
 dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true }
 dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true }
 dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true }
-dioxus-rsx = { path = "./packages/rsx", optional = true }
 fermi = { path = "./packages/fermi", version = "^0.1.0", optional = true }
+# dioxus-rsx = { path = "./packages/rsx", optional = true }
 
 dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true }
 dioxus-desktop = { path = "./packages/desktop", version = "^0.1.6", optional = true }
@@ -31,12 +31,14 @@ dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.0.0", o
 [features]
 default = ["macro", "hooks", "html"]
 
-macro = ["dioxus-core-macro", "dioxus-rsx"]
+macro = ["dioxus-core-macro"]
+# macro = ["dioxus-core-macro", "dioxus-rsx"]
 hooks = ["dioxus-hooks"]
 html = ["dioxus-html"]
 ssr = ["dioxus-ssr"]
 web = ["dioxus-web"]
 desktop = ["dioxus-desktop"]
+ayatana = ["dioxus-desktop/ayatana"]
 router = ["dioxus-router"]
 
 [workspace]

+ 2 - 5
README.md

@@ -1,8 +1,5 @@
 <div align="center">
   <h1>Dioxus</h1>
-  <p>
-    <strong>Frontend that scales.</strong>
-  </p>
 </div>
 
 <div align="center">
@@ -160,9 +157,9 @@ You shouldn't use Dioxus if:
 ## Comparison with other Rust UI frameworks
 Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**.
 
-- [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead of React-hooks, no borrowed props, supports SSR (no hydration).
+- [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead, no borrowed props, supports SSR (no hydration), no direct desktop/mobile support.
 - [Percy](https://github.com/chinedufn/percy): Supports SSR but with less emphasis on state management and event handling.
-- [Sycamore](https://github.com/sycamore-rs/sycamore): VDOM-less using fine-grained reactivity, but lacking in ergonomics.
+- [Sycamore](https://github.com/sycamore-rs/sycamore): VDOM-less using fine-grained reactivity, but no direct support for desktop/mobile.
 - [Dominator](https://github.com/Pauan/rust-dominator): Signal-based zero-cost alternative, less emphasis on community and docs.
 - [Azul](https://azul.rs): Fully native HTML/CSS renderer for desktop applications, no support for web/ssr
 

+ 12 - 7
docs/guide/src/hello_world.md

@@ -63,7 +63,7 @@ $ cat Cargo.toml
 [package]
 name = "hello-dioxus"
 version = "0.1.0"
-edition = "2018"
+edition = "2021"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
@@ -81,6 +81,13 @@ $ cargo add dioxus --features desktop
 
 It's very important to add `dioxus` with the `desktop` feature for this example. The `dioxus` crate is a batteries-include crate that combines a bunch of utility crates together, ensuring compatibility of the most important parts of the ecosystem. Under the hood, the `dioxus` crate configures various renderers, hooks, debug tooling, and more. The `desktop` feature ensures the we only depend on the smallest set of required crates to compile and render.
 
+If you system does not provide the `libappindicator3` library, like Debian/bullseye, you can enable the replacement `ayatana` with an additional flag:
+
+```shell
+$ # On Debian/bullseye use:
+$ cargo add dioxus --features desktop --features ayatana
+```
+
 If you plan to develop extensions for the `Dioxus` ecosystem, please use the `dioxus` crate with the `core` feature to limit the amount of dependencies your project brings in.
 
 ### Our first app
@@ -92,10 +99,10 @@ use dioxus::prelude::*;
 
 
 fn main() {
-    dioxus::desktop::launch(App);
+    dioxus::desktop::launch(app);
 }
 
-fn App(cx: Scope) -> Element {
+fn app(cx: Scope) -> Element {
     cx.render(rsx! (
         div { "Hello, world!" }
     ))
@@ -118,14 +125,14 @@ This initialization code launches a Tokio runtime on a helper thread where your
 
 ```rust
 fn main() {
-    dioxus::desktop::launch(App);
+    dioxus::desktop::launch(app);
 }
 ```
 
 Finally, our app. Every component in Dioxus is a function that takes in `Context` and `Props` and returns an `Element`.
 
 ```rust
-fn App(cx: Scope) -> Element {
+fn app(cx: Scope) -> Element {
     cx.render(rsx! {
         div { "Hello, world!" }
     })
@@ -143,5 +150,3 @@ For now, just know that `Scope` lets you store state with hooks and render eleme
 ## Moving on
 
 Congrats! You've built your first desktop application with Dioxus. Next, we're going to learn about the basics of building interactive user interfaces.
-
-

+ 6 - 0
docs/guide/src/setup.md

@@ -52,6 +52,12 @@ Webview Linux apps require WebkitGtk. When distributing, this can be part of you
 sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev
 ```
 
+When using Debian/bullseye `libappindicator3-dev` is no longer available but replaced by `libayatana-appindicator3-dev`.
+
+```
+# on Debian/bullseye use:
+sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatanta-appindicator3-dev
+```
 
 If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/en/docs/get-started/setup-linux).
 

+ 28 - 0
examples/heavy_compute.rs

@@ -0,0 +1,28 @@
+//! This example shows that you can place heavy work on the main thread, and then
+//!
+//! You *should* be using `tokio::spawn_blocking` instead.
+//!
+//! Your app runs in an async runtime (Tokio), so you should avoid blocking
+//! the rendering of the VirtualDom.
+//!
+//!
+
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    // This is discouraged
+    std::thread::sleep(std::time::Duration::from_millis(2_000));
+
+    // This is suggested
+    tokio::task::spawn_blocking(move || {
+        std::thread::sleep(std::time::Duration::from_millis(2_000));
+    });
+
+    cx.render(rsx! {
+        div { "Hello, world!" }
+    })
+}

+ 7 - 5
examples/login_form.rs

@@ -12,7 +12,7 @@ fn app(cx: Scope) -> Element {
     let onsubmit = move |evt: FormEvent| {
         cx.spawn(async move {
             let resp = reqwest::Client::new()
-                .post("http://localhost/login")
+                .post("http://localhost:8080/login")
                 .form(&[
                     ("username", &evt.values["username"]),
                     ("password", &evt.values["password"]),
@@ -22,10 +22,12 @@ fn app(cx: Scope) -> Element {
 
             match resp {
                 // Parse data from here, such as storing a response token
-                Ok(_data) => println!("Login successful"),
+                Ok(_data) => println!("Login successful!"),
 
                 //Handle any errors from the fetch here
-                Err(_err) => println!("Login failed"),
+                Err(_err) => {
+                    println!("Login failed - you need a login server running on localhost:8080.")
+                }
             }
         });
     };
@@ -36,10 +38,10 @@ fn app(cx: Scope) -> Element {
             onsubmit: onsubmit,
             prevent_default: "onsubmit", // Prevent the default behavior of <form> to post
 
-            input { "type": "text" }
+            input { "type": "text", id: "username", name: "username" }
             label { "Username" }
             br {}
-            input { "type": "password" }
+            input { "type": "password", id: "password", name: "password" }
             label { "Password" }
             br {}
             button { "Login" }

+ 23 - 0
examples/textarea.rs

@@ -0,0 +1,23 @@
+// How to use textareas
+
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    let (model, set_model) = use_state(&cx, || String::from("asd"));
+
+    println!("{}", model);
+
+    cx.render(rsx! {
+        textarea {
+            class: "border",
+            rows: "10",
+            cols: "80",
+            value: "{model}",
+            oninput: move |e| set_model(e.value.clone()),
+        }
+    })
+}

+ 1 - 1
examples/todomvc.rs

@@ -129,7 +129,6 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
             label {
                 r#for: "cbg-{todo.id}",
                 onclick: move |_| set_is_editing(true),
-                onfocusout: move |_| set_is_editing(false),
                 "{todo.contents}"
             }
         }
@@ -139,6 +138,7 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
                 value: "{todo.contents}",
                 oninput: move |evt| cx.props.set_todos.make_mut()[&cx.props.id].contents = evt.value.clone(),
                 autofocus: "true",
+                onfocusout: move |_| set_is_editing(false),
                 onkeydown: move |evt| {
                     match evt.key.as_str() {
                         "Enter" | "Escape" | "Tab" => set_is_editing(false),

+ 2 - 2
packages/core-macro/Cargo.toml

@@ -15,8 +15,8 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 proc-macro = true
 
 [dependencies]
-dioxus-rsx = { path = "../rsx" }
-proc-macro-error = "1.0.4"
+# dioxus-rsx = { path = "../rsx" }
+proc-macro-error = "1"
 proc-macro2 = { version = "1.0.6" }
 quote = "1.0"
 syn = { version = "1.0.11", features = ["full", "extra-traits"] }

+ 2 - 1
packages/core-macro/src/lib.rs

@@ -5,6 +5,7 @@ use syn::parse_macro_input;
 mod ifmt;
 mod inlineprops;
 mod props;
+mod rsx;
 
 #[proc_macro]
 pub fn format_args_f(input: TokenStream) -> TokenStream {
@@ -178,7 +179,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
 #[proc_macro_error::proc_macro_error]
 #[proc_macro]
 pub fn rsx(s: TokenStream) -> TokenStream {
-    match syn::parse::<dioxus_rsx::CallBody>(s) {
+    match syn::parse::<rsx::CallBody>(s) {
         Err(err) => err.to_compile_error().into(),
         Ok(stream) => stream.to_token_stream().into(),
     }

+ 234 - 0
packages/core-macro/src/rsx/component.rs

@@ -0,0 +1,234 @@
+//! Parse components into the VComponent VNode
+//! ==========================================
+//!
+//! This parsing path emerges from [`AmbiguousElement`] which supports validation of the vcomponent format.
+//! We can be reasonably sure that whatever enters this parsing path is in the right format.
+//! This feature must support
+//! - [x] Namespaced components
+//! - [x] Fields
+//! - [x] Componentbuilder synax
+//! - [x] Optional commas
+//! - [ ] Children
+//! - [ ] Keys
+//! - [ ] Properties spreading with with `..` syntax
+
+use super::*;
+
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{quote, ToTokens, TokenStreamExt};
+use syn::{
+    ext::IdentExt,
+    parse::{Parse, ParseBuffer, ParseStream},
+    token, Expr, Ident, LitStr, Result, Token,
+};
+
+pub struct Component {
+    pub name: syn::Path,
+    pub body: Vec<ComponentField>,
+    pub children: Vec<BodyNode>,
+    pub manual_props: Option<Expr>,
+}
+
+impl Parse for Component {
+    fn parse(stream: ParseStream) -> Result<Self> {
+        let name = syn::Path::parse_mod_style(stream)?;
+
+        let content: ParseBuffer;
+
+        // if we see a `{` then we have a block
+        // else parse as a function-like call
+        if stream.peek(token::Brace) {
+            syn::braced!(content in stream);
+        } else {
+            syn::parenthesized!(content in stream);
+        }
+
+        let mut body = Vec::new();
+        let mut children = Vec::new();
+        let mut manual_props = None;
+
+        while !content.is_empty() {
+            // if we splat into a component then we're merging properties
+            if content.peek(Token![..]) {
+                content.parse::<Token![..]>()?;
+                manual_props = Some(content.parse::<Expr>()?);
+            } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
+                body.push(content.parse::<ComponentField>()?);
+            } else {
+                children.push(content.parse::<BodyNode>()?);
+            }
+
+            if content.peek(Token![,]) {
+                let _ = content.parse::<Token![,]>();
+            }
+        }
+
+        Ok(Self {
+            name,
+            body,
+            children,
+            manual_props,
+        })
+    }
+}
+
+impl ToTokens for Component {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let name = &self.name;
+
+        let mut has_key = None;
+
+        let builder = match &self.manual_props {
+            Some(manual_props) => {
+                let mut toks = quote! {
+                    let mut __manual_props = #manual_props;
+                };
+                for field in &self.body {
+                    if field.name == "key" {
+                        has_key = Some(field);
+                    } else {
+                        let name = &field.name;
+                        let val = &field.content;
+                        toks.append_all(quote! {
+                            __manual_props.#name = #val;
+                        });
+                    }
+                }
+                toks.append_all(quote! {
+                    __manual_props
+                });
+                quote! {{
+                    #toks
+                }}
+            }
+            None => {
+                let mut toks = quote! { fc_to_builder(#name) };
+                for field in &self.body {
+                    match field.name.to_string().as_str() {
+                        "key" => {
+                            //
+                            has_key = Some(field);
+                        }
+                        _ => toks.append_all(quote! {#field}),
+                    }
+                }
+
+                if !self.children.is_empty() {
+                    let childs = &self.children;
+                    toks.append_all(quote! {
+                        .children(__cx.create_children([ #( #childs ),* ]))
+                    });
+                }
+
+                toks.append_all(quote! {
+                    .build()
+                });
+                toks
+            }
+        };
+
+        let key_token = match has_key {
+            Some(field) => {
+                let inners = &field.content;
+                quote! { Some(format_args_f!(#inners)) }
+            }
+            None => quote! { None },
+        };
+
+        let fn_name = self.name.segments.last().unwrap().ident.to_string();
+
+        tokens.append_all(quote! {
+            __cx.component(
+                #name,
+                #builder,
+                #key_token,
+                #fn_name
+            )
+        })
+    }
+}
+
+// the struct's fields info
+pub struct ComponentField {
+    name: Ident,
+    content: ContentField,
+}
+
+enum ContentField {
+    ManExpr(Expr),
+    Formatted(LitStr),
+    OnHandlerRaw(Expr),
+}
+
+impl ToTokens for ContentField {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        match self {
+            ContentField::ManExpr(e) => e.to_tokens(tokens),
+            ContentField::Formatted(s) => tokens.append_all(quote! {
+                __cx.raw_text(format_args_f!(#s)).0
+            }),
+            ContentField::OnHandlerRaw(e) => tokens.append_all(quote! {
+                __cx.event_handler(#e)
+            }),
+        }
+    }
+}
+
+impl Parse for ComponentField {
+    fn parse(input: ParseStream) -> Result<Self> {
+        let name = Ident::parse_any(input)?;
+        input.parse::<Token![:]>()?;
+
+        if name.to_string().starts_with("on") {
+            let content = ContentField::OnHandlerRaw(input.parse()?);
+            return Ok(Self { name, content });
+        }
+
+        if name == "key" {
+            let content = ContentField::ManExpr(input.parse()?);
+            return Ok(Self { name, content });
+        }
+
+        if input.peek(LitStr) && input.peek2(Token![,]) {
+            let t: LitStr = input.fork().parse()?;
+
+            if is_literal_foramtted(&t) {
+                let content = ContentField::Formatted(input.parse()?);
+                return Ok(Self { name, content });
+            }
+        }
+
+        if input.peek(LitStr) && input.peek2(LitStr) {
+            let item = input.parse::<LitStr>().unwrap();
+            proc_macro_error::emit_error!(item, "This attribute is missing a trailing comma")
+        }
+
+        let content = ContentField::ManExpr(input.parse()?);
+        Ok(Self { name, content })
+    }
+}
+
+impl ToTokens for ComponentField {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let ComponentField { name, content, .. } = self;
+        tokens.append_all(quote! {
+            .#name(#content)
+        })
+    }
+}
+
+fn is_literal_foramtted(lit: &LitStr) -> bool {
+    let s = lit.value();
+    let mut chars = s.chars();
+
+    while let Some(next) = chars.next() {
+        if next == '{' {
+            let nen = chars.next();
+            if nen != Some('{') {
+                return true;
+            }
+        }
+    }
+
+    false
+}

+ 282 - 0
packages/core-macro/src/rsx/element.rs

@@ -0,0 +1,282 @@
+use super::*;
+
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{quote, ToTokens, TokenStreamExt};
+use syn::{
+    parse::{Parse, ParseBuffer, ParseStream},
+    Expr, Ident, LitStr, Result, Token,
+};
+
+// =======================================
+// Parse the VNode::Element type
+// =======================================
+pub struct Element {
+    pub name: Ident,
+    pub key: Option<LitStr>,
+    pub attributes: Vec<ElementAttrNamed>,
+    pub children: Vec<BodyNode>,
+    pub _is_static: bool,
+}
+
+impl Parse for Element {
+    fn parse(stream: ParseStream) -> Result<Self> {
+        let el_name = Ident::parse(stream)?;
+
+        // parse the guts
+        let content: ParseBuffer;
+        syn::braced!(content in stream);
+
+        let mut attributes: Vec<ElementAttrNamed> = vec![];
+        let mut children: Vec<BodyNode> = vec![];
+        let mut key = None;
+        let mut _el_ref = None;
+
+        // parse fields with commas
+        // break when we don't get this pattern anymore
+        // start parsing bodynodes
+        // "def": 456,
+        // abc: 123,
+        loop {
+            // Parse the raw literal fields
+            if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
+                let name = content.parse::<LitStr>()?;
+                let ident = name.clone();
+
+                content.parse::<Token![:]>()?;
+
+                if content.peek(LitStr) && content.peek2(Token![,]) {
+                    let value = content.parse::<LitStr>()?;
+                    attributes.push(ElementAttrNamed {
+                        el_name: el_name.clone(),
+                        attr: ElementAttr::CustomAttrText { name, value },
+                    });
+                } else {
+                    let value = content.parse::<Expr>()?;
+
+                    attributes.push(ElementAttrNamed {
+                        el_name: el_name.clone(),
+                        attr: ElementAttr::CustomAttrExpression { name, value },
+                    });
+                }
+
+                if content.is_empty() {
+                    break;
+                }
+
+                // todo: add a message saying you need to include commas between fields
+                if content.parse::<Token![,]>().is_err() {
+                    proc_macro_error::emit_error!(
+                        ident,
+                        "This attribute is missing a trailing comma"
+                    )
+                }
+                continue;
+            }
+
+            if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
+                let name = content.parse::<Ident>()?;
+                let ident = name.clone();
+
+                let name_str = name.to_string();
+                content.parse::<Token![:]>()?;
+
+                if name_str.starts_with("on") {
+                    attributes.push(ElementAttrNamed {
+                        el_name: el_name.clone(),
+                        attr: ElementAttr::EventTokens {
+                            name,
+                            tokens: content.parse()?,
+                        },
+                    });
+                } else {
+                    match name_str.as_str() {
+                        "key" => {
+                            key = Some(content.parse()?);
+                        }
+                        "classes" => todo!("custom class list not supported yet"),
+                        // "namespace" => todo!("custom namespace not supported yet"),
+                        "node_ref" => {
+                            _el_ref = Some(content.parse::<Expr>()?);
+                        }
+                        _ => {
+                            if content.peek(LitStr) {
+                                attributes.push(ElementAttrNamed {
+                                    el_name: el_name.clone(),
+                                    attr: ElementAttr::AttrText {
+                                        name,
+                                        value: content.parse()?,
+                                    },
+                                });
+                            } else {
+                                attributes.push(ElementAttrNamed {
+                                    el_name: el_name.clone(),
+                                    attr: ElementAttr::AttrExpression {
+                                        name,
+                                        value: content.parse()?,
+                                    },
+                                });
+                            }
+                        }
+                    }
+                }
+
+                if content.is_empty() {
+                    break;
+                }
+
+                // todo: add a message saying you need to include commas between fields
+                if content.parse::<Token![,]>().is_err() {
+                    proc_macro_error::emit_error!(
+                        ident,
+                        "This attribute is missing a trailing comma"
+                    )
+                }
+                continue;
+            }
+
+            break;
+        }
+
+        while !content.is_empty() {
+            if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
+                let ident = content.parse::<LitStr>().unwrap();
+                let name = ident.value();
+                proc_macro_error::emit_error!(
+                    ident, "This attribute `{}` is in the wrong place.", name;
+                    help =
+"All attribute fields must be placed above children elements.
+
+                div {
+                   attr: \"...\",  <---- attribute is above children
+                   div { }       <---- children are below attributes
+                }";
+                )
+            }
+
+            if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
+                let ident = content.parse::<Ident>().unwrap();
+                let name = ident.to_string();
+                proc_macro_error::emit_error!(
+                    ident, "This attribute `{}` is in the wrong place.", name;
+                    help =
+"All attribute fields must be placed above children elements.
+
+                div {
+                   attr: \"...\",  <---- attribute is above children
+                   div { }       <---- children are below attributes
+                }";
+                )
+            }
+
+            children.push(content.parse::<BodyNode>()?);
+            // consume comma if it exists
+            // we don't actually care if there *are* commas after elements/text
+            if content.peek(Token![,]) {
+                let _ = content.parse::<Token![,]>();
+            }
+        }
+
+        Ok(Self {
+            key,
+            name: el_name,
+            attributes,
+            children,
+            _is_static: false,
+        })
+    }
+}
+
+impl ToTokens for Element {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let name = &self.name;
+        let children = &self.children;
+
+        let key = match &self.key {
+            Some(ty) => quote! { Some(format_args_f!(#ty)) },
+            None => quote! { None },
+        };
+
+        let listeners = self
+            .attributes
+            .iter()
+            .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. }));
+
+        let attr = self
+            .attributes
+            .iter()
+            .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. }));
+
+        tokens.append_all(quote! {
+            __cx.element(
+                dioxus_elements::#name,
+                __cx.bump().alloc([ #(#listeners),* ]),
+                __cx.bump().alloc([ #(#attr),* ]),
+                __cx.bump().alloc([ #(#children),* ]),
+                #key,
+            )
+        });
+    }
+}
+
+pub enum ElementAttr {
+    /// attribute: "valuee {}"
+    AttrText { name: Ident, value: LitStr },
+
+    /// attribute: true,
+    AttrExpression { name: Ident, value: Expr },
+
+    /// "attribute": "value {}"
+    CustomAttrText { name: LitStr, value: LitStr },
+
+    /// "attribute": true,
+    CustomAttrExpression { name: LitStr, value: Expr },
+
+    // /// onclick: move |_| {}
+    // EventClosure { name: Ident, closure: ExprClosure },
+    /// onclick: {}
+    EventTokens { name: Ident, tokens: Expr },
+}
+
+pub struct ElementAttrNamed {
+    pub el_name: Ident,
+    pub attr: ElementAttr,
+}
+
+impl ToTokens for ElementAttrNamed {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let ElementAttrNamed { el_name, attr } = self;
+
+        tokens.append_all(match attr {
+            ElementAttr::AttrText { name, value } => {
+                quote! {
+                    dioxus_elements::#el_name.#name(__cx, format_args_f!(#value))
+                }
+            }
+            ElementAttr::AttrExpression { name, value } => {
+                quote! {
+                    dioxus_elements::#el_name.#name(__cx, #value)
+                }
+            }
+            ElementAttr::CustomAttrText { name, value } => {
+                quote! {
+                    __cx.attr( #name, format_args_f!(#value), None, false )
+                }
+            }
+            ElementAttr::CustomAttrExpression { name, value } => {
+                quote! {
+                    __cx.attr( #name, format_args_f!(#value), None, false )
+                }
+            }
+            // ElementAttr::EventClosure { name, closure } => {
+            //     quote! {
+            //         dioxus_elements::on::#name(__cx, #closure)
+            //     }
+            // }
+            ElementAttr::EventTokens { name, tokens } => {
+                quote! {
+                    dioxus_elements::on::#name(__cx, #tokens)
+                }
+            }
+        });
+    }
+}

+ 97 - 0
packages/core-macro/src/rsx/mod.rs

@@ -0,0 +1,97 @@
+//! Parse the root tokens in the rsx!{} macro
+//! =========================================
+//!
+//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing.
+//! This feature must support:
+//! - [x] Optionally rendering if the `in XYZ` pattern is present
+//! - [x] Fragments as top-level element (through ambiguous)
+//! - [x] Components as top-level element (through ambiguous)
+//! - [x] Tags as top-level elements (through ambiguous)
+//! - [x] Good errors if parsing fails
+//!
+//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful.
+
+mod component;
+mod element;
+mod node;
+
+pub mod pretty;
+
+// Re-export the namespaces into each other
+pub use component::*;
+pub use element::*;
+pub use node::*;
+
+// imports
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{quote, ToTokens, TokenStreamExt};
+use syn::{
+    parse::{Parse, ParseStream},
+    Ident, Result, Token,
+};
+
+pub struct CallBody {
+    pub custom_context: Option<Ident>,
+    pub roots: Vec<BodyNode>,
+}
+
+impl Parse for CallBody {
+    fn parse(input: ParseStream) -> Result<Self> {
+        let custom_context = if input.peek(Ident) && input.peek2(Token![,]) {
+            let name = input.parse::<Ident>()?;
+            input.parse::<Token![,]>()?;
+
+            Some(name)
+        } else {
+            None
+        };
+
+        let mut roots = Vec::new();
+
+        while !input.is_empty() {
+            let node = input.parse::<BodyNode>()?;
+
+            if input.peek(Token![,]) {
+                let _ = input.parse::<Token![,]>();
+            }
+
+            roots.push(node);
+        }
+
+        Ok(Self {
+            custom_context,
+            roots,
+        })
+    }
+}
+
+/// Serialize the same way, regardless of flavor
+impl ToTokens for CallBody {
+    fn to_tokens(&self, out_tokens: &mut TokenStream2) {
+        let inner = if self.roots.len() == 1 {
+            let inner = &self.roots[0];
+            quote! { #inner }
+        } else {
+            let childs = &self.roots;
+            quote! { __cx.fragment_root([ #(#childs),* ]) }
+        };
+
+        match &self.custom_context {
+            // The `in cx` pattern allows directly rendering
+            Some(ident) => out_tokens.append_all(quote! {
+                #ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
+                    use dioxus_elements::{GlobalAttributes, SvgAttributes};
+                    #inner
+                }))
+            }),
+
+            // Otherwise we just build the LazyNode wrapper
+            None => out_tokens.append_all(quote! {
+                LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
+                    use dioxus_elements::{GlobalAttributes, SvgAttributes};
+                    #inner
+                })
+            }),
+        };
+    }
+}

+ 84 - 0
packages/core-macro/src/rsx/node.rs

@@ -0,0 +1,84 @@
+use super::*;
+
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{quote, ToTokens, TokenStreamExt};
+use syn::{
+    parse::{Parse, ParseStream},
+    token, Expr, LitStr, Result, Token,
+};
+
+/*
+Parse
+-> div {}
+-> Component {}
+-> component()
+-> "text {with_args}"
+-> (0..10).map(|f| rsx!("asd")),  // <--- notice the comma - must be a complete expr
+*/
+pub enum BodyNode {
+    Element(Element),
+    Component(Component),
+    Text(LitStr),
+    RawExpr(Expr),
+}
+
+impl Parse for BodyNode {
+    fn parse(stream: ParseStream) -> Result<Self> {
+        if stream.peek(LitStr) {
+            return Ok(BodyNode::Text(stream.parse()?));
+        }
+
+        // div {} -> el
+        // Div {} -> comp
+        if stream.peek(syn::Ident) && stream.peek2(token::Brace) {
+            if stream
+                .fork()
+                .parse::<Ident>()?
+                .to_string()
+                .chars()
+                .next()
+                .unwrap()
+                .is_ascii_uppercase()
+            {
+                return Ok(BodyNode::Component(stream.parse()?));
+            } else {
+                return Ok(BodyNode::Element(stream.parse::<Element>()?));
+            }
+        }
+
+        // component() -> comp
+        // ::component {} -> comp
+        // ::component () -> comp
+        if (stream.peek(syn::Ident) && stream.peek2(token::Paren))
+            || (stream.peek(Token![::]))
+            || (stream.peek(Token![:]) && stream.peek2(Token![:]))
+        {
+            return Ok(BodyNode::Component(stream.parse::<Component>()?));
+        }
+
+        // crate::component{} -> comp
+        // crate::component() -> comp
+        if let Ok(pat) = stream.fork().parse::<syn::Path>() {
+            if pat.segments.len() > 1 {
+                return Ok(BodyNode::Component(stream.parse::<Component>()?));
+            }
+        }
+
+        Ok(BodyNode::RawExpr(stream.parse::<Expr>()?))
+    }
+}
+
+impl ToTokens for BodyNode {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        match &self {
+            BodyNode::Element(el) => el.to_tokens(tokens),
+            BodyNode::Component(comp) => comp.to_tokens(tokens),
+            BodyNode::Text(txt) => tokens.append_all(quote! {
+                __cx.text(format_args_f!(#txt))
+            }),
+            BodyNode::RawExpr(exp) => tokens.append_all(quote! {
+                 __cx.fragment_from_iter(#exp)
+            }),
+        }
+    }
+}

+ 1 - 0
packages/core-macro/src/rsx/pretty.rs

@@ -0,0 +1 @@
+//! pretty printer for rsx!

+ 7 - 16
packages/core/src/diff.rs

@@ -852,7 +852,7 @@ impl<'b> DiffState<'b> {
                     nodes_created += self.create_node(new_node);
                 } else {
                     self.diff_node(&old[old_index], new_node);
-                    nodes_created += self.push_all_nodes(new_node);
+                    nodes_created += self.push_all_real_nodes(new_node);
                 }
             }
 
@@ -875,7 +875,7 @@ impl<'b> DiffState<'b> {
                         nodes_created += self.create_node(new_node);
                     } else {
                         self.diff_node(&old[old_index], new_node);
-                        nodes_created += self.push_all_nodes(new_node);
+                        nodes_created += self.push_all_real_nodes(new_node);
                     }
                 }
 
@@ -898,7 +898,7 @@ impl<'b> DiffState<'b> {
                     nodes_created += self.create_node(new_node);
                 } else {
                     self.diff_node(&old[old_index], new_node);
-                    nodes_created += self.push_all_nodes(new_node);
+                    nodes_created += self.push_all_real_nodes(new_node);
                 }
             }
 
@@ -1092,9 +1092,9 @@ impl<'b> DiffState<'b> {
     }
 
     // recursively push all the nodes of a tree onto the stack and return how many are there
-    fn push_all_nodes(&mut self, node: &'b VNode<'b>) -> usize {
+    fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
         match node {
-            VNode::Text(_) | VNode::Placeholder(_) => {
+            VNode::Text(_) | VNode::Placeholder(_) | VNode::Element(_) => {
                 self.mutations.push_root(node.mounted_id());
                 1
             }
@@ -1102,7 +1102,7 @@ impl<'b> DiffState<'b> {
             VNode::Fragment(frag) => {
                 let mut added = 0;
                 for child in frag.children {
-                    added += self.push_all_nodes(child);
+                    added += self.push_all_real_nodes(child);
                 }
                 added
             }
@@ -1110,16 +1110,7 @@ impl<'b> DiffState<'b> {
             VNode::Component(c) => {
                 let scope_id = c.scope.get().unwrap();
                 let root = self.scopes.root_node(scope_id);
-                self.push_all_nodes(root)
-            }
-
-            VNode::Element(el) => {
-                let mut num_on_stack = 0;
-                for child in el.children.iter() {
-                    num_on_stack += self.push_all_nodes(child);
-                }
-                self.mutations.push_root(el.id.get().unwrap());
-                num_on_stack + 1
+                self.push_all_real_nodes(root)
             }
         }
     }

+ 10 - 2
packages/core/src/mutations.rs

@@ -181,6 +181,7 @@ pub enum DomEdit<'bump> {
 
         /// The name of the attribute to remove.
         name: &'static str,
+        ns: Option<&'bump str>,
     },
 }
 
@@ -292,8 +293,15 @@ impl<'a> Mutations<'a> {
     }
 
     pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) {
-        let name = attribute.name;
-        self.edits.push(RemoveAttribute { name, root });
+        let Attribute {
+            name, namespace, ..
+        } = attribute;
+
+        self.edits.push(RemoveAttribute {
+            name,
+            ns: *namespace,
+            root,
+        });
     }
 
     pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) {

+ 14 - 11
packages/core/src/scopes.rs

@@ -11,6 +11,7 @@ use std::{
     future::Future,
     pin::Pin,
     rc::Rc,
+    sync::Arc,
 };
 
 /// for traceability, we use the raw fn pointer to identify the function
@@ -96,11 +97,10 @@ impl ScopeArena {
 
         // Get the height of the scope
         let height = parent_scope
-            .map(|id| self.get_scope(id).map(|scope| scope.height + 1))
-            .flatten()
+            .and_then(|id| self.get_scope(id).map(|scope| scope.height + 1))
             .unwrap_or_default();
 
-        let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten();
+        let parent_scope = parent_scope.and_then(|f| self.get_scope_raw(f));
 
         /*
         This scopearena aggressively reuses old scopes when possible.
@@ -582,12 +582,17 @@ impl ScopeState {
         self.our_arena_idx
     }
 
+    /// Get a handle to the raw update scheduler channel
+    pub fn scheduler_channel(&self) -> UnboundedSender<SchedulerMsg> {
+        self.tasks.sender.clone()
+    }
+
     /// Create a subscription that schedules a future render for the reference component
     ///
     /// ## Notice: you should prefer using prepare_update and get_scope_id
-    pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
+    pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
         let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
-        Rc::new(move || {
+        Arc::new(move || {
             let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
         })
     }
@@ -597,9 +602,9 @@ impl ScopeState {
     /// A component's ScopeId can be obtained from `use_hook` or the [`ScopeState::scope_id`] method.
     ///
     /// This method should be used when you want to schedule an update for a component
-    pub fn schedule_update_any(&self) -> Rc<dyn Fn(ScopeId)> {
+    pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
         let chan = self.tasks.sender.clone();
-        Rc::new(move |id| {
+        Arc::new(move |id| {
             let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
         })
     }
@@ -656,8 +661,7 @@ impl ScopeState {
         self.shared_contexts
             .borrow_mut()
             .insert(TypeId::of::<T>(), value.clone())
-            .map(|f| f.downcast::<T>().ok())
-            .flatten();
+            .and_then(|f| f.downcast::<T>().ok());
         value
     }
 
@@ -687,8 +691,7 @@ impl ScopeState {
             self.shared_contexts
                 .borrow_mut()
                 .insert(TypeId::of::<T>(), value.clone())
-                .map(|f| f.downcast::<T>().ok())
-                .flatten();
+                .and_then(|f| f.downcast::<T>().ok());
             return value;
         }
 

+ 8 - 0
packages/desktop/src/cfg.rs

@@ -17,6 +17,7 @@ pub struct DesktopConfig {
     pub(crate) protocols: Vec<WryProtocol>,
     pub(crate) pre_rendered: Option<String>,
     pub(crate) event_handler: Option<Box<DynEventHandlerFn>>,
+    pub(crate) disable_context_menu: bool,
 }
 
 pub(crate) type WryProtocol = (
@@ -29,15 +30,22 @@ impl DesktopConfig {
     #[inline]
     pub fn new() -> Self {
         let window = WindowBuilder::new().with_title("Dioxus app");
+
         Self {
             event_handler: None,
             window,
             protocols: Vec::new(),
             file_drop_handler: None,
             pre_rendered: None,
+            disable_context_menu: !cfg!(debug_assertions),
         }
     }
 
+    pub fn with_disable_context_menu(&mut self, disable: bool) -> &mut Self {
+        self.disable_context_menu = disable;
+        self
+    }
+
     pub fn with_prerendered(&mut self, content: String) -> &mut Self {
         self.pre_rendered = Some(content);
         self

+ 3 - 0
packages/desktop/src/controller.rs

@@ -56,6 +56,9 @@ impl DesktopController {
                     .unwrap()
                     .push_front(serde_json::to_string(&edits.edits).unwrap());
 
+                // Make sure the window is ready for any new updates
+                proxy.send_event(UserWindowEvent::Update).unwrap();
+
                 loop {
                     dom.wait_for_work().await;
                     let mut muts = dom.work_with_deadline(|| false);

+ 4 - 5
packages/desktop/src/lib.rs

@@ -143,7 +143,6 @@ pub fn launch_with_props<P: 'static + Send>(
                                     let _ = proxy.send_event(UserWindowEvent::Update);
                                 }
                                 "browser_open" => {
-                                    println!("browser_open");
                                     let data = message.params();
                                     log::trace!("Open browser: {:?}", data);
                                     if let Some(temp) = data.as_object() {
@@ -173,10 +172,7 @@ pub fn launch_with_props<P: 'static + Send>(
                     webview = webview.with_custom_protocol(name, handler)
                 }
 
-                if cfg!(debug_assertions) {
-                    // in debug, we are okay with the reload menu showing and dev tool
-                    webview = webview.with_dev_tool(true);
-                } else {
+                if cfg.disable_context_menu {
                     // in release mode, we don't want to show the dev tool or reload menus
                     webview = webview.with_initialization_script(
                         r#"
@@ -193,6 +189,9 @@ pub fn launch_with_props<P: 'static + Send>(
                         }
                     "#,
                     )
+                } else {
+                    // in debug, we are okay with the reload menu showing and dev tool
+                    webview = webview.with_dev_tool(true);
                 }
 
                 desktop.webviews.insert(window_id, webview.build().unwrap());

+ 3 - 3
packages/fermi/src/root.rs

@@ -1,4 +1,4 @@
-use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc};
+use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
 
 use dioxus_core::ScopeId;
 use im_rc::HashSet;
@@ -9,7 +9,7 @@ pub type AtomId = *const ();
 
 pub struct AtomRoot {
     pub atoms: RefCell<HashMap<AtomId, Slot>>,
-    pub update_any: Rc<dyn Fn(ScopeId)>,
+    pub update_any: Arc<dyn Fn(ScopeId)>,
 }
 
 pub struct Slot {
@@ -18,7 +18,7 @@ pub struct Slot {
 }
 
 impl AtomRoot {
-    pub fn new(update_any: Rc<dyn Fn(ScopeId)>) -> Self {
+    pub fn new(update_any: Arc<dyn Fn(ScopeId)>) -> Self {
         Self {
             update_any,
             atoms: RefCell::new(HashMap::new()),

+ 2 - 1
packages/hooks/src/use_shared_state.rs

@@ -3,6 +3,7 @@ use std::{
     cell::{Cell, Ref, RefCell, RefMut},
     collections::HashSet,
     rc::Rc,
+    sync::Arc,
 };
 
 type ProvidedState<T> = RefCell<ProvidedStateInner<T>>;
@@ -10,7 +11,7 @@ type ProvidedState<T> = RefCell<ProvidedStateInner<T>>;
 // Tracks all the subscribers to a shared State
 pub struct ProvidedStateInner<T> {
     value: Rc<RefCell<T>>,
-    notify_any: Rc<dyn Fn(ScopeId)>,
+    notify_any: Arc<dyn Fn(ScopeId)>,
     consumers: HashSet<ScopeId>,
 }
 

+ 2 - 2
packages/hooks/src/usefuture.rs

@@ -1,6 +1,6 @@
 #![allow(missing_docs)]
 use dioxus_core::{ScopeState, TaskId};
-use std::{cell::Cell, future::Future, rc::Rc};
+use std::{cell::Cell, future::Future, rc::Rc, sync::Arc};
 
 /// A future that resolves to a value.
 ///
@@ -61,7 +61,7 @@ pub enum FutureState<'a, T> {
 }
 
 pub struct UseFuture<T> {
-    update: Rc<dyn Fn()>,
+    update: Arc<dyn Fn()>,
     needs_regen: Cell<bool>,
     value: Option<T>,
     pending: bool,

+ 2 - 1
packages/hooks/src/useref.rs

@@ -2,6 +2,7 @@ use dioxus_core::ScopeState;
 use std::{
     cell::{Ref, RefCell, RefMut},
     rc::Rc,
+    sync::Arc,
 };
 
 /// `use_ref` is a key foundational hook for storing state in Dioxus.
@@ -121,7 +122,7 @@ pub fn use_ref<'a, T: 'static>(
 
 /// A type created by the [`use_ref`] hook. See its documentation for more details.
 pub struct UseRef<T> {
-    update: Rc<dyn Fn()>,
+    update: Arc<dyn Fn()>,
     value: Rc<RefCell<T>>,
 }
 

+ 2 - 1
packages/hooks/src/usestate.rs

@@ -5,6 +5,7 @@ use std::{
     cell::{RefCell, RefMut},
     fmt::{Debug, Display},
     rc::Rc,
+    sync::Arc,
 };
 
 /// Store state between component renders.
@@ -69,7 +70,7 @@ pub fn use_state<'a, T: 'static>(
 
 pub struct UseState<T: 'static> {
     pub(crate) current_val: Rc<T>,
-    pub(crate) update_callback: Rc<dyn Fn()>,
+    pub(crate) update_callback: Arc<dyn Fn()>,
     pub(crate) setter: Rc<dyn Fn(T)>,
     pub(crate) slot: Rc<RefCell<Rc<T>>>,
 }

+ 1 - 1
packages/interpreter/src/bindings.rs

@@ -63,5 +63,5 @@ extern "C" {
     );
 
     #[wasm_bindgen(method)]
-    pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str);
+    pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str, ns: Option<&str>);
 }

+ 8 - 8
packages/interpreter/src/interpreter.js

@@ -53,14 +53,12 @@ export class Interpreter {
     }
   }
   CreateTextNode(text, root) {
-    // todo: make it so the types are okay
     const node = document.createTextNode(text);
     this.nodes[root] = node;
     this.stack.push(node);
   }
   CreateElement(tag, root) {
     const el = document.createElement(tag);
-    // el.setAttribute("data-dioxus-id", `${root}`);
     this.nodes[root] = el;
     this.stack.push(el);
   }
@@ -105,7 +103,7 @@ export class Interpreter {
     if (ns === "style") {
       // @ts-ignore
       node.style[name] = value;
-    } else if (ns != null || ns != undefined) {
+    } else if (ns !== null || ns !== undefined) {
       node.setAttributeNS(ns, name, value);
     } else {
       switch (name) {
@@ -133,10 +131,12 @@ export class Interpreter {
       }
     }
   }
-  RemoveAttribute(root, name) {
+  RemoveAttribute(root, field, ns) {
+    const name = field;
     const node = this.nodes[root];
-
-    if (name === "value") {
+    if (ns !== null || ns !== undefined) {
+      node.removeAttributeNS(ns, name);
+    } else if (name === "value") {
       node.value = "";
     } else if (name === "checked") {
       node.checked = false;
@@ -260,7 +260,7 @@ export class Interpreter {
               }
             }
 
-            if (realId == null) {
+            if (realId === null) {
               return;
             }
             window.ipc.postMessage(
@@ -281,7 +281,7 @@ export class Interpreter {
         this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
         break;
       case "RemoveAttribute":
-        this.RemoveAttribute(edit.root, edit.name);
+        this.RemoveAttribute(edit.root, edit.name, edit.ns);
         break;
     }
   }

+ 3 - 2
packages/router/src/service.rs

@@ -3,6 +3,7 @@ use std::{
     cell::{Cell, Ref, RefCell},
     collections::{HashMap, HashSet},
     rc::Rc,
+    sync::Arc,
 };
 
 use dioxus_core::ScopeId;
@@ -10,7 +11,7 @@ use dioxus_core::ScopeId;
 use crate::platform::RouterProvider;
 
 pub struct RouterService {
-    pub(crate) regen_route: Rc<dyn Fn(ScopeId)>,
+    pub(crate) regen_route: Arc<dyn Fn(ScopeId)>,
     pub(crate) pending_events: Rc<RefCell<Vec<RouteEvent>>>,
     slots: Rc<RefCell<Vec<(ScopeId, String)>>>,
     onchange_listeners: Rc<RefCell<HashSet<ScopeId>>>,
@@ -42,7 +43,7 @@ enum RouteSlot {
 }
 
 impl RouterService {
-    pub fn new(regen_route: Rc<dyn Fn(ScopeId)>, root_scope: ScopeId) -> Self {
+    pub fn new(regen_route: Arc<dyn Fn(ScopeId)>, root_scope: ScopeId) -> Self {
         let history = BrowserHistory::default();
         let location = history.location();
         let path = location.path();

+ 2 - 3
packages/rsx/Cargo.toml

@@ -1,13 +1,12 @@
 [package]
 name = "dioxus-rsx"
-version = "0.0.0"
+version = "0.1.0"
 edition = "2018"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-once_cell = "1.8"
-proc-macro-error = "1.0.4"
+proc-macro-error = "1"
 proc-macro2 = { version = "1.0.6" }
 quote = "1.0"
 syn = { version = "1.0.11", features = ["full", "extra-traits"] }

+ 2 - 2
packages/web/src/dom.rs

@@ -127,8 +127,8 @@ impl WebsysDom {
                     self.interpreter.RemoveEventListener(root, event)
                 }
 
-                DomEdit::RemoveAttribute { root, name } => {
-                    self.interpreter.RemoveAttribute(root, name)
+                DomEdit::RemoveAttribute { root, name, ns } => {
+                    self.interpreter.RemoveAttribute(root, name, ns)
                 }
 
                 DomEdit::CreateTextNode { text, root } => {