├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release-plz.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE.txt ├── LICENSE-MIT.txt ├── README.md ├── dioxus-web-component-macro ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE.txt ├── LICENSE-MIT.txt ├── README.md ├── src │ ├── attribute.rs │ ├── doc.md │ ├── event.rs │ ├── lib.rs │ ├── parameter.rs │ ├── properties.rs │ ├── snapshots │ │ ├── dioxus_web_component_macro__tests__should_parse_multiple_events.snap │ │ └── dioxus_web_component_macro__web_component__tests__should_parse_attributes_args_with_error.snap │ ├── tag.rs │ └── web_component.rs └── tests │ ├── annotation_style.rs │ ├── assets │ ├── failures │ │ ├── invalid_attribute.rs │ │ ├── invalid_attribute.stderr │ │ ├── invalid_tag_missing_hyphen.rs │ │ ├── invalid_tag_missing_hyphen.stderr │ │ ├── invalid_tag_upper_case.rs │ │ └── invalid_tag_upper_case.stderr │ └── success │ │ ├── basic_annotation.rs │ │ ├── full_annotation.rs │ │ └── no_annotation.rs │ └── trybuild_tests.rs ├── dioxus-web-component ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE.txt ├── LICENSE-MIT.txt ├── README.md └── src │ ├── event.rs │ ├── lib.rs │ ├── rust_component.rs │ ├── shim.js │ └── style.rs ├── examples ├── counter │ ├── Cargo.toml │ ├── LICENSE-APACHE.txt │ ├── LICENSE-MIT.txt │ ├── README.md │ ├── favicon.ico │ ├── index.html │ ├── index.js │ ├── src │ │ └── lib.rs │ └── style.css ├── dx-in-dx │ ├── .gitignore │ ├── Cargo.toml │ ├── Dioxus.toml │ ├── README.md │ ├── assets │ │ ├── favicon.ico │ │ ├── header.svg │ │ └── main.css │ ├── src │ │ ├── lib.rs │ │ ├── link.css │ │ └── main.rs │ └── test.rs └── greeting │ ├── Cargo.toml │ ├── LICENSE-APACHE.txt │ ├── LICENSE-MIT.txt │ ├── README.md │ ├── favicon.ico │ ├── index.html │ ├── index.js │ └── src │ ├── lib.rs │ └── style.css ├── favicon.png ├── justfile └── release-plz.toml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | # Look for `Cargo.toml` and `Cargo.lock` in the root directory 5 | directory: "/" 6 | # Check for updates every Monday 7 | schedule: 8 | interval: "weekly" 9 | open-pull-requests-limit: 10 10 | 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | # Check for updates every Monday 14 | schedule: 15 | interval: "weekly" 16 | open-pull-requests-limit: 10 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | 8 | name: Continuous integration 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | CARGO_INCREMENTAL: 0 13 | MSRV: 1.79.0 14 | 15 | jobs: 16 | tests: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | include: 21 | - rust: $MSRV 22 | - rust: stable 23 | - rust: beta 24 | - rust: nightly 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: dtolnay/rust-toolchain@master 29 | with: 30 | toolchain: ${{ matrix.rust }} 31 | - name: Build 32 | run: cargo build --verbose 33 | - name: Documentation 34 | run: cargo doc --verbose 35 | - name: Tests 36 | run: cargo test --verbose 37 | 38 | clippy: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: dtolnay/rust-toolchain@beta 43 | with: 44 | components: clippy 45 | - name: Lint 46 | run: cargo clippy 47 | 48 | minimal-versions: 49 | name: Check MSRV and minimal-versions 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v4 53 | - uses: dtolnay/rust-toolchain@master 54 | with: 55 | toolchain: nightly 56 | - uses: dtolnay/rust-toolchain@master 57 | with: 58 | toolchain: $MSRV 59 | - uses: taiki-e/install-action@v2 60 | with: 61 | tool: cargo-hack 62 | - run: cargo +nightly hack generate-lockfile --remove-dev-deps -Z direct-minimal-versions 63 | - name: Build 64 | run: cargo build --verbose --all-features 65 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Install Rust toolchain 23 | uses: dtolnay/rust-toolchain@stable 24 | with: 25 | components: clippy, rustfmt 26 | - name: Check format 27 | run: cargo fmt --all -- --check 28 | - name: Check lint 29 | run: cargo clippy --workspace --all-features --all-targets 30 | - name: Test 31 | run: cargo test 32 | - name: Cleanup 33 | run: rm Cargo.lock 34 | - name: Run release-plz 35 | uses: MarcoIeni/release-plz-action@v0.5 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | dist/ 3 | /Cargo.lock 4 | pkg/ 5 | *.br 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "dioxus-web-component", 4 | "dioxus-web-component-macro", 5 | "examples/counter", 6 | "examples/dx-in-dx", 7 | "examples/greeting", 8 | ] 9 | resolver = "2" 10 | 11 | [workspace.package] 12 | edition = "2021" 13 | authors = ["ilaborie@gmail.com"] 14 | license = "MIT OR Apache-2.0" 15 | repository = "https://github.com/ilaborie/dioxus-web-component" 16 | rust-version = "1.79.0" 17 | 18 | [workspace.dependencies] 19 | dioxus = "0.6.1" 20 | wasm-bindgen = "0.2.99" 21 | wasm-bindgen-futures = "0.4.49" 22 | web-sys = "0.3.76" 23 | syn = "2.0.90" 24 | quote = "1.0.37" 25 | proc-macro2 = "1.0.92" 26 | darling = "0.20.10" 27 | heck = "0.5.0" 28 | futures = "0.3.31" 29 | insta = "1.41.1" 30 | 31 | [workspace.lints.rust] 32 | unsafe_code = "deny" 33 | missing_docs = "warn" 34 | 35 | [workspace.lints.clippy] 36 | perf = { level = "warn", priority = -1 } 37 | pedantic = { level = "warn", priority = -1 } 38 | cargo = { level = "warn", priority = -1 } 39 | 40 | undocumented_unsafe_blocks = "deny" 41 | 42 | dbg_macro = "warn" 43 | expect_used = "warn" 44 | if_then_some_else_none = "warn" 45 | indexing_slicing = "warn" 46 | large_include_file = "warn" 47 | min_ident_chars = "warn" 48 | print_stderr = "warn" 49 | print_stdout = "warn" 50 | rc_buffer = "warn" 51 | rc_mutex = "warn" 52 | unnecessary_safety_doc = "warn" 53 | unwrap_used = "warn" 54 | 55 | module_name_repetitions = "allow" 56 | 57 | [profile.release] 58 | opt-level = "z" 59 | debug = false 60 | lto = true 61 | codegen-units = 1 62 | panic = "abort" 63 | strip = true 64 | incremental = false 65 | 66 | [profile.wasm-dev] 67 | inherits = "dev" 68 | opt-level = 1 69 | 70 | [profile.server-dev] 71 | inherits = "dev" 72 | 73 | [profile.android-dev] 74 | inherits = "dev" 75 | -------------------------------------------------------------------------------- /LICENSE-APACHE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dioxus-web-component/README.md -------------------------------------------------------------------------------- /dioxus-web-component-macro/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.4.0](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-macro-v0.3.2...dioxus-web-component-macro-v0.4.0) - 2024-09-29 10 | - Support Dioxus 0.6 11 | 12 | ## [0.3.2](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-macro-v0.3.1...dioxus-web-component-macro-v0.3.2) - 2024-06-02 13 | 14 | ### Added 15 | - Generate the web-component typescript definition ([#43](https://github.com/ilaborie/dioxus-web-component/pull/43)) 16 | 17 | ### Other 18 | - fix typos ([#41](https://github.com/ilaborie/dioxus-web-component/pull/41)) 19 | 20 | ## [0.3.1](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-macro-v0.3.0...dioxus-web-component-macro-v0.3.1) - 2024-06-01 21 | 22 | ### Added 23 | - Allow attribute + property ([#38](https://github.com/ilaborie/dioxus-web-component/pull/38)) 24 | 25 | ## [0.3.0](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-macro-v0.2.2...dioxus-web-component-macro-v0.3.0) - 2024-05-31 26 | 27 | ### Added 28 | - [**breaking**] Javascript property support ([#36](https://github.com/ilaborie/dioxus-web-component/pull/36)) 29 | 30 | ## [0.2.2](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-macro-v0.2.1...dioxus-web-component-macro-v0.2.2) - 2024-05-25 31 | 32 | ### Fixed 33 | - fix inifinite loop while setting attribute ([#33](https://github.com/ilaborie/dioxus-web-component/pull/33)) 34 | 35 | ## [0.2.1](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-macro-v0.2.0...dioxus-web-component-macro-v0.2.1) - 2024-05-23 36 | 37 | ### Fixed 38 | - Shared expose the HTML element ([#30](https://github.com/ilaborie/dioxus-web-component/pull/30)) 39 | 40 | ## [0.2.0](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-macro-v0.1.3...dioxus-web-component-macro-v0.2.0) - 2024-05-20 41 | 42 | ### Other 43 | - [**breaking**] remove async-channel dependency ([#27](https://github.com/ilaborie/dioxus-web-component/pull/27)) 44 | 45 | ## [0.1.3](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-macro-v0.1.2...dioxus-web-component-macro-v0.1.3) - 2024-05-18 46 | 47 | ### Added 48 | - check custom element tag validity ([#26](https://github.com/ilaborie/dioxus-web-component/pull/26)) 49 | 50 | ## [0.1.2](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-macro-v0.1.1...dioxus-web-component-macro-v0.1.2) - 2024-05-13 51 | 52 | ### Other 53 | - fix small doc issues ([#15](https://github.com/ilaborie/dioxus-web-component/pull/15)) 54 | 55 | ## [0.1.1](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-macro-v0.1.0...dioxus-web-component-macro-v0.1.1) - 2024-05-13 56 | 57 | ### Fixed 58 | - Fix macro attribute generation for [#12](https://github.com/ilaborie/dioxus-web-component/pull/12) ([#13](https://github.com/ilaborie/dioxus-web-component/pull/13)) 59 | 60 | ## [0.0.4](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-macro-v0.0.3...dioxus-web-component-macro-v0.0.4) - 2024-05-06 61 | 62 | ### Added 63 | - add a proc macro ([#8](https://github.com/ilaborie/dioxus-web-component/pull/8)) 64 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dioxus-web-component-macro" 3 | version = "0.4.0" 4 | edition = { workspace = true } 5 | authors = { workspace = true } 6 | license = { workspace = true } 7 | repository = { workspace = true } 8 | rust-version = { workspace = true } 9 | 10 | documentation = "https://docs.rs/dioxus-web-component-macro" 11 | description = "dioxus-web-component proc macro" 12 | categories = ["gui", "wasm", "web-programming"] 13 | keywords = ["dioxus", "web-component", "wasm"] 14 | readme = "README.md" 15 | 16 | [lib] 17 | proc-macro = true 18 | 19 | [dependencies] 20 | darling = { workspace = true } 21 | heck = { workspace = true } 22 | proc-macro2 = { workspace = true } 23 | quote = { workspace = true } 24 | syn = { workspace = true } 25 | 26 | [dev-dependencies] 27 | assert2 = "0.3.14" 28 | dioxus = { workspace = true, default-features = true, features = ["web"] } 29 | dioxus-web-component = { path = "../dioxus-web-component" } 30 | insta = { workspace = true } 31 | prettyplease = "0.2.25" 32 | rstest = { version = "0.23.0", default-features = false } 33 | serde = "1.0.194" 34 | trybuild = { version = "1.0.93", features = ["diff"] } 35 | wasm-bindgen = { workspace = true } 36 | 37 | [lints] 38 | workspace = true 39 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/LICENSE-APACHE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/README.md: -------------------------------------------------------------------------------- 1 | # dioxus-web-component-macro 2 | 3 | Provide a proc macro to build Dioxus web component. 4 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/src/attribute.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::min_ident_chars)] 2 | 3 | use std::borrow::Cow; 4 | use std::fmt::Debug; 5 | 6 | use darling::FromMeta; 7 | use heck::ToKebabCase as _; 8 | use proc_macro2::{Ident, TokenStream}; 9 | use quote::{quote, ToTokens}; 10 | use syn::ext::IdentExt; 11 | use syn::{Expr, Meta, Type}; 12 | 13 | #[derive(Debug, FromMeta, Default)] 14 | struct AttributeReceiver { 15 | name: Option, 16 | option: Option, 17 | initial: Option, 18 | parse: Option, 19 | } 20 | 21 | pub(super) struct Attribute { 22 | pub ident: Ident, 23 | ty: Type, 24 | name: Option, 25 | is_option: Option, 26 | initial: Option, 27 | parse: Option, 28 | } 29 | 30 | impl Debug for Attribute { 31 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | f.debug_struct("Attribute") 33 | .field("ident", &self.ident.to_string()) 34 | .field("ty", &self.ty.to_token_stream().to_string()) 35 | .field("name", &self.name) 36 | .field("is_option", &self.is_option) 37 | .field("initial", &self.initial.to_token_stream().to_string()) 38 | .field("parse", &self.parse.to_token_stream().to_string()) 39 | .finish() 40 | } 41 | } 42 | 43 | impl Attribute { 44 | pub(super) fn new(ident: Ident, ty: Type) -> Self { 45 | Self { 46 | ident, 47 | ty, 48 | name: None, 49 | is_option: None, 50 | initial: None, 51 | parse: None, 52 | } 53 | } 54 | 55 | pub(super) fn parse( 56 | attr: &syn::Attribute, 57 | ident: Ident, 58 | ty: Type, 59 | ) -> Result { 60 | let receiver = if let Meta::List(_) = &attr.meta { 61 | AttributeReceiver::from_meta(&attr.meta)? 62 | } else { 63 | AttributeReceiver::default() 64 | }; 65 | 66 | let result = Self { 67 | ident, 68 | ty, 69 | name: receiver.name, 70 | is_option: receiver.option, 71 | initial: receiver.initial, 72 | parse: receiver.parse, 73 | }; 74 | Ok(result) 75 | } 76 | } 77 | 78 | impl Attribute { 79 | pub(super) fn name(&self) -> Cow { 80 | self.name.as_deref().map_or_else( 81 | || { 82 | let name = self.ident.unraw().to_string(); 83 | let name = name.as_str().to_kebab_case(); 84 | Cow::Owned(name) 85 | }, 86 | Cow::Borrowed, 87 | ) 88 | } 89 | 90 | fn option(&self) -> bool { 91 | self.is_option.as_ref().copied().unwrap_or_else(|| { 92 | let ty_str = self.ty.to_token_stream().to_string(); 93 | ty_str.starts_with("Option <") 94 | }) 95 | } 96 | 97 | fn initial(&self) -> TokenStream { 98 | self.initial.as_ref().map_or_else( 99 | || { 100 | quote! { 101 | ::std::default::Default::default() 102 | } 103 | }, 104 | ToTokens::to_token_stream, 105 | ) 106 | } 107 | 108 | fn parse_value(&self) -> TokenStream { 109 | self.parse.as_ref().map_or_else( 110 | || { 111 | quote! { 112 | |value| value.parse().ok() 113 | } 114 | }, 115 | ToTokens::to_token_stream, 116 | ) 117 | } 118 | 119 | pub(super) fn struct_attribute(&self) -> TokenStream { 120 | let Self { ident, ty, .. } = &self; 121 | quote! { 122 | #ident : ::dioxus::prelude::Signal<#ty> 123 | } 124 | } 125 | 126 | pub(super) fn new_instance(&self) -> TokenStream { 127 | let ident = &self.ident; 128 | let initial = self.initial(); 129 | quote! { 130 | let #ident = ::dioxus::prelude::use_signal(|| #initial); 131 | } 132 | } 133 | 134 | pub(super) fn pattern_attribute_changed(&self) -> TokenStream { 135 | let ident = &self.ident; 136 | let name = self.name(); 137 | let parse = self.parse_value(); 138 | let initial = self.initial(); 139 | 140 | if self.option() { 141 | quote! { 142 | #name => { 143 | let value = new_value.and_then(#parse); 144 | self.#ident.set(value); 145 | } 146 | } 147 | } else { 148 | quote! { 149 | #name => { 150 | let value = new_value.and_then(#parse).unwrap_or_else(|| #initial); 151 | self.#ident.set(value); 152 | } 153 | } 154 | } 155 | } 156 | 157 | pub(super) fn rsx_attribute(&self) -> TokenStream { 158 | let ident = &self.ident; 159 | 160 | quote! { 161 | #ident: #ident().clone(), 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/src/doc.md: -------------------------------------------------------------------------------- 1 | Proc macro to create the web component glue 2 | 3 | # Examples 4 | 5 | ## Greeting example 6 | 7 | ```rust 8 | use dioxus::logger::tracing::Level; 9 | use dioxus::{logger, prelude::*}; 10 | use dioxus_web_component::{web_component, InjectedStyle}; 11 | use wasm_bindgen::prelude::*; 12 | 13 | /// Install (register) the web component 14 | #[wasm_bindgen(start)] 15 | pub fn register() { 16 | let _ = logger::init(Level::INFO); 17 | register_greetings(); 18 | } 19 | 20 | #[web_component(tag = "plop-greeting", style = InjectedStyle::css(":host { /* ... */ }") )] 21 | fn Greetings( 22 | // The name can be set as an attribute of the plop-greeting HTML element 23 | #[attribute] 24 | #[property] 25 | name: String, 26 | ) -> Element { 27 | rsx! { 28 | p { "Hello {name}!" } 29 | } 30 | } 31 | ``` 32 | 33 | See [full example](https://github.com/ilaborie/dioxus-web-component/tree/main/examples/greeting) 34 | 35 | ## Counter example 36 | 37 | ```rust 38 | use dioxus::logger::tracing::Level; 39 | use dioxus::{logger, prelude::*}; 40 | use dioxus_web_component::{web_component, InjectedStyle}; 41 | use wasm_bindgen::prelude::*; 42 | 43 | /// Install (register) the web component 44 | #[wasm_bindgen(start)] 45 | pub fn register() { 46 | let _ = logger::init(Level::INFO); 47 | 48 | // The register counter is generated by the `#[web_component(...)]` macro 49 | register_counter(); 50 | } 51 | 52 | /// The Dioxus component 53 | #[web_component(tag = "plop-counter", style = InjectedStyle::stylesheet("./style.css"))] 54 | fn Counter( 55 | // The label is only available with a property 56 | #[property] label: String, 57 | // This component can trigger a custom 'count' event 58 | on_count: EventHandler, 59 | ) -> Element { 60 | let mut counter = use_signal(|| 0); 61 | 62 | rsx! { 63 | span { "{label}" } 64 | button { 65 | onclick: move |_| { 66 | counter += 1; 67 | on_count(counter()); 68 | }, 69 | "+" 70 | } 71 | output { "{counter}" } 72 | } 73 | } 74 | ``` 75 | 76 | See [full example](https://github.com/ilaborie/dioxus-web-component/tree/main/examples/counter) 77 | 78 | # Macro attributes 79 | 80 | ## Tag 81 | 82 | The custom element tag is built from the component name. 83 | 84 | By default, the tag is the kebab-case version of the name. 85 | For example, `MyWebComponent` becomes `my-web-component`. 86 | 87 | You can change the default behavior with the `tag` attribute. 88 | 89 | 90 | ```rust 91 | use dioxus::prelude::*; 92 | use dioxus_web_component::{web_component, DioxusWebComponent}; 93 | 94 | #[web_component(tag = "plop-component")] 95 | fn MyWebComponent( 96 | // ... 97 | ) -> Element { todo!() } 98 | ``` 99 | 100 | 101 | ```html 102 | 103 | 104 | ``` 105 | 106 | ℹ️ INFO: the custom element tag name has constraints. 107 | The macro checks the validity of the tag for you. 108 | See [MDN - Valid custom element names](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#valid_custom_element_names) 109 | 110 | ## Style 111 | 112 | You can provide the web component style with the `style` attribute. 113 | 114 | ```rust, ignore 115 | use dioxus::prelude::*; 116 | use dioxus_web_component::{web_component, InjectedStyle}; 117 | 118 | #[web_component( 119 | tag = "plop-greeting", 120 | style = InjectedStyle::css(include_str!("style.css")) 121 | )] 122 | fn Greeting( 123 | // ... 124 | ) -> Element { 125 | todo!() 126 | } 127 | ``` 128 | 129 | The `dioxus_web_component::InjectedStyle` could be raw CSS included in 130 | an HTML `` element, or a link to an external stylesheet, 131 | or a list of `InjectedStyle` styles. 132 | 133 | ⚠️ WARNING: the web component is wrapped into an HTML `div` with the `dioxus` CSS class. 134 | 135 | # Component fields annotations 136 | 137 | Every parameter of your component should be an attribute, a property, or an event. 138 | Note that a parameter could be both an attribute and a property. 139 | 140 | The proc macro tries to detect the kind of parameter by looking at its type. 141 | If the type starts by `EventHandler` it is expected to be an event. 142 | But, this kind of detection is not fully reliable, so you might need to add an annotation 143 | to correct this behavior. 144 | 145 | The annotations are also required if you need to customize the behavior. 146 | 147 | ## Attributes 148 | 149 | Attributes are like the `href` of an `` HTML element. 150 | 151 | You can enforce the parameter to be an attribute with the `#[attribute]` annotation. 152 | 153 | When the attribute value changes the dioxus component will be rendered. 154 | 155 | The HTML value of an attribute is a `String`, so you should be able 156 | to parse that string into the target type. 157 | 158 | - `name` 159 | 160 | The attribute name is by default the kebab-case of the parameter name. 161 | You can choose another name with `#[attribute(name = "my-custom-name")]`. 162 | 163 | - `option` 164 | 165 | The attribute could be optional or not. 166 | The proc macro tries to detect it automatically with the type name. 167 | However the detection is not fully reliable, so you can use the `#[attribute(option = true)]` 168 | to fix the detection if necessary. 169 | 170 | 171 | - `initial` 172 | 173 | Attributes require to have an initial value. 174 | This value is used when no HTML attribute is provided, or if the attribute is removed. 175 | 176 | By default, we expect the attribute type to implement [`std::default::Default`]. 177 | If it's not the case, or if you want to use another value for your attribute you 178 | can provide your default expression with `#[attribute(initial = String::from("World"))]`. 179 | 180 | Note that `Option` implements `Default` with the `None` value 181 | even if `T` does not implement itself `Default`. 182 | 183 | - `parse` 184 | 185 | HTML attributes are strings and optional, so we need to convert the attribute value 186 | into the component parameter type. 187 | 188 | The proc macro uses the `std::str::parse` method. That means the target type 189 | needs to implement the `std::str::FromStr` trait. 190 | 191 | In case of an error, the initial value (see below) is used. 192 | 193 | If you want to change this behavior, you can provide your parsing expression. 194 | 195 | If the parameter type is optional, the parse expression is used in this code: 196 | `let value = new_value.and_then(#parse);`. 197 | If the type is NOT optional, the code looks like `let value = new_value.and_then(#parse).unwrap_or_else(|| #initial);`. 198 | 199 | The expected type for the parsing expression is `FnOnce(String) -> Option`. 200 | The default expression is `|value| value.parse().ok()`. 201 | 202 | For example, if you have a parameter `required` of type `bool` and you want the value to be `true` 203 | if the attribute is present whatever the content of the attribute, you could use `#[attribute(parse = |s| !s.is_empty() )]`. 204 | 205 | ## Property 206 | 207 | On the Rust side of the code, properties work like attributes. 208 | The property is not accessible with pure HTML, 209 | you need Javascript to get/set the property. 210 | 211 | Instead of the `String` representation, you need to be able to convert the Rust type into a 212 | Javascript type (here a `wasm_bindgen::JsValue`). 213 | For the setter, you need the opposite conversion. 214 | 215 | - `name` 216 | 217 | The attribute name is by default the camelCase of the parameter name. 218 | You can choose another name with `#[property(name = "valueAsDate")]`. 219 | 220 | - `readonly` 221 | 222 | If `true`, it avoids setting the property from the javascript side. 223 | By default getter and setter are generated. 224 | 225 | - `initial` 226 | 227 | Properties require to have an initial value. 228 | This value is used when the component is initialized. 229 | 230 | By default, we expect the property type to implement [`std::default::Default`]. 231 | If it's not the case, or if you want to use another value for your property you 232 | can provide your default expression with `#[property(initial = String::from("World"))]`. 233 | 234 | - Conversion with `try_into_js`, `try_from_js` 235 | 236 | For the getter, the property value should be converted to a `wasm_bindgen::JsValue`. 237 | By default, we use the `std::convert::TryInto` implementation. 238 | 239 | Note that there are many ways to implement `TryInto`, 240 | for example with `impl TryFrom for JsValue` or even `impl From for JsValue`. 241 | See [Rust `TryInto`](https://doc.rust-lang.org/std/convert/trait.TryInto.html). 242 | You can also use [`wasm-bindgen`] to generate the conversion of a struct. 243 | 244 | You may required to provide your custom conversion into the `JsValue` 245 | with the `try_into_js` attribute (orphan rule). 246 | The expected type for the parsing expression is `FnOnce(T) -> Result`. 247 | Not that we do not care about the error type because 248 | the error case is ignored and returns `undefined`. 249 | 250 | The default expression is `|value| value.try_into()`. 251 | 252 | 253 | For the setter, the property value should be converted from a `wasm_bindgen::JsValue`. 254 | By default, we use the `std::convert::TryInto` implementation. 255 | 256 | Note that there are many ways to implement `TryInto`, 257 | for example with `impl TryFrom for T` or even `impl From for T`. 258 | See [Rust `TryInto`](https://doc.rust-lang.org/std/convert/trait.TryInto.html). 259 | 260 | You can provide your custom conversion from the `JsValue` 261 | with the `try_from_js` attribute. 262 | The expected type for the parsing expression is `FnOnce(JsValue) -> Result`. 263 | Not that we do not care about the error type because 264 | the error case is ignored. 265 | 266 | The default expression is `|value| value.try_into()`. 267 | 268 | Example to convert a custom type that wraps a `bool`: 269 | 270 | ```rust 271 | use dioxus::prelude::*; 272 | use dioxus_web_component::web_component; 273 | use std::convert::Infallible; 274 | use wasm_bindgen::JsValue; 275 | 276 | #[derive(Clone, PartialEq, Default)] 277 | pub struct MyProp(bool); 278 | 279 | #[web_component] 280 | fn MyComponent( 281 | #[property( 282 | js_type = "bool", 283 | try_from_js= |value| Ok::<_, Infallible>(MyProp(value.is_truthy())), 284 | try_into_js = |prop| { 285 | let js_value = if prop.0 { 286 | JsValue::TRUE 287 | } else { 288 | JsValue::FALSE 289 | }; 290 | Ok::<_, Infallible>(js_value) 291 | }, 292 | )] 293 | prop2: MyProp, 294 | // ... 295 | ) -> Element { 296 | todo!() 297 | } 298 | ``` 299 | 300 | But in that situation, the recommended way is to implement `From for JsValue` and 301 | use the [`wasm-bindgen`] macro: 302 | 303 | ```rust 304 | use dioxus::prelude::*; 305 | use dioxus_web_component::web_component; 306 | use wasm_bindgen::prelude::*; 307 | 308 | // mapping to JsValue done by the #[wasm_bindgen] macro 309 | #[wasm_bindgen] 310 | #[derive(Clone, PartialEq, Default)] 311 | pub struct MyProp(bool); 312 | 313 | // mapping from JsValue 314 | impl From for MyProp { 315 | fn from(value: JsValue) -> Self { 316 | Self(value.is_truthy()) 317 | } 318 | } 319 | 320 | #[web_component] 321 | fn MyComponent( 322 | // only need to declare the typescript type 323 | #[property(js_type = "bool")] prop2: MyProp, 324 | ) -> Element { 325 | todo!() 326 | } 327 | ``` 328 | 329 | - Typescript generation with `js_type`, `no_typescript` 330 | 331 | The macro try to generate generate the typescript definition of the web-component. 332 | In some cases, it need more information, so you had to specify the `js_type` attribute, 333 | or disable the typescript generation with `no_typescript = true`. 334 | 335 | See the example above 336 | 337 | ## Events 338 | 339 | The web component could send [custom events]. 340 | If the type of the component parameter is `EventHandler`, the parameter is detected as an event. 341 | Because this detection is not fully reliable, you could enforce a parameter to be 342 | an event with the `#[event]` annotation. 343 | 344 | The custom event detail corresponds to the generic type of the Dioxus `EventHandler`. 345 | 346 | ⚠️ IMPORTANT: The event type needs to implement `Into` and be `'static` (does not have any reference). 347 | 348 | You may need to implement it manually. 349 | You could use [`serde-wasm-bindgen`], [`gloo_utils::format::JsValueSerdeExt`], [`wasm_bindgen::UnwrapThrowExt`] 350 | to implement the `Into` trait. 351 | 352 | 353 | - `name` 354 | 355 | The HTML event name is detected from the parameter name by removing the `on_` (or `on`) prefix 356 | and converting the name to kebab-case. 357 | You can choose your value with the `name` attribute like `#[event(name = "build")]` 358 | to dispatch a `build` event. 359 | 360 | - `no_bubble` 361 | 362 | By default, the event bubbles up through the DOM. 363 | You can avoid the bubbling with `#[event(no_bubble = true)]`. 364 | 365 | - `no_cancel` 366 | 367 | By default, the event is cancelable. 368 | You can avoid the bubbling with `#[event(no_cancel = true)]`. 369 | 370 | 371 | [custom events]: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent 372 | [`wasm-bindgen`]: https://rustwasm.github.io/docs/wasm-bindgen/ 373 | [`serde-wasm-bindgen`]: https://docs.rs/serde-wasm-bindgen 374 | [`gloo_utils::format::JsValueSerdeExt`]: https://docs.rs/gloo-utils/latest/gloo_utils/format/trait.JsValueSerdeExt.html 375 | [`wasm_bindgen::UnwrapThrowExt`]: https://docs.rs/wasm-bindgen/latest/wasm_bindgen/trait.UnwrapThrowExt.html 376 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/src/event.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::min_ident_chars)] 2 | 3 | use std::fmt::Debug; 4 | 5 | use darling::{Error, FromMeta}; 6 | use heck::ToKebabCase; 7 | use proc_macro2::{Ident, TokenStream}; 8 | use quote::{quote, ToTokens}; 9 | use syn::ext::IdentExt; 10 | use syn::{Attribute, Meta, Type}; 11 | 12 | #[derive(Debug, FromMeta, Default)] 13 | pub struct EventReceiver { 14 | name: Option, 15 | no_bubble: Option, 16 | no_cancel: Option, 17 | } 18 | 19 | pub struct Event { 20 | pub ident: Ident, 21 | ty: Type, 22 | web_event_name: Option, 23 | can_bubble: bool, 24 | cancelable: bool, 25 | } 26 | 27 | impl Debug for Event { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | f.debug_struct("Event") 30 | .field("ident", &self.ident.to_string()) 31 | .field("ty", &self.ty.to_token_stream().to_string()) 32 | .field("web_event_name", &self.web_event_name) 33 | .field("can_bubble", &self.can_bubble) 34 | .field("cancelable", &self.cancelable) 35 | .finish() 36 | } 37 | } 38 | 39 | impl Event { 40 | pub(super) fn new(ident: Ident, ty: Type) -> Self { 41 | Self { 42 | ident, 43 | ty, 44 | web_event_name: None, 45 | can_bubble: true, 46 | cancelable: true, 47 | } 48 | } 49 | 50 | pub(super) fn parse(attr: &Attribute, ident: Ident, ty: Type) -> Result { 51 | let receiver = if let Meta::List(_) = &attr.meta { 52 | EventReceiver::from_meta(&attr.meta)? 53 | } else { 54 | EventReceiver::default() 55 | }; 56 | 57 | let web_event_name = receiver.name; 58 | let can_bubble = !(receiver.no_bubble.unwrap_or_default()); 59 | let cancelable = !(receiver.no_cancel.unwrap_or_default()); 60 | 61 | let result = Self { 62 | ident, 63 | ty, 64 | web_event_name, 65 | can_bubble, 66 | cancelable, 67 | }; 68 | Ok(result) 69 | } 70 | } 71 | 72 | impl Event { 73 | pub(super) fn struct_attribute(&self) -> TokenStream { 74 | let Self { ident, ty, .. } = &self; 75 | quote! { 76 | #ident : #ty 77 | } 78 | } 79 | 80 | fn web_event_name(&self) -> String { 81 | self.web_event_name.clone().unwrap_or_else(|| { 82 | self.ident 83 | .unraw() 84 | .to_string() 85 | .trim_start_matches("on_") 86 | .trim_start_matches("on") 87 | .to_kebab_case() 88 | }) 89 | } 90 | 91 | pub(super) fn rsx_attribute(&self) -> TokenStream { 92 | let ident = &self.ident; 93 | 94 | quote! { 95 | #ident, 96 | } 97 | } 98 | 99 | pub(super) fn new_instance(&self, shared: &Ident) -> TokenStream { 100 | let Self { 101 | ident, 102 | can_bubble, 103 | cancelable, 104 | .. 105 | } = &self; 106 | 107 | let web_event_name = self.web_event_name(); 108 | 109 | quote! { 110 | let #ident = ::dioxus_web_component::custom_event_handler( 111 | #shared.event_target().clone(), 112 | #web_event_name, 113 | ::dioxus_web_component::CustomEventOptions { 114 | can_bubble: #can_bubble, 115 | cancelable: #cancelable, 116 | }); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![allow(clippy::multiple_crate_versions)] 3 | 4 | use proc_macro::TokenStream; 5 | use syn::ItemFn; 6 | 7 | mod web_component; 8 | pub(crate) use self::web_component::WebComponent; 9 | 10 | mod parameter; 11 | pub(crate) use self::parameter::Parameter; 12 | 13 | mod attribute; 14 | pub(crate) use self::attribute::Attribute; 15 | 16 | mod properties; 17 | pub(crate) use self::properties::Property; 18 | 19 | mod event; 20 | pub(crate) use self::event::Event; 21 | 22 | pub(crate) mod tag; 23 | 24 | #[doc = include_str!("./doc.md")] 25 | #[proc_macro_attribute] 26 | pub fn web_component(args: TokenStream, input: TokenStream) -> TokenStream { 27 | let item = syn::parse_macro_input!(input as ItemFn); 28 | 29 | let mut errors = darling::Error::accumulator(); 30 | let wc = WebComponent::parse(args.into(), item, &mut errors); 31 | let result = wc.generate(&mut errors); 32 | 33 | if let Err(err) = errors.finish() { 34 | return TokenStream::from(err.write_errors()); 35 | } 36 | 37 | proc_macro::TokenStream::from(result) 38 | } 39 | 40 | #[cfg(test)] 41 | #[allow(clippy::expect_used)] 42 | mod tests { 43 | use super::*; 44 | use assert2::let_assert; 45 | use syn::ItemFn; 46 | 47 | #[test] 48 | fn should_parse_multiple_events() { 49 | let_assert!(Ok(args) = "".parse()); 50 | let input = "fn MyWebComponent( 51 | #[event] on_event: EventHandler, 52 | #[event] on_snake_evt: EventHandler, 53 | ) -> Element { 54 | rsx!() 55 | }"; 56 | let item = syn::parse_str::(input).expect("valid rust code"); 57 | 58 | let mut errors = darling::Error::accumulator(); 59 | let wc = WebComponent::parse(args, item, &mut errors); 60 | 61 | let tokens = wc.generate(&mut errors); 62 | let syntax_tree = syn::parse_file(&tokens.to_string()).expect("a file"); 63 | let formatted = prettyplease::unparse(&syntax_tree); 64 | insta::assert_snapshot!(formatted); 65 | 66 | // insta::assert_debug_snapshot!(tokens); 67 | // insta::assert_snapshot!(tokens); 68 | 69 | let errors = errors.finish(); 70 | errors.expect("no errors"); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/src/parameter.rs: -------------------------------------------------------------------------------- 1 | use darling::error::Accumulator; 2 | use proc_macro2::TokenStream; 3 | use quote::ToTokens as _; 4 | use syn::punctuated::Punctuated; 5 | use syn::token::Comma; 6 | use syn::{FnArg, Ident, Pat, PatIdent, PatType, Type}; 7 | 8 | use crate::{Attribute, Event, Property}; 9 | 10 | #[derive(Debug)] 11 | pub enum Parameter { 12 | Attribute(Attribute, Option), 13 | Property(Property), 14 | Event(Event), 15 | } 16 | 17 | impl Parameter { 18 | pub fn parse(errors: &mut Accumulator, inputs: &mut Punctuated) -> Vec { 19 | inputs 20 | .iter_mut() 21 | .filter_map(|arg| ParameterInfo::build(errors, arg)) 22 | .map(ParameterInfo::into_parameter) 23 | .collect() 24 | } 25 | } 26 | 27 | impl Parameter { 28 | pub fn struct_attribute(&self) -> TokenStream { 29 | match self { 30 | Self::Attribute(attr, _) => attr.struct_attribute(), 31 | Self::Property(prop) => prop.struct_attribute(), 32 | Self::Event(evt) => evt.struct_attribute(), 33 | } 34 | } 35 | 36 | pub fn new_instance(&self, shared: &Ident) -> TokenStream { 37 | match self { 38 | Self::Attribute(attr, _) => attr.new_instance(), 39 | Self::Property(prop) => prop.new_instance(), 40 | Self::Event(evt) => evt.new_instance(shared), 41 | } 42 | } 43 | 44 | pub fn ident(&self) -> Ident { 45 | match self { 46 | Self::Attribute(attr, _) => attr.ident.clone(), 47 | Self::Property(prop) => prop.ident.clone(), 48 | Self::Event(evt) => evt.ident.clone(), 49 | } 50 | } 51 | 52 | pub fn rsx_attribute(&self) -> TokenStream { 53 | match self { 54 | Self::Attribute(attr, _) => attr.rsx_attribute(), 55 | Self::Property(prop) => prop.rsx_attribute(), 56 | Self::Event(evt) => evt.rsx_attribute(), 57 | } 58 | } 59 | } 60 | 61 | struct ParameterInfo { 62 | ident: Ident, 63 | ty: Type, 64 | attribute: Option, 65 | property: Option, 66 | event: Option, 67 | } 68 | 69 | impl ParameterInfo { 70 | fn build(errors: &mut Accumulator, arg: &mut FnArg) -> Option { 71 | let FnArg::Typed(arg) = arg else { 72 | return None; 73 | }; 74 | 75 | let PatType { attrs, pat, ty, .. } = arg; 76 | let Pat::Ident(PatIdent { ident, .. }) = pat.as_ref() else { 77 | panic!("Expected an ident, got {pat:#?}"); 78 | }; 79 | 80 | let ident = ident.clone(); 81 | let ty = Type::clone(ty); 82 | let mut result = Self { 83 | ident, 84 | ty, 85 | attribute: None, 86 | property: None, 87 | event: None, 88 | }; 89 | 90 | attrs.retain(|attr| result.parse_attribute(errors, attr)); 91 | 92 | Some(result) 93 | } 94 | 95 | fn parse_attribute(&mut self, errors: &mut Accumulator, attr: &syn::Attribute) -> bool { 96 | if attr.path().is_ident("event") { 97 | let event = Event::parse(attr, self.ident.clone(), self.ty.clone()); 98 | self.event = errors.handle(event); 99 | false 100 | } else if attr.path().is_ident("property") { 101 | let property = Property::parse(attr, self.ident.clone(), self.ty.clone()); 102 | self.property = errors.handle(property); 103 | false 104 | } else if attr.path().is_ident("attribute") { 105 | let attribute = Attribute::parse(attr, self.ident.clone(), self.ty.clone()); 106 | self.attribute = errors.handle(attribute); 107 | false 108 | } else { 109 | true 110 | } 111 | } 112 | 113 | fn into_parameter(self) -> Parameter { 114 | let Self { 115 | ident, 116 | ty, 117 | attribute, 118 | property, 119 | event, 120 | } = self; 121 | 122 | match (attribute, property, event) { 123 | (Some(attr), prop, _) => Parameter::Attribute(attr, prop), 124 | (None, Some(prop), _) => Parameter::Property(prop), 125 | (None, None, Some(event)) => Parameter::Event(event), 126 | (None, None, None) => { 127 | let ty_str = ty.to_token_stream().to_string(); 128 | let is_event = 129 | ty_str.starts_with("EventHandler <") || ty_str.starts_with("Callback <"); 130 | if is_event { 131 | Parameter::Event(Event::new(ident, ty)) 132 | } else { 133 | Parameter::Attribute(Attribute::new(ident, ty), None) 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/src/properties.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::min_ident_chars)] 2 | 3 | use std::borrow::Cow; 4 | use std::fmt::Debug; 5 | 6 | use darling::error::Accumulator; 7 | use darling::FromMeta; 8 | use heck::{ToKebabCase, ToLowerCamelCase}; 9 | use proc_macro2::{Ident, TokenStream}; 10 | use quote::{quote, ToTokens}; 11 | use syn::ext::IdentExt; 12 | use syn::{Expr, GenericArgument, Meta, PathArguments, PathSegment, Type}; 13 | 14 | #[derive(Debug, FromMeta, Default)] 15 | struct PropertyReceiver { 16 | name: Option, 17 | readonly: Option, 18 | initial: Option, 19 | try_from_js: Option, 20 | try_into_js: Option, 21 | js_type: Option, 22 | } 23 | 24 | #[derive(Clone)] 25 | pub(super) struct Property { 26 | pub ident: Ident, 27 | ty: Type, 28 | name: Option, 29 | readonly: Option, 30 | initial: Option, 31 | try_from_js: Option, 32 | try_into_js: Option, 33 | js_type: Option, 34 | } 35 | 36 | impl Debug for Property { 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | f.debug_struct("Property") 39 | .field("ident", &self.ident.to_string()) 40 | .field("ty", &self.ty.to_token_stream().to_string()) 41 | .field("name", &self.name) 42 | .field("readonly", &self.readonly) 43 | .field("initial", &self.initial.to_token_stream().to_string()) 44 | .field( 45 | "try_from_js", 46 | &self.try_from_js.to_token_stream().to_string(), 47 | ) 48 | .field( 49 | "try_into_js", 50 | &self.try_into_js.to_token_stream().to_string(), 51 | ) 52 | .field("js_type", &self.js_type) 53 | .finish() 54 | } 55 | } 56 | 57 | impl Property { 58 | pub(super) fn parse( 59 | attr: &syn::Attribute, 60 | ident: Ident, 61 | ty: Type, 62 | ) -> Result { 63 | let receiver = if let Meta::List(_) = &attr.meta { 64 | PropertyReceiver::from_meta(&attr.meta)? 65 | } else { 66 | PropertyReceiver::default() 67 | }; 68 | 69 | let result = Self { 70 | ident, 71 | ty, 72 | name: receiver.name, 73 | readonly: receiver.readonly, 74 | initial: receiver.initial, 75 | try_from_js: receiver.try_from_js, 76 | try_into_js: receiver.try_into_js, 77 | js_type: receiver.js_type, 78 | }; 79 | Ok(result) 80 | } 81 | } 82 | 83 | impl Property { 84 | pub(super) fn name(&self) -> Cow { 85 | self.name.as_deref().map_or_else( 86 | || { 87 | let name = self.ident.unraw().to_string(); 88 | let name = name.as_str().to_kebab_case(); 89 | Cow::Owned(name) 90 | }, 91 | Cow::Borrowed, 92 | ) 93 | } 94 | 95 | pub(super) fn readonly(&self) -> bool { 96 | self.readonly.unwrap_or_default() 97 | } 98 | 99 | fn initial(&self) -> TokenStream { 100 | self.initial.as_ref().map_or_else( 101 | || { 102 | quote! { 103 | ::std::default::Default::default() 104 | } 105 | }, 106 | ToTokens::to_token_stream, 107 | ) 108 | } 109 | 110 | fn try_from_js_value(&self) -> TokenStream { 111 | self.try_from_js.as_ref().map_or_else( 112 | || { 113 | quote! { 114 | |value| value.try_into() 115 | } 116 | }, 117 | ToTokens::to_token_stream, 118 | ) 119 | } 120 | 121 | fn try_into_js_value(&self) -> TokenStream { 122 | self.try_into_js.as_ref().map_or_else( 123 | || { 124 | quote! { 125 | |value| value.try_into() 126 | } 127 | }, 128 | ToTokens::to_token_stream, 129 | ) 130 | } 131 | 132 | pub(super) fn new_property(&self) -> TokenStream { 133 | let name = self.js_name(); 134 | let readonly = self.readonly.unwrap_or_default(); 135 | 136 | quote! { 137 | ::dioxus_web_component::Property::new(#name, #readonly) 138 | } 139 | } 140 | 141 | pub(super) fn struct_attribute(&self) -> TokenStream { 142 | let Self { ident, ty, .. } = &self; 143 | quote! { 144 | #ident : ::dioxus::prelude::Signal<#ty> 145 | } 146 | } 147 | 148 | pub(super) fn new_instance(&self) -> TokenStream { 149 | let ident = &self.ident; 150 | let initial = self.initial(); 151 | quote! { 152 | let #ident = ::dioxus::prelude::use_signal(|| #initial); 153 | } 154 | } 155 | 156 | pub(super) fn pattern_set_property(&self) -> TokenStream { 157 | let ident = &self.ident; 158 | let name = self.name(); 159 | let try_from_js = self.try_from_js_value(); 160 | 161 | quote! { 162 | #name => { 163 | if let Ok(new_value) = Ok(value).and_then(#try_from_js) { 164 | self.#ident.set(new_value); 165 | } 166 | } 167 | } 168 | } 169 | 170 | pub(super) fn pattern_get_property(&self) -> TokenStream { 171 | let ident = &self.ident; 172 | let name = self.name(); 173 | let try_into_js = self.try_into_js_value(); 174 | 175 | quote! { 176 | #name => { 177 | let value = self.#ident.read().clone(); 178 | Ok(value) 179 | .and_then(#try_into_js) 180 | .unwrap_or_else(|err| { 181 | ::dioxus::logger::tracing::warn!("get {} conversion error {:?}, return undefined", #name, err); 182 | wasm_bindgen::JsValue::undefined() 183 | }) 184 | } 185 | } 186 | } 187 | 188 | pub(super) fn rsx_attribute(&self) -> TokenStream { 189 | let ident = &self.ident; 190 | 191 | quote! { 192 | #ident: #ident().clone(), 193 | } 194 | } 195 | 196 | pub(super) fn js_name(&self) -> String { 197 | self.name().to_lower_camel_case() 198 | } 199 | 200 | pub(super) fn js_type(&self, errors: &mut Accumulator) -> String { 201 | if let Some(ty) = &self.js_type { 202 | return ty.clone(); 203 | } 204 | extract_js_type(&self.ty, errors) 205 | } 206 | } 207 | 208 | #[allow(clippy::print_stderr)] 209 | // TODO add a warning 210 | // see https://github.com/rust-lang/rust/issues/54140 211 | fn extract_js_type(ty: &Type, errors: &mut Accumulator) -> String { 212 | let result = match ty { 213 | Type::Array(arr) => { 214 | let inner = extract_js_type(&arr.elem, errors); 215 | Some(format!("Array<{inner}>")) 216 | } 217 | Type::Group(grp) => Some(extract_js_type(&grp.elem, errors)), 218 | Type::Never(_) => Some("never".to_string()), 219 | Type::Paren(paren) => Some(extract_js_type(&paren.elem, errors)), 220 | Type::Tuple(tpl) => { 221 | let inner = tpl 222 | .elems 223 | .iter() 224 | .map(|ty| extract_js_type(ty, errors)) 225 | .collect::>(); 226 | Some(format!("[{}]", inner.join(", "))) 227 | } 228 | Type::Path(path) if path.path.segments.len() == 1 => 229 | { 230 | #[allow(clippy::indexing_slicing)] 231 | extract_path_segment_js_type(&path.path.segments[0], errors) 232 | } 233 | // TODO maybe detect some predefine path like std collections 234 | // Other cases are not handled 235 | _ => None, 236 | }; 237 | 238 | result.unwrap_or_else(|| { 239 | let msg = format!( 240 | "Oops, we cannot define the Javascript type for {ty:?}. 241 | Use the explicit `js_type` attribute on the property to define the expected type. 242 | Or, disable the typescript generation with `#[web_component(no_typescript = true, ...)]`" 243 | ); 244 | errors.push(darling::Error::custom(msg).with_span(ty)); 245 | "any".to_string() 246 | }) 247 | } 248 | 249 | #[allow(clippy::match_same_arms)] 250 | fn extract_path_segment_js_type(segment: &PathSegment, errors: &mut Accumulator) -> Option { 251 | let ident = segment.ident.to_string(); 252 | match ident.to_string().as_str() { 253 | "bool" => Some("boolean".to_string()), 254 | "u8" | "u16" | "u32" | "i16" | "i32" | "i64" | "f32" | "f64" => Some("number".to_string()), 255 | // it's probably better to have a number for usize/isize 256 | "isize" | "usize" => Some("number".to_string()), 257 | "u64" => Some("bigint".to_string()), 258 | "char" | "String" => Some("string".to_string()), 259 | "Option" => { 260 | let PathArguments::AngleBracketed(generics) = &segment.arguments else { 261 | return None; 262 | }; 263 | let Some(GenericArgument::Type(inner_ty)) = generics.args.first() else { 264 | return None; 265 | }; 266 | 267 | let inner = extract_js_type(inner_ty, errors); 268 | Some(format!("{inner} | null")) 269 | } 270 | "Vec" => { 271 | let PathArguments::AngleBracketed(generics) = &segment.arguments else { 272 | return None; 273 | }; 274 | let Some(GenericArgument::Type(inner_ty)) = generics.args.first() else { 275 | return None; 276 | }; 277 | 278 | let inner = extract_js_type(inner_ty, errors); 279 | Some(format!("Array<{inner}>")) 280 | } 281 | _ => None, 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/src/snapshots/dioxus_web_component_macro__tests__should_parse_multiple_events.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: dioxus-web-component-macro/src/lib.rs 3 | expression: formatted 4 | snapshot_kind: text 5 | --- 6 | #[component] 7 | fn MyWebComponent( 8 | on_event: EventHandler, 9 | on_snake_evt: EventHandler, 10 | ) -> Element { 11 | rsx!() 12 | } 13 | ///Register the `` web-component 14 | fn register_my_web_component() { 15 | let attributes = ::std::vec![]; 16 | let properties = ::std::vec![]; 17 | let style = ::dioxus_web_component::InjectedStyle::default(); 18 | ::dioxus_web_component::register_dioxus_web_component( 19 | "my-web-component", 20 | attributes, 21 | properties, 22 | style, 23 | my_web_component_builder, 24 | ); 25 | } 26 | ///The `MyWebComponentWebComponent` web-component that implement [`::dioxus_web_component::DioxusWebComponent`] 27 | #[automatically_derived] 28 | #[derive(Clone, Copy)] 29 | #[allow(dead_code)] 30 | struct MyWebComponentWebComponent { 31 | on_event: EventHandler, 32 | on_snake_evt: EventHandler, 33 | } 34 | #[automatically_derived] 35 | impl ::dioxus_web_component::DioxusWebComponent for MyWebComponentWebComponent { 36 | #[allow(clippy::single_match, clippy::redundant_closure)] 37 | fn set_attribute(&mut self, attribute: &str, new_value: Option) { 38 | match attribute { 39 | _ => { 40 | ::dioxus::logger::tracing::warn!("No attribute {attribute} to set"); 41 | } 42 | } 43 | } 44 | #[allow(clippy::single_match, clippy::redundant_closure)] 45 | fn set_property(&mut self, property: &str, value: ::wasm_bindgen::JsValue) { 46 | match property { 47 | _ => { 48 | ::dioxus::logger::tracing::warn!("No property {property} to set"); 49 | } 50 | } 51 | } 52 | #[allow(clippy::single_match, clippy::redundant_closure)] 53 | fn get_property(&mut self, property: &str) -> ::wasm_bindgen::JsValue { 54 | match property { 55 | _ => { 56 | ::dioxus::logger::tracing::warn!("No property {property} to get"); 57 | ::wasm_bindgen::JsValue::undefined() 58 | } 59 | } 60 | } 61 | } 62 | #[doc(hidden)] 63 | #[automatically_derived] 64 | #[allow(clippy::default_trait_access, clippy::clone_on_copy, clippy::redundant_closure)] 65 | fn my_web_component_builder() -> ::dioxus::prelude::Element { 66 | let mut __wc = ::dioxus::prelude::use_context::<::dioxus_web_component::Shared>(); 67 | let on_event = ::dioxus_web_component::custom_event_handler( 68 | __wc.event_target().clone(), 69 | "event", 70 | ::dioxus_web_component::CustomEventOptions { 71 | can_bubble: true, 72 | cancelable: true, 73 | }, 74 | ); 75 | let on_snake_evt = ::dioxus_web_component::custom_event_handler( 76 | __wc.event_target().clone(), 77 | "snake-evt", 78 | ::dioxus_web_component::CustomEventOptions { 79 | can_bubble: true, 80 | cancelable: true, 81 | }, 82 | ); 83 | let mut __my_web_component_web_component = MyWebComponentWebComponent { 84 | on_event, 85 | on_snake_evt, 86 | }; 87 | let __coroutine = ::dioxus::prelude::use_coroutine(move |mut rx| async move { 88 | use ::dioxus_web_component::{StreamExt, DioxusWebComponent}; 89 | while let Some(message) = rx.next().await { 90 | ::dioxus::prelude::spawn(async move { 91 | __my_web_component_web_component.handle_message(message); 92 | }); 93 | } 94 | }); 95 | ::dioxus::prelude::use_effect(move || { 96 | __wc.set_tx(__coroutine.tx()); 97 | }); 98 | rsx! { 99 | MyWebComponent { on_event, on_snake_evt, } 100 | } 101 | } 102 | #[::wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] 103 | const MY_WEB_COMPONENT_TYPESCRIPT: &str = "\nexport type MyWebComponentElement = HTMLElement & {\n \n};\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'my-web-component': MyWebComponentElement;\n }\n}"; 104 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/src/snapshots/dioxus_web_component_macro__web_component__tests__should_parse_attributes_args_with_error.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: dioxus-web-component-macro/src/web_component.rs 3 | expression: error 4 | snapshot_kind: text 5 | --- 6 | Error { 7 | kind: Custom( 8 | "a custom element tag should contains an hyphen '-', having \"toto\"", 9 | ), 10 | locations: [ 11 | "tag", 12 | ], 13 | span: Some( 14 | bytes(5..11), 15 | ), 16 | } 17 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/src/tag.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | use std::str::FromStr; 3 | 4 | use darling::{Error, FromMeta}; 5 | 6 | #[derive(Debug)] 7 | pub enum InvalidTagError { 8 | Empty, 9 | InvalidStartingLetter(String), 10 | NoHyphen(String), 11 | HasUpperCase(char, String), 12 | InvalidChar(char, String), 13 | ForbiddenName(String), 14 | } 15 | 16 | impl Display for InvalidTagError { 17 | #[allow(clippy::min_ident_chars)] 18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | match self { 20 | Self::Empty => write!(f, "need a non-empty custom element tag"), 21 | Self::InvalidStartingLetter(tag) => { 22 | write!( 23 | f, 24 | "a custom element tag should start with an ASCII lower case letter (a..z), having \"{tag}\"" 25 | ) 26 | } 27 | Self::NoHyphen(tag) => write!( 28 | f, 29 | "a custom element tag should contains an hyphen '-', having \"{tag}\"" 30 | ), 31 | Self::HasUpperCase(ch, tag) => { 32 | write!( 33 | f, 34 | "a custom element cannot contains an ASCII upper case letter, having \"{tag}\" containing '{ch}'" 35 | ) 36 | } 37 | Self::InvalidChar(ch, tag) => write!( 38 | f, 39 | "invalid char for a custom element tag \"{tag}\" containing '{ch}'" 40 | ), 41 | Self::ForbiddenName(s) => write!(f, "this custom element tag is reserved \"{s}\""), 42 | } 43 | } 44 | } 45 | 46 | const FORBIDDEN_NAMES: &[&str] = &[ 47 | "annotation-xml", 48 | "color-profile", 49 | "font-face", 50 | "font-face-src", 51 | "font-face-uri", 52 | "font-face-format", 53 | "font-face-name", 54 | "missing-glyph", 55 | ]; 56 | 57 | #[derive(Debug)] 58 | pub struct Tag(String); 59 | 60 | impl Tag { 61 | pub(crate) fn new(value: String) -> Self { 62 | Self(value) 63 | } 64 | } 65 | 66 | impl FromStr for Tag { 67 | type Err = InvalidTagError; 68 | 69 | fn from_str(value: &str) -> Result { 70 | check_tag(value)?; 71 | Ok(Self(value.to_owned())) 72 | } 73 | } 74 | 75 | impl FromMeta for Tag { 76 | fn from_string(value: &str) -> darling::Result { 77 | Tag::from_str(value).map_err(Error::custom) 78 | } 79 | } 80 | 81 | impl Display for Tag { 82 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 83 | write!(fmt, "{}", self.0) 84 | } 85 | } 86 | 87 | /// Check the tag validity 88 | /// 89 | /// See [MDN - Valid custom element names]() 90 | /// 91 | /// # Errors 92 | /// 93 | /// Fail if the tag is invalid. 94 | fn check_tag(tag: &str) -> Result<(), InvalidTagError> { 95 | // not empty 96 | let Some(start) = tag.chars().next() else { 97 | return Err(InvalidTagError::Empty); 98 | }; 99 | // start with an ASCII lower letter (a..=z) 100 | if !start.is_ascii_lowercase() { 101 | return Err(InvalidTagError::InvalidStartingLetter(tag.to_owned())); 102 | } 103 | // contains a hyphen 104 | if !tag.contains('-') { 105 | return Err(InvalidTagError::NoHyphen(tag.to_owned())); 106 | } 107 | // no ASCII uppercase 108 | let search = tag.chars().find(char::is_ascii_uppercase); 109 | if let Some(invalid_char) = search { 110 | return Err(InvalidTagError::HasUpperCase(invalid_char, tag.to_owned())); 111 | } 112 | // avoid some chars 113 | let search = tag.chars().skip(1).find(|ch| !valid_chars(*ch)); 114 | if let Some(invalid_char) = search { 115 | return Err(InvalidTagError::InvalidChar(invalid_char, tag.to_owned())); 116 | } 117 | // Forbidden 118 | let search = FORBIDDEN_NAMES.iter().find(|name| **name == tag); 119 | if let Some(name) = search { 120 | return Err(InvalidTagError::ForbiddenName((*name).to_string())); 121 | } 122 | 123 | Ok(()) 124 | } 125 | 126 | // See 127 | fn valid_chars(ch: char) -> bool { 128 | matches!(ch, '-' 129 | | '.' 130 | | '0'..='9' 131 | | 'a'..='z' 132 | | '\u{00B7}' 133 | | '\u{00C0}'..='\u{00D6}' 134 | | '\u{00D8}'..='\u{00F6}' 135 | | '\u{00F8}'..='\u{037D}' 136 | | '\u{037F}'..='\u{1FFF}' 137 | | '\u{200C}'..='\u{200D}' 138 | | '\u{203F}'..='\u{2040}' 139 | | '\u{2070}'..='\u{218F}' 140 | | '\u{2C00}'..='\u{2FEF}' 141 | | '\u{3001}'..='\u{D7FF}' 142 | | '\u{F900}'..='\u{FDCF}' 143 | | '\u{FDF0}'..='\u{FFFD}' 144 | | '\u{10000}'..='\u{EFFFF}' 145 | ) 146 | } 147 | 148 | #[cfg(test)] 149 | mod tests { 150 | use assert2::let_assert; 151 | use rstest::rstest; 152 | 153 | use super::*; 154 | 155 | #[rstest] 156 | #[case("a-a")] 157 | #[case("my-custom-tag")] 158 | #[case("i-love-🦀")] 159 | fn should_accept_valid_tag(#[case] tag: &str) { 160 | let result = check_tag(tag); 161 | let_assert!(Ok(()) = result); 162 | } 163 | 164 | #[rstest] 165 | #[case::empty("")] 166 | #[case::start_not_letter("-")] 167 | #[case::start_not_letter("1")] 168 | #[case::start_not_letter("_")] 169 | #[case::uppercase("my-CustomTag")] 170 | #[case::char("my-custom tag")] 171 | #[case::forbidden("annotation-xml")] 172 | #[case::forbidden("color-profile")] 173 | #[case::forbidden("font-face")] 174 | #[case::forbidden("font-face-src")] 175 | #[case::forbidden("font-face-uri")] 176 | #[case::forbidden("font-face-format")] 177 | #[case::forbidden("font-face-name")] 178 | #[case::forbidden("missing-glyph")] 179 | fn should_reject_invalid_tag(#[case] tag: &str) { 180 | let result = check_tag(tag); 181 | let_assert!(Err(_) = result); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/src/web_component.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::min_ident_chars)] 2 | 3 | use std::fmt::Debug; 4 | 5 | use darling::ast::NestedMeta; 6 | use darling::error::Accumulator; 7 | use darling::{Error, FromMeta}; 8 | use heck::{ToKebabCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; 9 | use proc_macro2::TokenStream; 10 | use quote::{format_ident, quote, ToTokens}; 11 | use syn::ext::IdentExt; 12 | use syn::{Expr, Ident, ItemFn}; 13 | 14 | use crate::tag::Tag; 15 | use crate::{Attribute, Parameter, Property}; 16 | 17 | #[derive(Debug, Default, FromMeta)] 18 | struct WebComponentReceiver { 19 | tag: Option, 20 | style: Option, 21 | no_typescript: Option, 22 | } 23 | impl WebComponentReceiver { 24 | fn parse(attr_args: TokenStream) -> Result { 25 | let attr_args = NestedMeta::parse_meta_list(attr_args)?; 26 | Self::from_list(&attr_args) 27 | } 28 | } 29 | 30 | pub(crate) struct WebComponent { 31 | tag: Tag, 32 | style: Option, 33 | parameters: Vec, 34 | item_fn: ItemFn, 35 | no_typescript: Option, 36 | } 37 | 38 | impl WebComponent { 39 | pub(crate) fn parse( 40 | attr_args: TokenStream, 41 | mut item_fn: ItemFn, 42 | errors: &mut Accumulator, 43 | ) -> Self { 44 | let WebComponentReceiver { 45 | tag, 46 | style, 47 | no_typescript, 48 | } = errors 49 | .handle(WebComponentReceiver::parse(attr_args)) 50 | .unwrap_or_default(); 51 | 52 | let tag = if let Some(tag) = tag { 53 | tag 54 | } else { 55 | let tag = item_fn.sig.ident.unraw().to_string().to_kebab_case(); 56 | errors 57 | .handle_in(|| { 58 | tag.parse() 59 | .map_err(|err| Error::custom(err).with_span(&item_fn.sig.ident)) 60 | }) 61 | .unwrap_or(Tag::new(tag)) 62 | }; 63 | 64 | let parameters = Parameter::parse(errors, &mut item_fn.sig.inputs); 65 | 66 | Self { 67 | tag, 68 | style, 69 | parameters, 70 | item_fn, 71 | no_typescript, 72 | } 73 | } 74 | 75 | fn attributes(&self) -> impl Iterator { 76 | self.parameters.iter().filter_map(|it| match it { 77 | Parameter::Attribute(attr, _) => Some(attr), 78 | Parameter::Property(_) | Parameter::Event(_) => None, 79 | }) 80 | } 81 | 82 | fn properties(&self) -> impl Iterator { 83 | self.parameters.iter().filter_map(|it| match it { 84 | Parameter::Property(prop) | Parameter::Attribute(_, Some(prop)) => Some(prop), 85 | Parameter::Attribute(_, None) | Parameter::Event(_) => None, 86 | }) 87 | } 88 | } 89 | 90 | impl WebComponent { 91 | pub fn generate(&self, errors: &mut Accumulator) -> TokenStream { 92 | let dioxus_component = self.dioxus_component(); 93 | let register_fn = self.register_fn(); 94 | let web_component = self.web_component(); 95 | let impl_web_component = self.impl_dioxus_web_component(); 96 | let builder_fn = self.builder_fn(); 97 | let typescript = self.typescript(errors); 98 | 99 | quote! { 100 | #dioxus_component 101 | #register_fn 102 | #web_component 103 | #impl_web_component 104 | #builder_fn 105 | #typescript 106 | } 107 | } 108 | 109 | fn dioxus_component(&self) -> TokenStream { 110 | let item_fn = &self.item_fn; 111 | quote! { 112 | #[component] 113 | #item_fn 114 | } 115 | } 116 | 117 | fn register_fn(&self) -> TokenStream { 118 | let visibility = &self.item_fn.vis; 119 | let name = self.item_fn.sig.ident.to_string(); 120 | let fn_name = format_ident!("register_{}", name.to_snake_case()); 121 | let attribute_names = self.attributes().map(|attr| attr.name()); 122 | let props = self.properties().map(Property::new_property); 123 | let style = self.style.as_ref().map_or_else( 124 | || { 125 | quote! { 126 | ::dioxus_web_component::InjectedStyle::default() 127 | } 128 | }, 129 | quote::ToTokens::to_token_stream, 130 | ); 131 | let tag = &self.tag.to_string(); 132 | let builder_name = self.builder_name(); 133 | 134 | let doc = format!("Register the `<{}>` web-component", self.tag); 135 | 136 | quote! { 137 | #[doc = #doc] 138 | #visibility fn #fn_name() { 139 | let attributes = ::std::vec![ 140 | #(#attribute_names.to_string()),* 141 | ]; 142 | let properties = ::std::vec![ 143 | #(#props),* 144 | ]; 145 | let style = #style; 146 | ::dioxus_web_component::register_dioxus_web_component(#tag, attributes, properties, style, #builder_name); 147 | } 148 | } 149 | } 150 | 151 | fn web_component(&self) -> TokenStream { 152 | let visibility = &self.item_fn.vis; 153 | let name = self.web_component_name(); 154 | 155 | let attributes = self.parameters.iter().map(Parameter::struct_attribute); 156 | 157 | let doc = format!( 158 | "The `{name}` web-component that implement [`::dioxus_web_component::DioxusWebComponent`]", 159 | ); 160 | 161 | quote! { 162 | #[doc = #doc] 163 | #[automatically_derived] 164 | #[derive(Clone, Copy)] 165 | #[allow(dead_code)] 166 | #visibility struct #name { 167 | #(#attributes),* 168 | } 169 | } 170 | } 171 | 172 | fn impl_dioxus_web_component(&self) -> TokenStream { 173 | let wc_name = self.web_component_name(); 174 | let attribute_patterns = self.attributes().map(Attribute::pattern_attribute_changed); 175 | 176 | let property_set = self 177 | .properties() 178 | .filter(|prop| !prop.readonly()) 179 | .map(Property::pattern_set_property); 180 | let property_get = self.properties().map(Property::pattern_get_property); 181 | 182 | quote! { 183 | #[automatically_derived] 184 | impl ::dioxus_web_component::DioxusWebComponent for #wc_name { 185 | #[allow(clippy::single_match, clippy::redundant_closure)] 186 | fn set_attribute(&mut self, attribute: &str, new_value: Option) { 187 | match attribute { 188 | #(#attribute_patterns)* 189 | _ => { 190 | ::dioxus::logger::tracing::warn!("No attribute {attribute} to set"); 191 | } 192 | } 193 | } 194 | 195 | #[allow(clippy::single_match, clippy::redundant_closure)] 196 | fn set_property(&mut self, property: &str, value: ::wasm_bindgen::JsValue) { 197 | match property { 198 | #(#property_set)* 199 | _ => { 200 | ::dioxus::logger::tracing::warn!("No property {property} to set"); 201 | } 202 | } 203 | } 204 | 205 | #[allow(clippy::single_match, clippy::redundant_closure)] 206 | fn get_property(&mut self, property: &str) -> ::wasm_bindgen::JsValue { 207 | match property { 208 | #(#property_get)* 209 | _ => { 210 | ::dioxus::logger::tracing::warn!("No property {property} to get"); 211 | ::wasm_bindgen::JsValue::undefined() 212 | } 213 | } 214 | } 215 | } 216 | } 217 | } 218 | 219 | fn builder_fn(&self) -> TokenStream { 220 | let name = &self.item_fn.sig.ident; 221 | let builder_name = self.builder_name(); 222 | let wc_name = self.web_component_name(); 223 | let instance_name = format_ident!("__{}", wc_name.to_string().to_snake_case()); 224 | let shared_name = format_ident!("__wc"); 225 | let coroutine_name = format_ident!("__coroutine"); 226 | 227 | let instances = self 228 | .parameters 229 | .iter() 230 | .map(|param| param.new_instance(&shared_name)); 231 | 232 | let all_idents = self.parameters.iter().map(Parameter::ident); 233 | 234 | let all_rsx_attributes = self.parameters.iter().map(Parameter::rsx_attribute); 235 | 236 | quote! { 237 | #[doc(hidden)] 238 | #[automatically_derived] 239 | #[allow(clippy::default_trait_access, clippy::clone_on_copy, clippy::redundant_closure)] 240 | fn #builder_name() -> ::dioxus::prelude::Element { 241 | let mut #shared_name = ::dioxus::prelude::use_context::<::dioxus_web_component::Shared>(); 242 | 243 | #(#instances)* 244 | 245 | let mut #instance_name = #wc_name { 246 | #(#all_idents),* 247 | }; 248 | 249 | let #coroutine_name = ::dioxus::prelude::use_coroutine(move |mut rx| async move { 250 | use ::dioxus_web_component::{StreamExt, DioxusWebComponent}; 251 | while let Some(message) = rx.next().await { 252 | ::dioxus::prelude::spawn(async move { 253 | #instance_name.handle_message(message); 254 | }); 255 | } 256 | }); 257 | 258 | ::dioxus::prelude::use_effect(move || { 259 | #shared_name.set_tx(#coroutine_name.tx()); 260 | }); 261 | 262 | rsx! { 263 | #name { 264 | #(#all_rsx_attributes)* 265 | } 266 | } 267 | } 268 | } 269 | } 270 | 271 | fn web_component_name(&self) -> Ident { 272 | let name = &self.item_fn.sig.ident; 273 | format_ident!("{name}WebComponent") 274 | } 275 | 276 | fn builder_name(&self) -> Ident { 277 | let name = &self.item_fn.sig.ident; 278 | format_ident!("{}_builder", name.to_string().to_snake_case()) 279 | } 280 | 281 | pub fn typescript(&self, errors: &mut Accumulator) -> TokenStream { 282 | if self.no_typescript.unwrap_or_default() { 283 | return quote! {}; 284 | } 285 | let name = &self.item_fn.sig.ident; 286 | let const_name = format_ident!("{}_TYPESCRIPT", name.to_string().to_shouty_snake_case()); 287 | let type_name = format!("{}Element", name.to_string().to_upper_camel_case()); 288 | let tag_name = self.tag.to_string(); 289 | let properties = self 290 | .properties() 291 | .map(|prop| { 292 | let name = prop.js_name(); 293 | let ty = prop.js_type(errors); 294 | if prop.readonly() { 295 | format!("readonly {name}: {ty};") 296 | } else { 297 | format!("{name}: {ty};") 298 | } 299 | }) 300 | .collect::>() 301 | .join("\n"); 302 | 303 | let definition = format!( 304 | " 305 | export type {type_name} = HTMLElement & {{ 306 | {properties} 307 | }}; 308 | 309 | declare global {{ 310 | interface HTMLElementTagNameMap {{ 311 | '{tag_name}': {type_name}; 312 | }} 313 | }}" 314 | ); 315 | 316 | quote! { 317 | #[::wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] 318 | const #const_name: &str = #definition; 319 | } 320 | } 321 | } 322 | 323 | impl Debug for WebComponent { 324 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 325 | f.debug_struct("WebComponent") 326 | .field("tag", &self.tag) 327 | .field("style", &self.style.to_token_stream().to_string()) 328 | .field("parameters", &self.parameters) 329 | .field("item_fn", &self.item_fn.sig.to_token_stream().to_string()) 330 | .field("no_typescript", &self.no_typescript) 331 | .finish() 332 | } 333 | } 334 | 335 | #[cfg(test)] 336 | mod tests { 337 | use assert2::let_assert; 338 | 339 | use super::*; 340 | 341 | #[test] 342 | fn should_parse_attributes_args() { 343 | let_assert!(Ok(args) = r#"tag="toto-tata""#.parse()); 344 | let result = WebComponentReceiver::parse(args); 345 | let_assert!(Ok(_) = result); 346 | } 347 | 348 | #[test] 349 | fn should_parse_attributes_args_with_error() { 350 | let_assert!(Ok(args) = r#"tag="toto""#.parse()); 351 | let result = WebComponentReceiver::parse(args); 352 | let_assert!(Err(error) = result); 353 | insta::assert_debug_snapshot!(error); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/tests/annotation_style.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | use std::convert::Infallible; 3 | 4 | use dioxus::prelude::*; 5 | use dioxus_web_component::{web_component, InjectedStyle}; 6 | use wasm_bindgen::JsValue; 7 | 8 | #[test] 9 | fn just_need_to_compile() {} 10 | 11 | #[web_component(tag = "plop-wc")] 12 | fn MyWebComponent( 13 | attr1: String, 14 | attr_option: Option, 15 | event: EventHandler, 16 | on_snake_evt: EventHandler, 17 | ) -> Element { 18 | rsx!() 19 | } 20 | 21 | #[web_component(style = InjectedStyle::css(":host {display:flex;}"))] 22 | fn MyWebComponent2( 23 | #[attribute] attr1: String, 24 | #[attribute] attr_option: Option, 25 | // #[property] prop: MyProp, 26 | #[property] prop: String, 27 | #[event] event: EventHandler, 28 | #[event] on_snake_evt: EventHandler, 29 | ) -> Element { 30 | rsx!() 31 | } 32 | 33 | #[web_component(no_typescript)] 34 | fn MyWebComponent3( 35 | #[attribute(name= "attr1", option = false, initial = String::new(), parse = |value| Some(value.to_string()))] 36 | attr1: String, 37 | #[attribute(name = "attr-option", option = true, initial = None, parse = |value| Some(value.to_string()))] 38 | attr_option: Option, 39 | #[property(readonly)] prop: Option, 40 | #[property( 41 | initial = MyProp(true), 42 | try_into_js = |prop| { 43 | let js_value = if prop.0 { 44 | JsValue::TRUE 45 | } else { 46 | JsValue::FALSE 47 | }; 48 | Ok::<_, Infallible>(js_value) 49 | }, 50 | try_from_js= |value| Ok::<_, Infallible>(MyProp(value.is_truthy())), 51 | )] 52 | prop2: MyProp, 53 | #[event(name = "event", no_bubble = false, no_cancel = false)] event: EventHandler, 54 | #[event(name = "snake-evt", no_bubble = false, no_cancel = false)] on_snake_evt: EventHandler< 55 | bool, 56 | >, 57 | ) -> Element { 58 | rsx!() 59 | } 60 | 61 | #[derive(Clone, PartialEq)] 62 | struct MyProp(bool); 63 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/tests/assets/failures/invalid_attribute.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_web_component::web_component; 3 | 4 | fn main() { 5 | } 6 | 7 | #[web_component] 8 | fn MyWebComponent( 9 | #[attribute(value= "name")] 10 | attr: String, 11 | ) -> Element { 12 | rsx!() 13 | } 14 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/tests/assets/failures/invalid_attribute.stderr: -------------------------------------------------------------------------------- 1 | error: Unknown field: `value` 2 | --> tests/assets/failures/invalid_attribute.rs:9:17 3 | | 4 | 9 | #[attribute(value= "name")] 5 | | ^^^^^ 6 | 7 | warning: unused import: `dioxus::prelude::*` 8 | --> tests/assets/failures/invalid_attribute.rs:1:5 9 | | 10 | 1 | use dioxus::prelude::*; 11 | | ^^^^^^^^^^^^^^^^^^ 12 | | 13 | = note: `#[warn(unused_imports)]` on by default 14 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/tests/assets/failures/invalid_tag_missing_hyphen.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_web_component::web_component; 3 | 4 | fn main() { 5 | } 6 | 7 | #[web_component] 8 | fn Component(attr: String) -> Element { 9 | rsx!() 10 | } 11 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/tests/assets/failures/invalid_tag_missing_hyphen.stderr: -------------------------------------------------------------------------------- 1 | error: a custom element tag should contains an hyphen '-', having "component" 2 | --> tests/assets/failures/invalid_tag_missing_hyphen.rs:8:4 3 | | 4 | 8 | fn Component(attr: String) -> Element { 5 | | ^^^^^^^^^ 6 | 7 | warning: unused import: `dioxus::prelude::*` 8 | --> tests/assets/failures/invalid_tag_missing_hyphen.rs:1:5 9 | | 10 | 1 | use dioxus::prelude::*; 11 | | ^^^^^^^^^^^^^^^^^^ 12 | | 13 | = note: `#[warn(unused_imports)]` on by default 14 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/tests/assets/failures/invalid_tag_upper_case.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_web_component::web_component; 3 | 4 | fn main() { 5 | } 6 | 7 | #[web_component(tag = "my-AwesomeComponent")] 8 | fn Component(attr: String) -> Element { 9 | rsx!() 10 | } 11 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/tests/assets/failures/invalid_tag_upper_case.stderr: -------------------------------------------------------------------------------- 1 | error: a custom element cannot contains an ASCII upper case letter, having "my-AwesomeComponent" containing 'A' 2 | --> tests/assets/failures/invalid_tag_upper_case.rs:7:23 3 | | 4 | 7 | #[web_component(tag = "my-AwesomeComponent")] 5 | | ^^^^^^^^^^^^^^^^^^^^^ 6 | 7 | error: a custom element tag should contains an hyphen '-', having "component" 8 | --> tests/assets/failures/invalid_tag_upper_case.rs:8:4 9 | | 10 | 8 | fn Component(attr: String) -> Element { 11 | | ^^^^^^^^^ 12 | 13 | warning: unused import: `dioxus::prelude::*` 14 | --> tests/assets/failures/invalid_tag_upper_case.rs:1:5 15 | | 16 | 1 | use dioxus::prelude::*; 17 | | ^^^^^^^^^^^^^^^^^^ 18 | | 19 | = note: `#[warn(unused_imports)]` on by default 20 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/tests/assets/success/basic_annotation.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_web_component::web_component; 3 | 4 | fn main() {} 5 | 6 | #[web_component] 7 | fn MyWebComponent( 8 | #[attribute] attr1: String, 9 | #[attribute] attr_option: Option, 10 | #[property] prop: String, 11 | #[event] event: EventHandler, 12 | #[event] on_snake_evt: EventHandler, 13 | ) -> Element { 14 | rsx!() 15 | } 16 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/tests/assets/success/full_annotation.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use dioxus::prelude::*; 4 | use dioxus_web_component::{web_component, InjectedStyle}; 5 | use wasm_bindgen::JsValue; 6 | 7 | fn main() {} 8 | 9 | #[web_component(tag ="plop-test", style = InjectedStyle::css(":host {display:flex;}"))] 10 | fn MyWebComponent( 11 | #[attribute(name= "attr1", option = false, initial = String::new(), parse = |value| Some(value.to_string()))] 12 | attr1: String, 13 | #[attribute(name = "attr-option", option = true, initial = None, parse = |value| Some(value.to_string()))] 14 | attr_option: Option, 15 | #[property(name = "plop", readonly)] prop: Option, 16 | #[property( 17 | initial = MyProp(true), 18 | try_into_js = |prop| { 19 | let js_value = if prop.0 { 20 | JsValue::TRUE 21 | } else { 22 | JsValue::FALSE 23 | }; 24 | Ok::<_, Infallible>(js_value) 25 | }, 26 | try_from_js= |value| Ok::<_, Infallible>(MyProp(value.is_truthy())), 27 | js_type = "boolean", 28 | )] 29 | prop2: MyProp, 30 | #[event(name = "event", no_bubble = false, no_cancel = false)] event: EventHandler, 31 | #[event(name = "snake-evt", no_bubble = false, no_cancel = false)] on_snake_evt: EventHandler< 32 | bool, 33 | >, 34 | ) -> Element { 35 | rsx!() 36 | } 37 | 38 | #[derive(Clone, PartialEq)] 39 | struct MyProp(bool); 40 | -------------------------------------------------------------------------------- /dioxus-web-component-macro/tests/assets/success/no_annotation.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_web_component::web_component; 3 | 4 | fn main() { 5 | } 6 | 7 | #[web_component] 8 | fn MyWebComponent( 9 | attr1: String, 10 | attr_option: Option, 11 | event: EventHandler, 12 | on_snake_evt: EventHandler, 13 | ) -> Element { 14 | rsx!() 15 | } -------------------------------------------------------------------------------- /dioxus-web-component-macro/tests/trybuild_tests.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | use trybuild::TestCases; 3 | 4 | #[test] 5 | fn should_success() { 6 | let tests = TestCases::new(); 7 | tests.pass("tests/assets/success/*.rs"); 8 | } 9 | 10 | #[test] 11 | #[ignore = "Fail with nightly because of a different output"] 12 | fn should_fail() { 13 | let tests = TestCases::new(); 14 | tests.compile_fail("tests/assets/failures/*.rs"); 15 | } 16 | -------------------------------------------------------------------------------- /dioxus-web-component/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.4.0](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-v0.3.2...dioxus-web-component-v0.4.0) - 2024-09-29 10 | - Support Dioxus 0.6 11 | 12 | ## [0.3.2](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-v0.3.1...dioxus-web-component-v0.3.2) - 2024-06-02 13 | 14 | ### Added 15 | - Generate the web-component typescript definition ([#43](https://github.com/ilaborie/dioxus-web-component/pull/43)) 16 | 17 | ### Other 18 | - fix typos ([#41](https://github.com/ilaborie/dioxus-web-component/pull/41)) 19 | 20 | ## [0.3.1](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-v0.3.0...dioxus-web-component-v0.3.1) - 2024-06-01 21 | 22 | ### Added 23 | - Allow attribute + property ([#38](https://github.com/ilaborie/dioxus-web-component/pull/38)) 24 | 25 | ## [0.3.0](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-v0.2.2...dioxus-web-component-v0.3.0) - 2024-05-31 26 | 27 | ### Added 28 | - [**breaking**] Javascript property support ([#36](https://github.com/ilaborie/dioxus-web-component/pull/36)) 29 | 30 | ### Other 31 | - fix doc 32 | 33 | ## [0.2.2](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-v0.2.1...dioxus-web-component-v0.2.2) - 2024-05-25 34 | 35 | ### Fixed 36 | - fix inifinite loop while setting attribute ([#33](https://github.com/ilaborie/dioxus-web-component/pull/33)) 37 | 38 | ## [0.2.1](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-v0.2.0...dioxus-web-component-v0.2.1) - 2024-05-23 39 | 40 | ### Fixed 41 | - Shared expose the HTML element ([#30](https://github.com/ilaborie/dioxus-web-component/pull/30)) 42 | 43 | ## [0.2.0](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-v0.1.3...dioxus-web-component-v0.2.0) - 2024-05-20 44 | 45 | ### Other 46 | - [**breaking**] remove async-channel dependency ([#27](https://github.com/ilaborie/dioxus-web-component/pull/27)) 47 | 48 | ## [0.1.3](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-v0.1.2...dioxus-web-component-v0.1.3) - 2024-05-18 49 | 50 | ### Added 51 | - check custom element tag validity ([#26](https://github.com/ilaborie/dioxus-web-component/pull/26)) 52 | - add const to InjectedStyle helper functions ([#25](https://github.com/ilaborie/dioxus-web-component/pull/25)) 53 | 54 | ### Other 55 | - fix typos 56 | 57 | ## [0.1.2](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-v0.1.1...dioxus-web-component-v0.1.2) - 2024-05-13 58 | 59 | ### Other 60 | - fix small doc issues ([#15](https://github.com/ilaborie/dioxus-web-component/pull/15)) 61 | 62 | ## [0.1.1](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-v0.1.0...dioxus-web-component-v0.1.1) - 2024-05-13 63 | 64 | ### Other 65 | - updated the following local packages: dioxus-web-component-macro 66 | 67 | ## [0.0.4](https://github.com/ilaborie/dioxus-web-component/compare/dioxus-web-component-v0.0.3...dioxus-web-component-v0.0.4) - 2024-05-06 68 | 69 | ### Added 70 | - add a proc macro ([#8](https://github.com/ilaborie/dioxus-web-component/pull/8)) 71 | 72 | ## [0.0.3](https://github.com/ilaborie/dioxus-web-component/compare/v0.0.2...v0.0.3) - 2024-05-01 73 | 74 | ### Added 75 | - inject style in the web component ([#6](https://github.com/ilaborie/dioxus-web-component/pull/6)) 76 | 77 | ## [0.0.2](https://github.com/ilaborie/dioxus-web-component/compare/v0.0.1...v0.0.2) - 2024-04-28 78 | 79 | ### Added 80 | - add custom event support ([#4](https://github.com/ilaborie/dioxus-web-component/pull/4)) 81 | -------------------------------------------------------------------------------- /dioxus-web-component/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dioxus-web-component" 3 | version = "0.4.0" 4 | edition = { workspace = true } 5 | authors = { workspace = true } 6 | license = { workspace = true } 7 | repository = { workspace = true } 8 | rust-version = { workspace = true } 9 | 10 | documentation = "https://docs.rs/dioxus-web-component" 11 | description = "Expose a Dioxus component as a Web Component" 12 | categories = ["gui", "wasm", "web-programming"] 13 | keywords = ["dioxus", "web-component", "wasm"] 14 | readme = "README.md" 15 | 16 | [features] 17 | default = ["macros"] 18 | 19 | macros = ["dep:dioxus-web-component-macro"] 20 | 21 | [dependencies] 22 | dioxus = { workspace = true, features = ["web", "logger"] } 23 | dioxus-web = "0.6.1" 24 | dioxus-web-component-macro = { version = "0.4.0", path = "../dioxus-web-component-macro", optional = true } 25 | futures = { workspace = true } 26 | wasm-bindgen = { workspace = true } 27 | wasm-bindgen-futures = { workspace = true } 28 | 29 | [dependencies.web-sys] 30 | workspace = true 31 | features = [ 32 | "Document", 33 | "Element", 34 | "HtmlElement", 35 | "Node", 36 | "Window", 37 | "CustomEvent", 38 | "ShadowRoot", 39 | ] 40 | 41 | [lints] 42 | workspace = true 43 | -------------------------------------------------------------------------------- /dioxus-web-component/LICENSE-APACHE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /dioxus-web-component/LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /dioxus-web-component/README.md: -------------------------------------------------------------------------------- 1 | # Dioxus Web Component 2 | 3 | This crate provides a bridge to expose a [Dioxus] component as a [web component]. 4 | 5 | This crate supports web component attributes and custom events. 6 | You can also add CSS style to your web component. 7 | 8 | Take a look at the examples to see the usage in a full project: 9 | 10 | 11 | 12 | If you are new to WebAssembly with Rust, take a look at the [Rust WebAssembly book] first. 13 | 14 | ## Usage with macro 15 | 16 | See [`web_component`] macro documentation for more details. 17 | 18 | Ideally, you only need to replace the Dioxus `#[component]` by `#[web_component]`. 19 | Then you should register the web component with [wasm-bindgen]. 20 | To finish, you can create the [npm] package with [wasm-pack]. 21 | 22 | 23 | ```rust 24 | use dioxus::prelude::*; 25 | use dioxus_web_component::web_component; 26 | use wasm_bindgen::prelude::*; 27 | 28 | #[web_component] 29 | fn MyWebComponent( 30 | attribute: String, 31 | on_event: EventHandler, 32 | ) -> Element { 33 | todo!() 34 | } 35 | 36 | // Function to call from the JS side 37 | #[wasm_bindgen] 38 | pub fn register() { 39 | // Register the web component (aka custom element) 40 | register_my_web_component(); 41 | } 42 | ``` 43 | 44 | Then call the function from the JS side. 45 | 46 | 47 | ### Customization of the web component 48 | 49 | The `#[web_component]` annotation can be configured with: 50 | 51 | * `tag` to set the HTML custom element tag name. 52 | By default, it's the kebab case version of the function name. 53 | * `style` to provide the [`InjectedStyle`] to your component. 54 | 55 | The parameters of the component could be: 56 | 57 | * an __attribute__ if you want to pass the parameter as an HTML attribute, 58 | * a __property__ if you only want to read/write the parameter as a property of the Javascript `HTMLElement`, 59 | * or an __event__ if the parameter is a Dioxus `EventHandler`. 60 | 61 | 💡TIP: You can be an attribute AND a property if you use the two annotations. 62 | 63 | #### Attributes 64 | 65 | Attributes are pure HTML attributes, should be deserialize from string. 66 | Attributes can be customized with the `#[attribute]` annotation with: 67 | 68 | * `name` to set the HTML attribute name. 69 | By default, it's the kebab-case of the parameter name. 70 | * `option` to mark the attribute optional. 71 | `true` by default if the type is `Option<...>`. 72 | * `initial` to set the default value when the HTML attribute is missing 73 | By default use the `std::default::Default` implementation of the type. 74 | * `parse` to provide the conversion between the HTML attribute value (a string) to the type value. 75 | By default use the `std::str::FromStr` implementation, and fall to the default value if it fails. 76 | 77 | 78 | #### Property 79 | 80 | Properties are custom properties accessible from Javascript. 81 | To declare a property, you need to use the `#[property]` annotation. 82 | 83 | We use [wasm-bindgen] to convert the Rust side value to a Javascript value. 84 | 85 | You can customize the property with these attributes: 86 | 87 | * `name` to set the Javascript name of the property. 88 | By default, it's the camelCase of the parameter name. 89 | * `readonly` to only generate the custom getter 90 | * `initial` to set the default value when the HTML attribute is missing 91 | By default use the `std::defaultDefault` implementation of the type. 92 | * `try_from_js` to provide the conversion from a `JsValue` to the parameter type. 93 | By default use the `std::convert::TryInto` implementation. 94 | The error case is ignored (does not set the value) 95 | * `try_into_js` to provide the conversion from the parameter type to a `JsValue`. 96 | By default use the `std::convert::TryInto` implementation. 97 | Return `undefined` in case of error 98 | 99 | ⚠️ WARN: reading a property value return a JS Promise. 100 | 101 | #### Events 102 | 103 | Events are parameters with the Dioxus `EventHandler<...>` type. 104 | You can customize the event with these attributes: 105 | 106 | * `name` to set the HTML event name. 107 | By default use the parameter name without the `on` prefix (if any) 108 | * `no_bubble` to forbid the custom event from bubbling 109 | * `no_cancel` to remove the ability to cancel the custom event 110 | 111 | 112 | ## Usage without macro 113 | 114 | Currently, the idea is to avoid breaking changes when you use the macros, 115 | but you should expect to have some in the API. 116 | 117 |
118 | The usage without macro is discouraged 119 | 120 | You can provide your manual implementation of [`DioxusWebComponent`] and call 121 | [`register_dioxus_web_component`] to register your web component. 122 | 123 | The key point is to use a `Shared` element in the dioxus context. 124 | 125 | 126 | For example, the greeting example could be written with 127 | 128 | ```rust, ignore 129 | use dioxus::prelude::*; 130 | use dioxus_web_component::{ 131 | register_dioxus_web_component, DioxusWebComponent, InjectedStyle, Message, Property, Shared, 132 | }; 133 | use wasm_bindgen::prelude::*; 134 | 135 | /// Install (register) the web component 136 | #[wasm_bindgen(start)] 137 | pub fn register() { 138 | register_greetings(); 139 | } 140 | 141 | #[component] 142 | fn Greetings(name: String) -> Element { 143 | rsx! { p { "Hello {name}!" } } 144 | } 145 | 146 | 147 | fn register_greetings() { 148 | let properties = vec![Property::new("name", false)]; 149 | let style = InjectedStyle::css(include_str!("style.css")); 150 | register_dioxus_web_component( 151 | "plop-greeting", 152 | vec!["name".to_string()], 153 | properties, 154 | style, 155 | greetings_builder, 156 | ); 157 | } 158 | 159 | #[derive(Clone, Copy)] 160 | struct GreetingsWebComponent { 161 | name: Signal, 162 | } 163 | 164 | impl DioxusWebComponent for GreetingsWebComponent { 165 | fn set_attribute(&mut self, attribute: &str, value: Option) { 166 | match attribute { 167 | "name" => { 168 | let new_value = value.and_then(|attr| attr.parse().ok()).unwrap_or_default(); 169 | self.name.set(new_value); 170 | } 171 | _ => { 172 | // nop 173 | } 174 | } 175 | } 176 | 177 | fn set_property(&mut self, property: &str, value: JsValue) { 178 | match property { 179 | // we allow to set the name as a property 180 | "name" => { 181 | if let Ok(new_value) = Ok(value).and_then(|value| value.try_into()) { 182 | self.name.set(new_value); 183 | } 184 | } 185 | _ => { 186 | // nop 187 | } 188 | } 189 | } 190 | 191 | fn get_property(&mut self, property: &str) -> JsValue { 192 | match property { 193 | // we allow to get the name as a property 194 | "name" => Ok(self.name.read().clone()) 195 | .and_then(|value| value.try_into()) 196 | .unwrap_or(::wasm_bindgen::JsValue::NULL), 197 | _ => JsValue::undefined(), 198 | } 199 | } 200 | } 201 | 202 | fn greetings_builder() -> Element { 203 | let mut wc = use_context::(); 204 | let name = use_signal(String::new); 205 | let mut greetings = GreetingsWebComponent { name }; 206 | let coroutine = use_coroutine::(move |mut rx| async move { 207 | use dioxus_web_component::StreamExt; 208 | while let Some(msg) = rx.next().await { 209 | greetings.handle_message(msg); 210 | } 211 | }); 212 | 213 | use_effect(move || { 214 | wc.set_tx(coroutine.tx()); 215 | }); 216 | 217 | rsx! { 218 | Greetings { 219 | name 220 | } 221 | } 222 | } 223 | 224 | ``` 225 | 226 | The counter example looks like this: 227 | 228 | ```rust 229 | use dioxus::prelude::*; 230 | use dioxus_web_component::{ 231 | custom_event_handler, register_dioxus_web_component, CustomEventOptions, DioxusWebComponent, 232 | }; 233 | use dioxus_web_component::{InjectedStyle, Message, Property, Shared}; 234 | use wasm_bindgen::prelude::*; 235 | 236 | /// Install (register) the web component 237 | ///#[wasm_bindgen(start)] 238 | pub fn register(){ 239 | // The register counter is generated by the `#[web_component(...)]` macro 240 | register_counter(); 241 | } 242 | 243 | /// The Dioxus component 244 | #[component] 245 | fn Counter(label: String, on_count: EventHandler) -> Element { 246 | let mut counter = use_signal(|| 0); 247 | 248 | rsx! { 249 | span { "{label}" } 250 | button { 251 | onclick: move |_| { 252 | counter += 1; 253 | on_count(counter()); 254 | }, 255 | "+" 256 | } 257 | output { "{counter}" } 258 | } 259 | } 260 | 261 | fn register_counter() { 262 | let properties = vec![Property::new("label", false)]; 263 | let style = InjectedStyle::stylesheet("./style.css"); 264 | register_dioxus_web_component("plop-counter", vec![], properties, style, counter_builder); 265 | } 266 | 267 | #[derive(Clone, Copy)] 268 | #[allow(dead_code)] 269 | struct CounterWebComponent { 270 | label: Signal, 271 | on_count: EventHandler, 272 | } 273 | 274 | impl DioxusWebComponent for CounterWebComponent { 275 | #[allow(clippy::single_match_else)] 276 | fn set_property(&mut self, property: &str, value: JsValue) { 277 | match property { 278 | "label" => { 279 | let new_value = String::try_from(value).unwrap_throw(); 280 | self.label.set(new_value); 281 | } 282 | _ => { 283 | // nop 284 | } 285 | } 286 | } 287 | 288 | #[allow(clippy::single_match_else)] 289 | fn get_property(&mut self, property: &str) -> JsValue { 290 | match property { 291 | "label" => { 292 | let value = self.label.read().clone(); 293 | value.into() 294 | } 295 | _ => JsValue::undefined(), 296 | } 297 | } 298 | } 299 | 300 | fn counter_builder() -> Element { 301 | let mut wc = use_context::(); 302 | let label = use_signal(String::new); 303 | let on_count = custom_event_handler(wc.event_target().clone(), "count", CustomEventOptions::default()); 304 | 305 | let mut counter = CounterWebComponent { label, on_count }; 306 | let coroutine = use_coroutine::(move |mut rx| async move { 307 | use dioxus_web_component::StreamExt; 308 | while let Some(msg) = rx.next().await { 309 | counter.handle_message(msg); 310 | } 311 | }); 312 | 313 | use_effect(move || { 314 | wc.set_tx(coroutine.tx()); 315 | }); 316 | 317 | rsx! { 318 | Counter { 319 | label, 320 | on_count 321 | } 322 | } 323 | } 324 | ``` 325 | 326 |
327 | 328 | ## Limitations 329 | 330 | * only extends `HTMLElement` 331 | * only work as a replacement of Dioxus `#[component]` annotation (does not work with handmade `Props`) 332 | * cannot add a method callable from Javascript in the web component. 333 | * property getters return a JS promise 334 | 335 | ## Contributions 336 | 337 | Contributions are welcome ❤️. 338 | 339 | 340 | [Dioxus]: https://dioxuslabs.com/ 341 | [web component]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components 342 | [wasm-bindgen]: https://github.com/rustwasm/wasm-bindgen 343 | [npm]: https://www.npmjs.com/ 344 | [wasm-pack]: https://github.com/rustwasm/wasm-pack 345 | [Rust WebAssembly book]: https://rustwasm.github.io/docs/book/ 346 | [dioxus-web-component-macro]: https://github.com/ilaborie/dioxus-web-component/blob/main/dioxus-web-component-macro/README.md 347 | -------------------------------------------------------------------------------- /dioxus-web-component/src/event.rs: -------------------------------------------------------------------------------- 1 | use dioxus::logger::tracing::debug; 2 | use dioxus::prelude::EventHandler; 3 | use wasm_bindgen::{JsValue, UnwrapThrowExt}; 4 | use web_sys::{CustomEvent, EventTarget}; 5 | 6 | /// HTML custom event options 7 | /// 8 | /// See [MDN - custom event](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) 9 | /// 10 | /// Note that by default `can_bubble` & `cancelable` are `true` 11 | #[derive(Clone, Copy)] 12 | pub struct CustomEventOptions { 13 | /// Is the event bubble up through the DOM tree 14 | /// 15 | /// See [MDN - bubbles](https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles) 16 | pub can_bubble: bool, 17 | 18 | /// Is the event is cancelable 19 | /// 20 | /// See [MDN - cancelable](https://developer.mozilla.org/en-US/docs/Web/API/Event/cancelable) 21 | pub cancelable: bool, 22 | } 23 | 24 | impl Default for CustomEventOptions { 25 | fn default() -> Self { 26 | Self { 27 | can_bubble: true, 28 | cancelable: true, 29 | } 30 | } 31 | } 32 | 33 | /// Create a Dioxus event handler that send an HTML custom event 34 | pub fn custom_event_handler( 35 | target: impl AsRef + 'static, 36 | event_type: &'static str, 37 | options: CustomEventOptions, 38 | ) -> EventHandler 39 | where 40 | T: Into + 'static, 41 | { 42 | EventHandler::new(move |value: T| { 43 | let CustomEventOptions { 44 | can_bubble, 45 | cancelable, 46 | } = options; 47 | let event = CustomEvent::new(event_type).unwrap_throw(); 48 | let detail = value.into(); 49 | event.init_custom_event_with_can_bubble_and_cancelable_and_detail( 50 | event_type, can_bubble, cancelable, &detail, 51 | ); 52 | debug!(?event, "dispatch event"); 53 | let target = target.as_ref(); 54 | target.dispatch_event(&event).unwrap_throw(); 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /dioxus-web-component/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![allow(clippy::multiple_crate_versions)] 3 | 4 | use std::sync::Arc; 5 | use std::sync::RwLock; 6 | 7 | use dioxus::dioxus_core::Element; 8 | use dioxus::hooks::UnboundedSender; 9 | use dioxus::logger::tracing::debug; 10 | use futures::channel::oneshot; 11 | use wasm_bindgen::prelude::*; 12 | use web_sys::HtmlElement; 13 | 14 | use crate::rust_component::RustComponent; 15 | 16 | pub use dioxus_web_component_macro::web_component; 17 | 18 | mod event; 19 | pub use self::event::*; 20 | 21 | mod style; 22 | pub use self::style::*; 23 | 24 | mod rust_component; 25 | 26 | /// Re-export, use this trait in the coroutine 27 | pub use futures::StreamExt; 28 | 29 | /// Message from web component to dioxus 30 | #[derive(Debug)] 31 | #[non_exhaustive] 32 | pub enum Message { 33 | /// Set attribute 34 | SetAttribute { 35 | /// Attribute name 36 | name: String, 37 | /// Attribute value 38 | value: Option, 39 | }, 40 | /// Get property 41 | Get { 42 | /// Property name 43 | name: String, 44 | /// reply channel 45 | tx: oneshot::Sender, 46 | }, 47 | /// Set property 48 | Set { 49 | /// Property name 50 | name: String, 51 | /// Property value 52 | value: SharedJsValue, 53 | }, 54 | } 55 | 56 | #[derive(Clone)] 57 | struct SharedEventTarget(web_sys::HtmlElement); 58 | 59 | #[allow(unsafe_code)] 60 | // SAFETY: 61 | // In a Web WASM context, without thread. 62 | // This only be used to display an event, no update are made here 63 | unsafe impl Send for SharedEventTarget {} 64 | 65 | #[allow(unsafe_code)] 66 | // SAFETY: 67 | // In a Web WASM context, without thread. 68 | // This only be used to display an event, no update are made here 69 | unsafe impl Sync for SharedEventTarget {} 70 | 71 | #[doc(hidden)] 72 | #[derive(Debug, Clone)] 73 | pub struct SharedJsValue(JsValue); 74 | 75 | #[allow(unsafe_code)] 76 | // SAFETY: 77 | // In a Web WASM context, without thread. 78 | // This only be used to display an event, no update are made here 79 | unsafe impl Send for SharedJsValue {} 80 | 81 | #[allow(unsafe_code)] 82 | // SAFETY: 83 | // In a Web WASM context, without thread. 84 | // This only be used to display an event, no update are made here 85 | unsafe impl Sync for SharedJsValue {} 86 | 87 | /// A context provided by the web component 88 | #[derive(Clone)] 89 | pub struct Shared { 90 | attributes: Vec, 91 | event_target: SharedEventTarget, 92 | tx: Arc>>>, 93 | } 94 | 95 | impl Shared { 96 | /// The web component event target use to dispatch custom event 97 | #[must_use] 98 | pub fn event_target(&self) -> &HtmlElement { 99 | &self.event_target.0 100 | } 101 | 102 | /// Set the receiver 103 | pub fn set_tx(&mut self, tx: UnboundedSender) { 104 | // initial state 105 | let trg = self.event_target(); 106 | for attr in &self.attributes { 107 | let Some(value) = trg.get_attribute(attr) else { 108 | continue; 109 | }; 110 | let _ = tx.unbounded_send(Message::SetAttribute { 111 | name: attr.to_string(), 112 | value: Some(value), 113 | }); 114 | } 115 | 116 | // Keep sender (skip if poisoned) 117 | if let Ok(mut cell) = self.tx.write() { 118 | *cell = Some(tx); 119 | } 120 | } 121 | } 122 | 123 | /// Dioxus web component 124 | pub trait DioxusWebComponent { 125 | /// Set an HTML attribute 126 | fn set_attribute(&mut self, attribute: &str, value: Option) { 127 | let _ = value; 128 | let _ = attribute; 129 | } 130 | 131 | /// Set a property 132 | fn set_property(&mut self, property: &str, value: JsValue) { 133 | let _ = value; 134 | let _ = property; 135 | } 136 | 137 | /// Get a property 138 | fn get_property(&mut self, property: &str) -> JsValue { 139 | let _ = property; 140 | JsValue::undefined() 141 | } 142 | 143 | /// Handle a message 144 | fn handle_message(&mut self, message: Message) { 145 | debug!(?message, "handle message"); 146 | match message { 147 | Message::SetAttribute { name, value } => self.set_attribute(&name, value), 148 | Message::Get { name, tx } => { 149 | let value = self.get_property(&name); 150 | let _ = tx.send(SharedJsValue(value)); 151 | } 152 | Message::Set { name, value } => self.set_property(&name, value.0), 153 | } 154 | } 155 | } 156 | 157 | /// Property 158 | #[wasm_bindgen(skip_typescript)] 159 | #[derive(Debug, Clone)] 160 | pub struct Property { 161 | /// Name 162 | name: String, 163 | /// Readonly 164 | readonly: bool, 165 | } 166 | 167 | impl Property { 168 | /// Create a property 169 | pub fn new(name: impl Into, readonly: bool) -> Self { 170 | let name = name.into(); 171 | Self { name, readonly } 172 | } 173 | } 174 | 175 | #[wasm_bindgen] 176 | impl Property { 177 | /// Get name 178 | #[wasm_bindgen(getter)] 179 | #[must_use] 180 | pub fn name(&self) -> String { 181 | self.name.clone() 182 | } 183 | 184 | /// Is property readonly 185 | #[wasm_bindgen(getter)] 186 | #[must_use] 187 | pub fn readonly(&self) -> bool { 188 | self.readonly 189 | } 190 | } 191 | 192 | /// Register a Dioxus web component 193 | pub fn register_dioxus_web_component( 194 | custom_tag: &str, 195 | attributes: Vec, 196 | properties: Vec, 197 | style: InjectedStyle, 198 | dx_el_builder: fn() -> Element, 199 | ) { 200 | let rust_component = RustComponent { 201 | attributes, 202 | properties, 203 | style, 204 | dx_el_builder, 205 | }; 206 | register_web_component(custom_tag, rust_component); 207 | } 208 | 209 | #[wasm_bindgen(module = "/src/shim.js")] 210 | extern "C" { 211 | #[allow(unsafe_code)] 212 | fn register_web_component(custom_tag: &str, rust_component: RustComponent); 213 | } 214 | -------------------------------------------------------------------------------- /dioxus-web-component/src/rust_component.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, RwLock}; 2 | 3 | use dioxus::hooks::UnboundedSender; 4 | use dioxus::logger::tracing::{debug, warn}; 5 | use dioxus::prelude::LaunchBuilder; 6 | use dioxus::web::Config; 7 | use futures::channel::oneshot; 8 | use wasm_bindgen::prelude::*; 9 | use wasm_bindgen_futures::spawn_local; 10 | use web_sys::{window, HtmlElement, ShadowRoot}; 11 | 12 | use crate::{InjectedStyle, Message, Property, Shared, SharedEventTarget, SharedJsValue}; 13 | 14 | pub(crate) type DxElBuilder = fn() -> dioxus::dioxus_core::Element; 15 | 16 | /// The Rust component 17 | #[wasm_bindgen(skip_typescript)] 18 | pub struct RustComponent { 19 | pub(crate) attributes: Vec, 20 | pub(crate) properties: Vec, 21 | pub(crate) style: InjectedStyle, 22 | pub(crate) dx_el_builder: DxElBuilder, 23 | } 24 | 25 | #[wasm_bindgen] 26 | impl RustComponent { 27 | #[wasm_bindgen(getter)] 28 | pub fn attributes(&self) -> Vec { 29 | self.attributes.clone() 30 | } 31 | 32 | #[wasm_bindgen(getter)] 33 | pub fn properties(&self) -> Vec { 34 | self.properties.clone() 35 | } 36 | 37 | #[wasm_bindgen(js_name = "newInstance")] 38 | pub fn new_instance(&self, root: &ShadowRoot) -> RustComponentInstance { 39 | debug!(?root, "new instance"); 40 | let window = window().unwrap_throw(); 41 | let document = window.document().unwrap_throw(); 42 | self.style.inject(&document, root); 43 | 44 | // XXX Create an element to attach the dioxus component 45 | // Dioxus require a `web_sys::Element`, and ShadowRoot is not an Element 46 | // So we use a `
` to wrap the component 47 | let inner_elt = document.create_element("div").unwrap_throw(); 48 | inner_elt.set_class_name("dioxus"); 49 | root.append_child(&inner_elt).unwrap_throw(); 50 | 51 | RustComponentInstance { 52 | attributes: self.attributes(), 53 | inner: inner_elt.into(), 54 | dx_el_builder: self.dx_el_builder, 55 | tx: Arc::default(), 56 | } 57 | } 58 | } 59 | 60 | #[wasm_bindgen(skip_typescript)] 61 | pub struct RustComponentInstance { 62 | attributes: Vec, 63 | inner: web_sys::Node, 64 | dx_el_builder: DxElBuilder, 65 | tx: Arc>>>, 66 | } 67 | 68 | #[wasm_bindgen] 69 | impl RustComponentInstance { 70 | pub fn connect(&mut self, event_target: &HtmlElement) { 71 | debug!(host = ?event_target, "Connect"); 72 | let ctx = Shared { 73 | attributes: self.attributes.clone(), 74 | event_target: SharedEventTarget(event_target.clone()), 75 | tx: Arc::clone(&self.tx), 76 | }; 77 | 78 | let node = self.inner.clone().unchecked_into(); 79 | let config = Config::new().rootnode(node); 80 | LaunchBuilder::web() 81 | .with_cfg(config) 82 | .with_context(ctx) 83 | .launch(self.dx_el_builder); 84 | } 85 | 86 | fn send(&mut self, message: Message) { 87 | debug!(?message, "sending message"); 88 | let tx = Arc::clone(&self.tx); 89 | spawn_local(async move { 90 | // Read (skip if poisoned) 91 | if let Ok(sender) = tx.try_read() { 92 | if let Some(sender) = sender.as_ref() { 93 | let _ = sender.unbounded_send(message); 94 | } 95 | } 96 | }); 97 | } 98 | 99 | #[wasm_bindgen(js_name = "attributeChanged")] 100 | #[allow(clippy::needless_pass_by_value)] 101 | pub fn attribute_changed( 102 | &mut self, 103 | name: String, 104 | old_value: Option, 105 | new_value: Option, 106 | ) { 107 | debug!(%name, ?old_value, ?new_value, "attribute changed"); 108 | if old_value != new_value { 109 | self.send(Message::SetAttribute { 110 | name, 111 | value: new_value, 112 | }); 113 | } 114 | } 115 | 116 | #[wasm_bindgen(js_name = "getProperty")] 117 | pub async fn get_property(&mut self, name: String) -> JsValue { 118 | debug!(%name, "get property"); 119 | let (tx, rx) = oneshot::channel(); 120 | self.send(Message::Get { name, tx }); 121 | match rx.await { 122 | Ok(SharedJsValue(value)) => value, 123 | Err(error) => { 124 | warn!(?error, "Fail to get property"); 125 | JsValue::undefined() 126 | } 127 | } 128 | } 129 | 130 | #[wasm_bindgen(js_name = "setProperty")] 131 | pub fn set_property(&mut self, name: String, value: JsValue) { 132 | debug!(%name, ?value, "set property"); 133 | let value = SharedJsValue(value); 134 | let message = Message::Set { name, value }; 135 | self.send(message); 136 | } 137 | 138 | pub fn disconnect(&mut self) { 139 | debug!("disconnect"); 140 | // Skip if poisoned 141 | if let Ok(mut tx) = self.tx.write() { 142 | tx.take(); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /dioxus-web-component/src/shim.js: -------------------------------------------------------------------------------- 1 | export function register_web_component(custom_tag, rust_component) { 2 | customElements.define( 3 | custom_tag, 4 | class extends HTMLElement { 5 | static get observedAttributes() { 6 | return rust_component.attributes; 7 | } 8 | 9 | constructor() { 10 | super(); 11 | this.attachShadow({ mode: "open" }); 12 | const instance = rust_component.newInstance(this.shadowRoot); 13 | for (const prop of rust_component.properties) { 14 | const { name, readonly } = prop; 15 | if (readonly) { 16 | Object.defineProperty(this, name, { 17 | get() { 18 | return instance.getProperty(name); 19 | }, 20 | }); 21 | } else { 22 | Object.defineProperty(this, name, { 23 | get() { 24 | return instance.getProperty(name); 25 | }, 26 | set(value) { 27 | instance.setProperty(name, value); 28 | }, 29 | }); 30 | } 31 | } 32 | this.instance = instance; 33 | } 34 | 35 | attributeChangedCallback(name, oldValue, newValue) { 36 | this.instance.attributeChanged(name, oldValue, newValue); 37 | } 38 | 39 | connectedCallback() { 40 | this.instance.connect(this); 41 | } 42 | 43 | disconnectedCallback() { 44 | this.instance.disconnect(); 45 | } 46 | }, 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /dioxus-web-component/src/style.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use wasm_bindgen::UnwrapThrowExt as _; 4 | use web_sys::{Document, ShadowRoot}; 5 | 6 | /// Provide style to the web component 7 | /// 8 | /// Typical usage: 9 | /// 10 | /// ```rust, ignore 11 | /// use dioxus_web_component::InjectedStyle; 12 | /// 13 | /// const STYLE: InjectedStyle = InjectedStyle::css(include_str!("../style.css")); 14 | /// ``` 15 | #[derive(Debug, Clone, Default)] 16 | pub enum InjectedStyle { 17 | /// No style provided 18 | #[default] 19 | None, 20 | /// Raw CSS content to go in an HTML ` 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/counter/index.js: -------------------------------------------------------------------------------- 1 | import start from "./pkg/counter.js"; 2 | 3 | // Register the web component 4 | await start(); 5 | 6 | // Register set label buttons 7 | for (const btnSet of document.querySelectorAll("button.set")) { 8 | btnSet.onclick = () => { 9 | const label = prompt(`What's the counter label?`); 10 | if (label) { 11 | const elt = btnSet.parentElement.querySelector("plop-counter"); 12 | elt.label = label; 13 | } 14 | }; 15 | } 16 | 17 | // Register get label buttons 18 | for (const btnGet of document.querySelectorAll("button.get")) { 19 | btnGet.onclick = () => { 20 | const {label} = btnGet.parentElement.querySelector("plop-counter"); 21 | label.then((label) => alert(`Counter label: ${label}`)); 22 | }; 23 | } 24 | 25 | // Register 'count' custom events 26 | document.querySelectorAll("plop-counter").forEach((el, index) => { 27 | el.addEventListener("count", (evt) => { 28 | console.log(`plop-counter #${index}`, evt.detail); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/counter/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![allow(clippy::multiple_crate_versions)] 3 | 4 | use dioxus::logger::tracing::Level; 5 | use dioxus::{logger, prelude::*}; 6 | use dioxus_web_component::{web_component, InjectedStyle}; 7 | use wasm_bindgen::prelude::*; 8 | 9 | /// Install (register) the web component 10 | #[wasm_bindgen(start)] 11 | pub fn register() { 12 | let _ = logger::init(Level::INFO); 13 | 14 | // The register counter is generated by the `#[web_component(...)]` macro 15 | register_counter(); 16 | } 17 | 18 | /// The Dioxus component 19 | #[web_component(tag = "plop-counter", style = InjectedStyle::stylesheet("./style.css"))] 20 | fn Counter( 21 | // The label is only available with a property 22 | #[property] label: String, 23 | // This component can trigger a custom 'count' event 24 | on_count: EventHandler, 25 | ) -> Element { 26 | let mut counter = use_signal(|| 0); 27 | 28 | rsx! { 29 | span { "{label}" } 30 | button { 31 | onclick: move |_| { 32 | counter += 1; 33 | on_count(counter()); 34 | }, 35 | "+" 36 | } 37 | output { "{counter}" } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/counter/style.css: -------------------------------------------------------------------------------- 1 | :host { 2 | outline: medium dotted red; 3 | margin: .5rem 1rem; 4 | } 5 | 6 | .dioxus { 7 | display: flex; 8 | max-width: 10ch; 9 | align-items: center; 10 | justify-content: space-evenly; 11 | gap: .5rem; 12 | padding: .25rem; 13 | } 14 | 15 | output { 16 | font-family: monospace; 17 | } -------------------------------------------------------------------------------- /examples/dx-in-dx/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /dist/ 5 | /static/ 6 | /.dioxus/ 7 | 8 | # this file will generate by tailwind: 9 | /assets/tailwind.css 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | -------------------------------------------------------------------------------- /examples/dx-in-dx/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dx-in-dx" 3 | version = "0.1.0" 4 | edition = { workspace = true } 5 | authors = { workspace = true } 6 | license = { workspace = true } 7 | repository = { workspace = true } 8 | 9 | description = "Dioxus in Dioxus web component example" 10 | keywords = ["dioxus", "web-component", "wasm"] 11 | categories = ["wasm", "web-programming"] 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | dioxus = { workspace = true, features = ["web"] } 16 | dioxus-web-component = { path = "../../dioxus-web-component" } 17 | wasm-bindgen = { workspace = true } 18 | 19 | [lints] 20 | workspace = true 21 | -------------------------------------------------------------------------------- /examples/dx-in-dx/Dioxus.toml: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | # App (Project) Name 4 | name = "dx-in-dx" 5 | 6 | # Dioxus App Default Platform 7 | # web, desktop, fullstack 8 | default_platform = "web" 9 | 10 | # `build` & `serve` dist path 11 | out_dir = "dist" 12 | 13 | # resource (assets) file folder 14 | asset_dir = "assets" 15 | 16 | [web.app] 17 | 18 | # HTML title tag content 19 | title = "dx-in-dx" 20 | 21 | [web.watcher] 22 | 23 | # when watcher trigger, regenerate the `index.html` 24 | reload_html = true 25 | 26 | # which files or dirs will be watcher monitoring 27 | watch_path = ["src", "assets"] 28 | 29 | # include `assets` in web platform 30 | [web.resource] 31 | 32 | # CSS style file 33 | 34 | style = [] 35 | 36 | # Javascript code file 37 | script = [] 38 | 39 | [web.resource.dev] 40 | 41 | # Javascript code file 42 | # serve: [dev-server] only 43 | script = [] 44 | -------------------------------------------------------------------------------- /examples/dx-in-dx/README.md: -------------------------------------------------------------------------------- 1 | # dx-in-dx example 2 | 3 | This example is a traditional Dioxus application. 4 | 5 | It's show how to use a web-component inside a dioxus component. 6 | 7 | Here the `plop-link` web-component is also build with dioxus. 8 | 9 | 10 | ```bash 11 | dx serve --hot-reload 12 | ``` 13 | 14 | - Open the browser to 15 | -------------------------------------------------------------------------------- /examples/dx-in-dx/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilaborie/dioxus-web-component/ee2377a6a7ea2fce55a45662f8075456ee898392/examples/dx-in-dx/assets/favicon.ico -------------------------------------------------------------------------------- /examples/dx-in-dx/assets/header.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/dx-in-dx/assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #111216; 3 | color: white; 4 | } 5 | 6 | #main { 7 | margin: 0; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 13 | } 14 | 15 | #links { 16 | width: 400px; 17 | text-align: left; 18 | font-size: x-large; 19 | color: white; 20 | display: flex; 21 | flex-direction: column; 22 | } 23 | 24 | #header { 25 | max-width: 1200px; 26 | } -------------------------------------------------------------------------------- /examples/dx-in-dx/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![allow(clippy::multiple_crate_versions)] 3 | 4 | use dioxus::prelude::*; 5 | use dioxus_web_component::{web_component, InjectedStyle}; 6 | 7 | /// The main application 8 | /// 9 | /// This is a Dioxus standard component 10 | #[component] 11 | pub fn App() -> Element { 12 | rsx! { 13 | h1 { "You can use web-component in Dioxus" } 14 | h2 { "You can write web-component with Dioxus" } 15 | h3 { "Therefore you can write dioxus web-component in Dioxus" } 16 | div { id: "links", 17 | // We use the web-component inside dioxus 18 | plop-link { href: "https://dioxuslabs.com/learn/", "📚 Learn Dioxus" } 19 | plop-link { href: "https://docs.rs/dioxus-web-component/latest/dioxus_web_component/", 20 | "🕸️ Dioxus web components" 21 | } 22 | } 23 | } 24 | } 25 | 26 | /// A link component 27 | /// 28 | /// This is a web-component `plop-link` build with Dioxus 29 | #[web_component(tag = "plop-link", style = InjectedStyle::css(include_str!("link.css")))] 30 | pub fn Link( 31 | /// The link href 32 | href: String, 33 | ) -> Element { 34 | rsx! { 35 | a { target: "_blank", href: "{href}", 36 | // See 37 | slot {} 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/dx-in-dx/src/link.css: -------------------------------------------------------------------------------- 1 | :host { 2 | margin-top: 20px; 3 | margin: 10px; 4 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 5 | } 6 | 7 | a { 8 | color: #1f1f1f; 9 | background-color: #fff; 10 | text-decoration: none; 11 | border: thin dotted currentColor ; 12 | border-radius: 5px; 13 | padding: 10px; 14 | transition: all .2s; 15 | } 16 | 17 | a:hover { 18 | color: #fff; 19 | background-color: #1f1f1f; 20 | cursor: pointer; 21 | border-style: solid; 22 | } 23 | -------------------------------------------------------------------------------- /examples/dx-in-dx/src/main.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![allow(clippy::multiple_crate_versions)] 3 | 4 | use dioxus::logger::tracing::{info, Level}; 5 | use dioxus::{logger, prelude::*}; 6 | 7 | use dx_in_dx::{register_link, App}; 8 | 9 | #[allow(clippy::expect_used)] 10 | fn main() { 11 | // Init logger 12 | let _ = logger::init(Level::INFO); 13 | 14 | info!("Register 'plop-link' web component"); 15 | register_link(); 16 | 17 | info!("starting the app"); 18 | launch(App); 19 | } 20 | -------------------------------------------------------------------------------- /examples/greeting/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "greeting" 3 | version = "0.1.0" 4 | edition = { workspace = true } 5 | authors = { workspace = true } 6 | license = { workspace = true } 7 | repository = { workspace = true } 8 | 9 | description = "Greeting web component example" 10 | keywords = ["dioxus", "web-component", "wasm"] 11 | categories = ["wasm", "web-programming"] 12 | readme = "README.md" 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | 17 | [dependencies] 18 | dioxus = { workspace = true, features = ["web"] } 19 | dioxus-web-component = { path = "../../dioxus-web-component" } 20 | wasm-bindgen = { workspace = true } 21 | 22 | [lints] 23 | workspace = true 24 | -------------------------------------------------------------------------------- /examples/greeting/LICENSE-APACHE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /examples/greeting/LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/greeting/README.md: -------------------------------------------------------------------------------- 1 | # Greeting example 2 | 3 | This example provide a `plop-greeting` web component with a `name` attribute. 4 | 5 | The `name` is also a properties, so you can use Javascript to get/set the name. 6 | 7 | To build this sample, use [wasm-pack] 8 | 9 | ```shell 10 | wasm-pack build --release --target web 11 | ``` 12 | 13 | See [index.html](index.html) and [index.js](index.js) to see how to use it. 14 | 15 | [wasm-pack]: https://github.com/rustwasm/wasm-pack 16 | -------------------------------------------------------------------------------- /examples/greeting/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilaborie/dioxus-web-component/ee2377a6a7ea2fce55a45662f8075456ee898392/examples/greeting/favicon.ico -------------------------------------------------------------------------------- /examples/greeting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Dioxus web component - Greetings 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/greeting/index.js: -------------------------------------------------------------------------------- 1 | import start from "./pkg/greeting.js"; 2 | 3 | // Register the web component 4 | await start(); 5 | 6 | // Register the change name button 7 | const btn = document.querySelector("button"); 8 | btn.onclick = () => { 9 | const name = prompt(`What's your name?`); 10 | if (name) { 11 | for (const el of document.querySelectorAll("plop-greeting")) { 12 | // Set with attribute 13 | el.setAttribute("name", name); 14 | // Or directly with property 15 | el.name = name; 16 | } 17 | } 18 | }; 19 | 20 | // Register the change color button 21 | const colorInput = document.querySelector("input[type=color]"); 22 | colorInput.oninput = () => { 23 | const color = colorInput.value; 24 | for (const el of document.querySelectorAll("plop-greeting")) { 25 | el.style.setProperty("--my-color", color); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /examples/greeting/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![allow(clippy::multiple_crate_versions)] 3 | 4 | use dioxus::logger::tracing::Level; 5 | use dioxus::{logger, prelude::*}; 6 | use dioxus_web_component::{web_component, InjectedStyle}; 7 | use wasm_bindgen::prelude::*; 8 | 9 | /// Install (register) the web component 10 | #[wasm_bindgen(start)] 11 | pub fn register() { 12 | let _ = logger::init(Level::INFO); 13 | register_greetings(); 14 | } 15 | 16 | #[web_component(tag = "plop-greeting", style = InjectedStyle::css(include_str!("./style.css")) )] 17 | fn Greetings( 18 | // The name can be set as an attribute of the plop-greeting HTML element 19 | #[attribute] 20 | #[property] 21 | name: String, 22 | ) -> Element { 23 | rsx! { 24 | p { "Hello {name}!" } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/greeting/src/style.css: -------------------------------------------------------------------------------- 1 | :host { 2 | outline: medium dotted red; 3 | --my-color: black; 4 | } 5 | 6 | p { 7 | color: var(--my-color); 8 | } 9 | -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilaborie/dioxus-web-component/ee2377a6a7ea2fce55a45662f8075456ee898392/favicon.png -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | 2 | # List all just receipes 3 | default: 4 | @just --list --unsorted 5 | 6 | # Install requirement for recipes 7 | requirement: 8 | cargo binstall dioxus-cli bacon cargo-nextest cargo-sort wasm-pack basic-http-server 9 | 10 | # Format the code and sort dependencies 11 | format: 12 | cargo fmt 13 | dx fmt 14 | cargo sort --workspace --grouped 15 | 16 | _check_format: 17 | cargo fmt --all -- --check 18 | dx fmt --check 19 | cargo sort --workspace --grouped --check 20 | 21 | # Lint the rust code 22 | lint: 23 | cargo clippy --workspace --all-features --all-targets 24 | 25 | # Launch tests 26 | test: 27 | cargo nextest run 28 | cargo test --doc 29 | 30 | # Check the code (formatting, lint, and tests) 31 | check: && _check_format lint test 32 | 33 | # Run TDD mode 34 | tdd: 35 | bacon 36 | 37 | # Build documentation 38 | doc: 39 | cargo doc --all-features --no-deps 40 | 41 | _example name: 42 | cd examples/{{name}} && wasm-pack build --release --target web 43 | basic-http-server examples/{{name}} 44 | 45 | # Run Greeting example 46 | example-greeting: (_example "greeting") 47 | 48 | # Run Counter example 49 | example-counter: (_example "counter") 50 | 51 | # Run Dioxus (web component) in Dioxus example 52 | example-dx-in-dx: 53 | cd examples/dx-in-dx && dx serve 54 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "greeting" 3 | release = false 4 | 5 | [[package]] 6 | name = "counter" 7 | release = false 8 | 9 | [[package]] 10 | name = "dx-in-dx" 11 | release = false 12 | --------------------------------------------------------------------------------