├── .gitattributes ├── .github ├── FUNDING.yml ├── actions │ └── rust_toolchain │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── cd.yml │ ├── ci.yml │ └── tests.yml ├── .gitignore ├── .idea ├── .gitignore ├── dataSources.xml ├── leptos-struct-table.iml ├── modules.xml └── vcs.xml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── bootstrap │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ ├── src │ │ └── main.rs │ └── style.css ├── custom_renderers_svg │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── main.rs │ │ └── renderers.rs ├── custom_row_renderer │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── main.rs ├── custom_type │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── main.rs ├── editable │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ ├── input.css │ ├── src │ │ ├── main.rs │ │ ├── renderer.rs │ │ └── tailwind.rs │ ├── style │ │ └── output.css │ └── tailwind.config.js ├── generic │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── main.rs ├── getter │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── main.rs ├── i18n │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ ├── locales │ │ ├── de.json │ │ └── en.json │ └── src │ │ └── main.rs ├── paginated_rest_datasource │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ ├── src │ │ ├── data_provider.rs │ │ ├── main.rs │ │ ├── models.rs │ │ └── renderer.rs │ └── style.css ├── pagination │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ ├── input.css │ ├── src │ │ ├── data_provider.rs │ │ ├── main.rs │ │ ├── models.rs │ │ └── tailwind.rs │ ├── style │ │ └── output.css │ └── tailwind.config.js ├── selectable │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ ├── input.css │ ├── src │ │ ├── main.rs │ │ └── tailwind.rs │ ├── style │ │ └── output.css │ └── tailwind.config.js ├── serverfn_sqlx │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── db.sqlite3 │ ├── input.css │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── app.rs │ │ ├── classes.rs │ │ ├── data_provider.rs │ │ ├── database.rs │ │ ├── handlers.rs │ │ ├── lib.rs │ │ └── main.rs │ ├── style │ │ └── output.css │ └── tailwind.config.js ├── simple │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── main.rs └── tailwind │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ ├── input.css │ ├── src │ ├── main.rs │ └── tailwind.rs │ ├── style │ └── output.css │ └── tailwind.config.js ├── hero.afdesign ├── hero.webp └── src ├── cell_value.rs ├── chrono.rs ├── class_providers ├── bootstrap.rs ├── mod.rs └── tailwind.rs ├── components ├── cell.rs ├── mod.rs ├── renderer_fn.rs ├── row.rs ├── table_content.rs ├── tbody.rs └── thead.rs ├── data_provider.rs ├── display_strategy.rs ├── events.rs ├── lib.rs ├── loaded_rows.rs ├── reload_controller.rs ├── row_reader.rs ├── rust_decimal.rs ├── selection.rs ├── sorting.rs ├── table_row.rs ├── time.rs └── uuid.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sqlite3 filter=lfs diff=lfs merge=lfs -text 2 | *.sqlite3-wal filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Synphonyte]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/actions/rust_toolchain/action.yml: -------------------------------------------------------------------------------- 1 | name: Rust Toolchain 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - uses: actions-rs/toolchain@v1 7 | with: 8 | toolchain: stable 9 | profile: minimal 10 | override: true 11 | components: rustfmt 12 | 13 | - name: Cache 14 | uses: Swatinem/rust-cache@v2 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: weekly -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | # Pattern matched against refs/tags 6 | tags: 7 | - '*' # Push events to every tag not containing / 8 | workflow_dispatch: 9 | 10 | jobs: 11 | cargo_checks: 12 | name: Cargo Checks 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: ./.github/actions/rust_toolchain/ 17 | - name: Check formatting 18 | run: cargo fmt --check 19 | - name: Clippy 20 | run: cargo clippy --tests -- -D warnings 21 | - name: Check if the README is up to date. 22 | run: | 23 | cargo install cargo-rdme 24 | cargo rdme --check 25 | - name: Run tests 26 | run: cargo test --features chrono,uuid,rust_decimal,time 27 | 28 | test_examples: 29 | name: Test Examples 30 | runs-on: ubuntu-latest 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | example: 35 | - bootstrap 36 | - custom_renderers_svg 37 | - custom_row_renderer 38 | - editable 39 | - generic 40 | - getter 41 | - i18n 42 | - paginated_rest_datasource 43 | - pagination 44 | - selectable 45 | - serverfn_sqlx 46 | - simple 47 | - tailwind 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: ./.github/actions/rust_toolchain/ 51 | - name: Build example ${{ matrix.example }} 52 | run: | 53 | cd ${{ github.workspace }}/examples/${{ matrix.example }}/ 54 | cargo build 55 | shell: bash 56 | 57 | publish: 58 | name: Publish 59 | runs-on: ubuntu-latest 60 | needs: [ cargo_checks, test_examples ] 61 | steps: 62 | - uses: actions/checkout@v4 63 | - uses: ./.github/actions/rust_toolchain/ 64 | - name: Publish crate leptos-struct-table 65 | uses: katyo/publish-crates@v2 66 | with: 67 | registry-token: ${{ secrets.CRATES_TOKEN }} 68 | 69 | - uses: CSchoel/release-notes-from-changelog@v1 70 | - name: Create Release using GitHub CLI 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | run: > 74 | gh release create 75 | -d 76 | -F RELEASE.md 77 | -t "Version $RELEASE_VERSION" 78 | ${GITHUB_REF#refs/*/} 79 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths: 8 | - "**" 9 | - "!/*.md" 10 | - "!/**.md" 11 | 12 | concurrency: 13 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | integrity: 18 | name: Integrity Checks on Rust ${{ matrix.toolchain }} 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 15 21 | strategy: 22 | matrix: 23 | toolchain: 24 | - stable 25 | - nightly 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Setup Rust 32 | uses: dtolnay/rust-toolchain@master 33 | with: 34 | toolchain: ${{ matrix.toolchain }} 35 | targets: wasm32-unknown-unknown 36 | components: clippy, rustfmt 37 | 38 | - name: Setup Rust Cache 39 | uses: Swatinem/rust-cache@v2 40 | 41 | - name: Build 42 | run: cargo build 43 | 44 | - name: Format 45 | run: cargo fmt --check 46 | 47 | - name: Clippy 48 | run: cargo clippy -- -D warnings 49 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - main 5 | paths: 6 | - "**" 7 | - "!/*.md" 8 | - "!/**.md" 9 | workflow_dispatch: 10 | 11 | name: Tests 12 | 13 | permissions: write-all 14 | 15 | jobs: 16 | tests: 17 | name: Tests 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: nightly 24 | profile: minimal 25 | override: true 26 | components: rustfmt, clippy, rust-src 27 | - name: Cache 28 | uses: Swatinem/rust-cache@v2 29 | 30 | - name: Run tests (general) 31 | run: cargo test --features chrono,uuid,rust_decimal,time 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | dist 3 | Cargo.lock 4 | node_modules 5 | package*.json 6 | .DS_Store 7 | *.sqlite3-wal -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sqlite.xerial 6 | true 7 | org.sqlite.JDBC 8 | jdbc:sqlite:$PROJECT_DIR$/examples/serverfn_sqlx/db.sqlite3 9 | $ProjectFileDir$ 10 | 11 | 12 | file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.40.1/org/xerial/sqlite-jdbc/3.40.1.0/sqlite-jdbc-3.40.1.0.jar 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/leptos-struct-table.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.14.0-beta3] - 2025-03-17 4 | 5 | ### Fix 🐛 6 | 7 | - Fixed keeping track of the query params so the frontend doesn't crash (thanks to @holg). 8 | 9 | ### Changes 🔥 10 | 11 | - Made compatibility table much more prominent (thanks to @cramhead). 12 | - Adapted to allow --cfg erase_components usage (thanks to @Zagitta). 13 | 14 | ### Special thanks to our sponsor 15 | - @spencewenski 16 | 17 | 18 | ## [0.14.0-beta2] - 2025-01-05 19 | 20 | ### Breaking Changes 🛠️ 21 | 22 | - row and cell renderers now take a `RwSignal` which also makes it possible to edit the data much more easily. 23 | 24 | ### Fix 🐛 25 | 26 | - Fixed broken API in examples `paginated_rest_datasource` and `pagination` 27 | 28 | ### Special thanks to our sponsor 29 | - @spencewenski 30 | 31 | ## [0.14.0-beta1] - 2025-01-03 32 | 33 | ### Breaking Changes 🛠️ 34 | 35 | - Updated dependencies leptos-use to version 0.15 and leptos 0.7 36 | - The prop `scroll_container` of `` is now required. If you don't care about scrolling you can use `scroll_container="html"`. 37 | - Some smaller stuff that the compiler should tell you about. Please also refer to the updated examples. 38 | 39 | ### Known Issues 40 | 41 | - The virtualized scrolling is not perfectly smooth yet. 42 | - Editing doesn't work quite yet. 43 | - The examples `paginated_rest_datasource` and `pagination` are not working yet because the previously used external REST API changed. 44 | 45 | ### Thanks to contributor 46 | 47 | Thanks to @kstep for his many contributions to this release. 48 | 49 | ### Special thanks to our sponsor 50 | - @spencewenski 51 | 52 | ## [0.13.1] - 2024-10-31 53 | 54 | ### Fixes 🐛 55 | 56 | - Fixed edge case loading data ranges that are smaller than the chunk size (thanks to @mcbernie). 57 | 58 | ## [0.13.0] - 2024-09-05 59 | 60 | ### Breaking Change 🛠️ 61 | 62 | - Updated dependency leptos-use to version 0.13 which fixes some unsatisfied trait bounds. 63 | 64 | ## [0.12.1] - 2024-09-01 65 | 66 | ### Features 🚀 67 | 68 | - Added support for generics in struct field types (thanks to @frnsys) 69 | - Added macro options for much more flexibility with leptos-i18n (thanks to @Baptistemontan) 70 | - Added hint to readmes of examples how to run them (thanks to @luckynumberke7in) 71 | 72 | ## [0.12.0] - 2024-08-14 73 | 74 | ### Breaking Change 🛠️ 75 | 76 | - Updated dependency leptos-use to version 0.12 which supports web-sys 0.3.70 which introduced breaking changes. (thanks 77 | to @frnsys) 78 | 79 | ## [0.11.0] - 2024-08-05 80 | 81 | ### Features 🚀 82 | 83 | - Changed leptos-use to version 0.11 84 | - Added i18n support via the `"i18n"` feature which uses `leptos-i18n`. See the `i18n` example for usage. 85 | - Added row reader to `TableComponent` 86 | - Added `default_th_sorting_style` to make it easier to write a custom thead cell render component. 87 | 88 | ## [0.10.2] - 2024-06-07 89 | 90 | ### Fixes 🐛 91 | 92 | - Fixed race condition with loading row count and sorting update. 93 | - Fixed console errors/warnings for signals accessed in async blocks after component was disposed of. 94 | 95 | ## [0.10.1] - 2024-06-05 96 | 97 | ### Change 🔥 98 | 99 | - `CellValue` is now implemented for `leptos::View`. This makes `FieldGetter...` to produce valid HTML. This fixes SSR rendering issues. 154 | 155 | ### Other Changes 156 | 157 | - Added an example for how to use server functions and sqlx together with this crate. 158 | 159 | ## [0.8.3] - 2024-02-20 160 | 161 | ### Fix 🐛 162 | 163 | - When not limiting a scroll container this could lead to a runaway row loading. This is now limited to max 500 rows. 164 | 165 | ## [0.8.2] - 2024-02-18 166 | 167 | ### Feature 🚀 168 | 169 | - Added method `TableDataProvider::track` to easily specify reactive dependencies of data loading 170 | 171 | ## [0.8.1] - 2024-02-17 172 | 173 | ### Fix 🐛 174 | 175 | - Removed debug log 176 | 177 | ## [0.8.0] - 2024-02-17 178 | 179 | ### Feature 🚀 180 | 181 | - Added `loading_row_display_limit` prop to `TableContent` to make it possible to load smaller row counts nicely 182 | 183 | ### Breaking Changes 🛠️ 184 | 185 | - Added `row_index` and `col_index` to `TableClassesProvider::loading_cell` 186 | - Added `col_index` to `TableClassesProvider::loading_cell_inner` 187 | - Changed the type of prop `loading_row_renderer` of the component `TableContent` 188 | 189 | ### Fix 🐛 190 | 191 | - Data loading for small data sets 192 | 193 | ## [0.7.1] - 2024-02-14 194 | 195 | ### Changes 196 | 197 | - Added generic error type to `TableDataProvider` 198 | - Fixed sorting for tables with skipped fields 199 | 200 | ## [0.7.0] - 2024-02-08 201 | 202 | ### Features 🚀 203 | 204 | - Virtualization — Only elements that are visible are rendered (with some extra for smooth scrolling). 205 | - Other display acceleration strategies like infinite scroll and pagination are implemented as well. 206 | - Caching — Only rows that are visible are requested from the data source and then cached. 207 | - Error handling — If an error occurs while loading data, it is displayed in a table row instead of the failed data. 208 | - Easy reloading — The data can be reloaded through the `ReloadController`. 209 | 210 | ### Breaking Changes 🛠️ 211 | 212 | Everything? - sorry. This release is like half a rewrite with much less macro magic. 213 | Please check the docs and examples. 214 | 215 | ## [0.6.0] - 2023-11-02 216 | 217 | ### New Feature 🎉 218 | 219 | - Support for generic structs 220 | 221 | ### Fix 🐛 222 | 223 | - Fixed `#[table(skip_sort)]` on fields 224 | 225 | ## [0.5.0] - 2023-10-20 226 | 227 | ### Breaking Changes 🛠️ 228 | 229 | - Added `on_change` events to support editable data (see new editable example) 230 | 231 | ### Fixes 🐛 232 | 233 | - Fixed selection with `key`s that are not `Copy` 234 | 235 | ### Other Changes 236 | 237 | - Modified REST example to include sorting 238 | 239 | ## [0.4.0] - 2023-10-02 240 | 241 | - Updated to leptos 0.5 242 | 243 | ## [0.3.0] 244 | 245 | - Updated to leptos 0.4 246 | 247 | ## [0.2.0] 248 | 249 | - Updated to leptos 0.3 250 | - Deactivated `default-features` of leptos 251 | - New class provider `BootstrapClassesPreset` 252 | - New example `bootstrap` 253 | - Added `thead` and `tbody` with customizable renderers 254 | - Added `getter` and `FieldGetter` with new example -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "leptos-struct-table" 3 | version = "0.14.0-beta3" 4 | edition = "2021" 5 | authors = ["Marc-Stefan Cassola"] 6 | categories = ["gui", "web-programming", "wasm"] 7 | description = "Generate a complete batteries included leptos data table component from a struct definition." 8 | exclude = ["examples/", "tests/"] 9 | keywords = ["leptos", "table", "data-sheet", "data-grid"] 10 | license = "MIT OR Apache-2.0" 11 | readme = "README.md" 12 | repository = "https://github.com/Synphonyte/leptos-struct-table" 13 | 14 | [dependencies] 15 | leptos = { version = "0.7.0" } 16 | leptos-struct-table-macro = { version = "0.13.0-beta2" } 17 | leptos-use = { version = "0.15.2" } 18 | rust_decimal = { version = "1.35", optional = true } 19 | chrono = { version = "0.4", optional = true } 20 | send_wrapper = "0.6" 21 | serde = "1" 22 | time = { version = "0.3", optional = true, features = ["formatting"] } 23 | uuid = { version = "1", optional = true, features = [] } 24 | thiserror = "1" 25 | web-sys = "0.3.67" 26 | wasm-bindgen = "0.2" 27 | 28 | [features] 29 | chrono = ["dep:chrono"] 30 | uuid = ["dep:uuid"] 31 | rust_decimal = ["dep:rust_decimal"] 32 | time = ["dep:time"] 33 | i18n = ["leptos-struct-table-macro/i18n"] 34 | 35 | [package.metadata."docs.rs"] 36 | all-features = true 37 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 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: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Synphonyte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/bootstrap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bootstrap" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"]} 8 | leptos-struct-table = { path = "../..", features = ["chrono"] } 9 | chrono = { version = "0.4" } 10 | console_error_panic_hook = "0.1" 11 | console_log = "1" 12 | log = "0.4" 13 | 14 | [dev-dependencies] 15 | wasm-bindgen = "0.2" 16 | wasm-bindgen-test = "0.3.0" 17 | web-sys = "0.3" -------------------------------------------------------------------------------- /examples/bootstrap/README.md: -------------------------------------------------------------------------------- 1 | ### A simple table example with just local data stored as `Vec` Uses the Bootstrap class provider. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) 6 | as well as the wasm32-unknown-unknown target: 7 | 8 | ```bash 9 | cargo install trunk 10 | rustup target add wasm32-unknown-unknown 11 | ``` 12 | 13 | Then, to run this example, execute in a terminal: 14 | 15 | ```bash 16 | trunk serve --open 17 | ``` -------------------------------------------------------------------------------- /examples/bootstrap/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/bootstrap/src/main.rs: -------------------------------------------------------------------------------- 1 | use ::chrono::NaiveDate; 2 | use leptos::prelude::*; 3 | use leptos_struct_table::*; 4 | 5 | // This generates the component BookTable 6 | #[derive(TableRow, Clone)] 7 | #[table( 8 | sortable, 9 | classes_provider = "BootstrapClassesPreset", 10 | impl_vec_data_provider 11 | )] 12 | pub struct Book { 13 | pub id: u32, 14 | pub title: String, 15 | pub author: String, 16 | pub publish_date: NaiveDate, 17 | } 18 | 19 | fn main() { 20 | _ = console_log::init_with_level(log::Level::Debug); 21 | console_error_panic_hook::set_once(); 22 | 23 | mount_to_body(|| { 24 | let rows = vec![ 25 | Book { 26 | id: 1, 27 | title: "The Great Gatsby".to_string(), 28 | author: "F. Scott Fitzgerald".to_string(), 29 | publish_date: NaiveDate::from_ymd_opt(1925, 4, 10).unwrap(), 30 | }, 31 | Book { 32 | id: 2, 33 | title: "The Grapes of Wrath".to_string(), 34 | author: "John Steinbeck".to_string(), 35 | publish_date: NaiveDate::from_ymd_opt(1939, 4, 14).unwrap(), 36 | }, 37 | Book { 38 | id: 3, 39 | title: "Nineteen Eighty-Four".to_string(), 40 | author: "George Orwell".to_string(), 41 | publish_date: NaiveDate::from_ymd_opt(1949, 6, 8).unwrap(), 42 | }, 43 | Book { 44 | id: 4, 45 | title: "Ulysses".to_string(), 46 | author: "James Joyce".to_string(), 47 | publish_date: NaiveDate::from_ymd_opt(1922, 2, 2).unwrap(), 48 | }, 49 | ]; 50 | 51 | view! { 52 |
53 | 54 | 55 |
56 |
57 | } 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /examples/bootstrap/style.css: -------------------------------------------------------------------------------- 1 | table.table th { 2 | user-select: none; 3 | } 4 | 5 | table.table th > span { 6 | display: flex; 7 | align-items: center; 8 | } 9 | 10 | table.table th.sort-asc > span::after, table.table th.sort-desc > span::after { 11 | content: var(--sort-icon); 12 | padding-left: 0.25rem; 13 | opacity: 0.4; 14 | } 15 | 16 | table.table th.sort-asc > span::before, table.table th.sort-desc > span::before { 17 | content: var(--sort-priority); 18 | opacity: 0.4; 19 | font-weight: 300; 20 | padding-left: 0.125rem; 21 | order: 9999; 22 | } -------------------------------------------------------------------------------- /examples/custom_renderers_svg/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "custom_renderers_svg" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"]} 8 | leptos-struct-table = { path = "../.." } 9 | console_error_panic_hook = "0.1" 10 | console_log = "1" 11 | log = "0.4" 12 | 13 | [dev-dependencies] 14 | wasm-bindgen = "0.2" 15 | wasm-bindgen-test = "0.3.0" 16 | web-sys = "0.3" -------------------------------------------------------------------------------- /examples/custom_renderers_svg/README.md: -------------------------------------------------------------------------------- 1 | ### Example that shows how use custom renderers to render the table as SVG. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) 6 | as well as the wasm32-unknown-unknown target: 7 | 8 | ```bash 9 | cargo install trunk 10 | rustup target add wasm32-unknown-unknown 11 | ``` 12 | 13 | Then, to run this example, execute in a terminal: 14 | 15 | ```bash 16 | trunk serve --open 17 | ``` -------------------------------------------------------------------------------- /examples/custom_renderers_svg/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/custom_renderers_svg/src/main.rs: -------------------------------------------------------------------------------- 1 | mod renderers; 2 | 3 | use renderers::*; 4 | 5 | use leptos::prelude::*; 6 | use leptos_struct_table::*; 7 | 8 | // This generates the component BookTable 9 | #[derive(TableRow, Clone)] 10 | #[table(thead_cell_renderer = "SvgHeadCellRenderer", impl_vec_data_provider)] 11 | pub struct Form { 12 | #[table(renderer = "SvgTextCellRenderer")] 13 | pub name: String, 14 | #[table(renderer = "SvgPathCellRenderer")] 15 | pub path: String, 16 | } 17 | 18 | fn main() { 19 | _ = console_log::init_with_level(log::Level::Debug); 20 | console_error_panic_hook::set_once(); 21 | 22 | mount_to_body(|| { 23 | let rows = vec![ 24 | Form { 25 | name: "Heart".to_string(), 26 | path: "M12.82 5.58l-.82.822l-.824-.824a5.375 5.375 0 1 0-7.601 7.602l7.895 7.895a.75.75 0 0 0 1.06 0l7.902-7.897a5.376 5.376 0 0 0-.001-7.599a5.38 5.38 0 0 0-7.611 0zm6.548 6.54L12 19.485L4.635 12.12a3.875 3.875 0 1 1 5.48-5.48l1.358 1.357a.75.75 0 0 0 1.073-.012L13.88 6.64a3.88 3.88 0 0 1 5.487 5.48z".to_string(), 27 | }, 28 | Form { 29 | name: "Bell".to_string(), 30 | path: "M12 1.996a7.49 7.49 0 0 1 7.496 7.25l.004.25v4.097l1.38 3.156a1.249 1.249 0 0 1-1.145 1.75L15 18.502a3 3 0 0 1-5.995.177L9 18.499H4.275a1.251 1.251 0 0 1-1.147-1.747L4.5 13.594V9.496c0-4.155 3.352-7.5 7.5-7.5zM13.5 18.5l-3 .002a1.5 1.5 0 0 0 2.993.145l.007-.147zM12 3.496c-3.32 0-6 2.674-6 6v4.41L4.656 17h14.697L18 13.907V9.509l-.003-.225A5.988 5.988 0 0 0 12 3.496z".to_string(), 31 | }, 32 | Form { 33 | name: "Star".to_string(), 34 | path: "M10.788 3.102c.495-1.003 1.926-1.003 2.421 0l2.358 4.778l5.273.766c1.107.16 1.549 1.522.748 2.303l-3.816 3.719l.901 5.25c.19 1.104-.968 1.945-1.959 1.424l-4.716-2.48l-4.715 2.48c-.99.52-2.148-.32-1.96-1.423l.901-5.251l-3.815-3.72c-.801-.78-.359-2.141.748-2.302L8.43 7.88l2.358-4.778zm1.21.937L9.74 8.614a1.35 1.35 0 0 1-1.016.739l-5.05.734l3.654 3.562c.318.31.463.757.388 1.195l-.862 5.029l4.516-2.375a1.35 1.35 0 0 1 1.257 0l4.516 2.375l-.862-5.03a1.35 1.35 0 0 1 .388-1.194l3.654-3.562l-5.05-.734a1.35 1.35 0 0 1-1.016-.739L11.998 4.04z".to_string(), 35 | }, 36 | ]; 37 | 38 | view! { 39 | 40 | 50 | 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /examples/custom_renderers_svg/src/renderers.rs: -------------------------------------------------------------------------------- 1 | use crate::Form; 2 | use leptos::prelude::*; 3 | use leptos::web_sys; 4 | use leptos_struct_table::*; 5 | 6 | const ROW_HEIGHT: usize = 30; 7 | const ROW_HEIGHT_HALF: usize = ROW_HEIGHT / 2; 8 | 9 | wrapper_render_fn!( 10 | /// g 11 | GRenderer, 12 | g, 13 | ); 14 | 15 | #[allow(non_snake_case)] 16 | pub fn SvgTbodyRenderer( 17 | content: impl IntoView, 18 | class: Signal, 19 | body_ref: BodyRef, 20 | ) -> impl IntoView { 21 | view! { {content} } 22 | } 23 | 24 | #[allow(unused_variables, non_snake_case)] 25 | pub fn SvgRowRenderer( 26 | class: Signal, 27 | row: RwSignal
, 28 | index: usize, 29 | selected: Signal, 30 | on_select: EventHandler, 31 | ) -> impl IntoView { 32 | let transform = y_transform_from_index(index); 33 | 34 | view! { 35 | 40 | 49 | {TableRow::render_row(row, index)} 50 | 51 | } 52 | } 53 | 54 | fn y_transform_from_index(index: usize) -> String { 55 | format!("translate(0, {})", (index + 1) * ROW_HEIGHT) 56 | } 57 | 58 | #[allow(non_snake_case)] 59 | pub fn SvgErrorRowRenderer(err: String, index: usize, _col_count: usize) -> impl IntoView { 60 | let transform = y_transform_from_index(index); 61 | 62 | view! { 63 | 64 | 65 | {err} 66 | 67 | 68 | } 69 | } 70 | 71 | #[allow(non_snake_case, unstable_name_collisions)] 72 | pub fn SvgLoadingRowRenderer( 73 | class: Signal, 74 | _get_cell_class: Callback<(usize,), String>, 75 | get_inner_cell_class: Callback<(usize,), String>, 76 | index: usize, 77 | _col_count: usize, 78 | ) -> impl IntoView { 79 | let transform = y_transform_from_index(index); 80 | 81 | view! { 82 | 83 | 84 | Loading... 85 | 86 | 87 | } 88 | } 89 | 90 | #[component] 91 | pub fn SvgHeadCellRenderer( 92 | /// The class attribute for the head element. Generated by the classes provider. 93 | #[prop(into)] 94 | class: Signal, 95 | /// The class attribute for the inner element. Generated by the classes provider. 96 | #[prop(into)] 97 | inner_class: String, 98 | /// The index of the column. Starts at 0 for the first column. The order of the columns is the same as the order of the fields in the struct. 99 | index: usize, 100 | /// The sort priority of the column. `None` if the column is not sorted. `0` means the column is the primary sort column. 101 | #[prop(into)] 102 | sort_priority: Signal>, 103 | /// The sort direction of the column. See [`ColumnSort`]. 104 | #[prop(into)] 105 | sort_direction: Signal, 106 | /// The event handler for the click event. Has to be called with [`TableHeadEvent`]. 107 | on_click: F, 108 | children: Children, 109 | ) -> impl IntoView 110 | where 111 | F: Fn(TableHeadEvent) + 'static, 112 | { 113 | let style = default_th_sorting_style(sort_priority, sort_direction); 114 | 115 | let transform = transform_from_index(index, 0); 116 | 117 | view! { 118 | 128 | 129 | {children()} 130 | 131 | 132 | } 133 | } 134 | 135 | #[component] 136 | #[allow(unused_variables)] 137 | pub fn SvgTextCellRenderer( 138 | class: String, 139 | value: Signal, 140 | row: RwSignal, 141 | index: usize, 142 | ) -> impl IntoView 143 | where 144 | T: IntoView + Clone + Send + Sync + 'static, 145 | { 146 | let x = x_from_index(index); 147 | 148 | view! { 149 | 150 | {value} 151 | 152 | } 153 | } 154 | 155 | #[component] 156 | #[allow(unused_variables)] 157 | pub fn SvgPathCellRenderer( 158 | #[prop(into)] class: String, 159 | value: Signal, 160 | row: RwSignal, 161 | index: usize, 162 | ) -> impl IntoView { 163 | let transform = transform_from_index(index, 3); 164 | 165 | view! { } 166 | } 167 | 168 | fn transform_from_index(index: usize, y: usize) -> String { 169 | format!("translate({}, {y})", x_from_index(index)) 170 | } 171 | 172 | fn x_from_index(index: usize) -> usize { 173 | 5 + index * 100 174 | } 175 | -------------------------------------------------------------------------------- /examples/custom_row_renderer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "custom_row_renderer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"] } 8 | leptos-struct-table = { path = "../..", features = ["chrono", "uuid"] } 9 | chrono = { version = "0.4" } 10 | uuid = { version = "1.8", features= ["v4"]} 11 | console_error_panic_hook = "0.1" 12 | console_log = "1" 13 | log = "0.4" 14 | getrandom = { version = "0.2", features = ["js"] } 15 | 16 | [dev-dependencies] 17 | wasm-bindgen = "0.2" 18 | wasm-bindgen-test = "0.3.0" 19 | web-sys = "0.3" 20 | -------------------------------------------------------------------------------- /examples/custom_row_renderer/README.md: -------------------------------------------------------------------------------- 1 | ### A simple table example with just local data stored as `Vec` demonstrating a custom row renderer and skipping header titles. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) 6 | as well as the wasm32-unknown-unknown target: 7 | 8 | ```bash 9 | cargo install trunk 10 | rustup target add wasm32-unknown-unknown 11 | ``` 12 | 13 | Then, to run this example, execute in a terminal: 14 | 15 | ```bash 16 | trunk serve --open 17 | ``` -------------------------------------------------------------------------------- /examples/custom_row_renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/custom_row_renderer/src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | //! Simple showcase example. 3 | 4 | use ::chrono::NaiveDate; 5 | use ::uuid::Uuid; 6 | use leptos::prelude::*; 7 | use leptos::web_sys; 8 | use leptos_struct_table::*; 9 | 10 | /// Custom row renderer that adds a link to the end of the row 11 | #[allow(unused_variables, non_snake_case)] 12 | pub fn CustomTableRowRenderer( 13 | // The class attribute for the row element. Generated by the classes provider. 14 | class: Signal, 15 | // The row to render. 16 | row: RwSignal, 17 | // The index of the row. Starts at 0 for the first body row. 18 | index: usize, 19 | // The selected state of the row. True, when the row is selected. 20 | selected: Signal, 21 | // Event handler callback when this row is selected 22 | on_select: EventHandler, 23 | ) -> impl IntoView { 24 | 25 | view! { 26 | 27 | {TableRow::render_row(row, index)} 28 | 29 | "Some link" 30 | 31 | 32 | } 33 | } 34 | 35 | /// This generates the component BookTable 36 | #[derive(TableRow, Clone)] 37 | #[table(sortable, impl_vec_data_provider)] 38 | pub struct Book { 39 | /// Id of the entry. 40 | pub id: Uuid, 41 | /// Title of the book. 42 | pub title: String, 43 | /// Author of the book. 44 | pub author: String, 45 | /// Date when book has been published. 46 | pub publish_date: Option, 47 | /// Description of the book. Optional. 48 | #[table(none_value = "-")] 49 | pub description: Option, 50 | /// Example on hidden member. 51 | #[table(skip)] 52 | pub hidden_field: String, 53 | /// Example of a headerless column 54 | #[table(skip_header)] 55 | pub rating: String, 56 | } 57 | 58 | fn main() { 59 | _ = console_log::init_with_level(log::Level::Debug); 60 | console_error_panic_hook::set_once(); 61 | 62 | mount_to_body(|| { 63 | let rows = vec![ 64 | Book { 65 | id: Uuid::new_v4(), 66 | title: "The Great Gatsby".to_string(), 67 | author: "F. Scott Fitzgerald".to_string(), 68 | publish_date: Some(NaiveDate::from_ymd_opt(1925, 4, 10).unwrap()), 69 | description: Some( 70 | "A story of wealth, love, and the American Dream in the 1920s.".to_string(), 71 | ), 72 | hidden_field: "hidden".to_string(), 73 | rating: "5/5".to_string(), 74 | }, 75 | Book { 76 | id: Uuid::new_v4(), 77 | title: "The Grapes of Wrath".to_string(), 78 | author: "John Steinbeck".to_string(), 79 | publish_date: Some(NaiveDate::from_ymd_opt(1939, 4, 14).unwrap()), 80 | description: None, 81 | hidden_field: "not visible in the table".to_string(), 82 | rating: "4/5".to_string(), 83 | }, 84 | Book { 85 | id: Uuid::new_v4(), 86 | title: "Nineteen Eighty-Four".to_string(), 87 | author: "George Orwell".to_string(), 88 | publish_date: Some(NaiveDate::from_ymd_opt(1949, 6, 8).unwrap()), 89 | description: None, 90 | hidden_field: "hidden".to_string(), 91 | rating: "19/84".to_string(), 92 | }, 93 | Book { 94 | id: Uuid::new_v4(), 95 | title: "Ulysses".to_string(), 96 | author: "James Joyce".to_string(), 97 | publish_date: Some(NaiveDate::from_ymd_opt(1922, 2, 2).unwrap()), 98 | description: None, 99 | hidden_field: "hidden".to_string(), 100 | rating: "really long".to_string(), 101 | }, 102 | ]; 103 | 104 | view! { 105 | 106 | 111 |
112 | } 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /examples/custom_type/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "custom_type" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"] } 8 | leptos-struct-table = { path = "../..", features = ["chrono"] } 9 | chrono = { version = "0.4" } 10 | console_error_panic_hook = "0.1" 11 | console_log = "1" 12 | log = "0.4" 13 | derive_more = { version = "0.99" } 14 | 15 | [dev-dependencies] 16 | wasm-bindgen = "0.2" 17 | wasm-bindgen-test = "0.3.0" 18 | web-sys = "0.3" 19 | -------------------------------------------------------------------------------- /examples/custom_type/README.md: -------------------------------------------------------------------------------- 1 | ### A table example with custom local data stored as `Vec>`. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) 6 | as well as the wasm32-unknown-unknown target: 7 | 8 | ```bash 9 | cargo install trunk 10 | rustup target add wasm32-unknown-unknown 11 | ``` 12 | 13 | Then, to run this example, execute in a terminal: 14 | 15 | ```bash 16 | trunk serve --open 17 | ``` -------------------------------------------------------------------------------- /examples/custom_type/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/custom_type/src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | //! Simple showcase example. 3 | 4 | use ::chrono::NaiveDate; 5 | use derive_more::{Deref, DerefMut}; 6 | use leptos::prelude::*; 7 | use leptos_struct_table::*; 8 | use std::sync::Arc; 9 | 10 | /// This generates the component BookTable 11 | #[derive(TableRow)] 12 | #[table(sortable, impl_vec_data_provider, row_type = "ArcBook")] 13 | pub struct Book { 14 | /// Title of the book. 15 | pub title: String, 16 | /// Author of the book. 17 | pub author: String, 18 | /// Date when book has been published. 19 | pub publish_date: Option, 20 | /// Description of the book. Optional. 21 | #[table(none_value = "-")] 22 | pub description: Option, 23 | } 24 | 25 | /// New-type pattern because otherwise the impl TableRow doesn't work because of orphan rules. 26 | #[derive(Deref, DerefMut, Clone)] 27 | pub struct ArcBook(Arc); 28 | 29 | fn main() { 30 | _ = console_log::init_with_level(log::Level::Debug); 31 | console_error_panic_hook::set_once(); 32 | 33 | mount_to_body(|| { 34 | let rows = vec![ 35 | ArcBook(Arc::new(Book { 36 | title: "The Great Gatsby".to_string(), 37 | author: "F. Scott Fitzgerald".to_string(), 38 | publish_date: Some(NaiveDate::from_ymd_opt(1925, 4, 10).unwrap()), 39 | description: Some( 40 | "A story of wealth, love, and the American Dream in the 1920s.".to_string(), 41 | ), 42 | })), 43 | ArcBook(Arc::new(Book { 44 | title: "The Grapes of Wrath".to_string(), 45 | author: "John Steinbeck".to_string(), 46 | publish_date: Some(NaiveDate::from_ymd_opt(1939, 4, 14).unwrap()), 47 | description: None, 48 | })), 49 | ArcBook(Arc::new(Book { 50 | title: "Nineteen Eighty-Four".to_string(), 51 | author: "George Orwell".to_string(), 52 | publish_date: Some(NaiveDate::from_ymd_opt(1949, 6, 8).unwrap()), 53 | description: None, 54 | })), 55 | ArcBook(Arc::new(Book { 56 | title: "Ulysses".to_string(), 57 | author: "James Joyce".to_string(), 58 | publish_date: Some(NaiveDate::from_ymd_opt(1922, 2, 2).unwrap()), 59 | description: None, 60 | })), 61 | ]; 62 | 63 | view! { 64 | 65 | 66 |
67 | } 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /examples/editable/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "editable" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"]} 8 | leptos-struct-table = { path = "../..", features = ["chrono"] } 9 | chrono = { version = "0.4" } 10 | console_error_panic_hook = "0.1" 11 | console_log = "1" 12 | log = "0.4" 13 | 14 | [dev-dependencies] 15 | wasm-bindgen = "0.2" 16 | wasm-bindgen-test = "0.3.0" 17 | web-sys = "0.3" -------------------------------------------------------------------------------- /examples/editable/README.md: -------------------------------------------------------------------------------- 1 | ### A table example with editable cells and data that is stored as `Vec`. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | #### Uses the Tailwind class provider. 6 | 7 | The way Tailwind works, is to scan the classes in the code. Due to this it is 8 | recommended to copy the file `src/class_providers/tailwind.rs` into your project as done in this example. 9 | 10 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation) 11 | as well as the wasm32-unknown-unknown target: 12 | 13 | ```bash 14 | cargo install trunk 15 | npm install -D tailwindcss 16 | rustup target add wasm32-unknown-unknown 17 | ``` 18 | 19 | Then, open two terminals. In the first one, run: 20 | 21 | ``` 22 | npx tailwindcss -i ./input.css -o ./style/output.css --watch 23 | ``` 24 | 25 | In the second one, run: 26 | 27 | ```bash 28 | trunk serve --open 29 | ``` -------------------------------------------------------------------------------- /examples/editable/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/editable/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /examples/editable/src/main.rs: -------------------------------------------------------------------------------- 1 | mod renderer; 2 | mod tailwind; 3 | 4 | use crate::renderer::*; 5 | use ::chrono::NaiveDate; 6 | use leptos::prelude::*; 7 | use leptos_struct_table::*; 8 | use std::ops::Range; 9 | use tailwind::TailwindClassesPreset; 10 | 11 | // This generates the component BookTable 12 | #[derive(TableRow, Clone, Debug)] 13 | #[table(classes_provider = "TailwindClassesPreset")] 14 | pub struct Book { 15 | pub id: u32, 16 | #[table(renderer = "InputCellRenderer")] 17 | pub title: String, 18 | #[table(renderer = "InputCellRenderer")] 19 | pub author: String, 20 | pub publish_date: NaiveDate, 21 | } 22 | 23 | impl TableDataProvider for RwSignal> { 24 | async fn get_rows(&self, _: Range) -> Result<(Vec, Range), String> { 25 | let books = self.get_untracked().to_vec(); 26 | let len = books.len(); 27 | Ok((books, 0..len)) 28 | } 29 | 30 | async fn row_count(&self) -> Option { 31 | Some(self.get_untracked().len()) 32 | } 33 | } 34 | 35 | fn main() { 36 | _ = console_log::init_with_level(log::Level::Debug); 37 | console_error_panic_hook::set_once(); 38 | 39 | mount_to_body(|| { 40 | let rows = RwSignal::new(vec![ 41 | Book { 42 | id: 1, 43 | title: "The Great Gatsby".to_string(), 44 | author: "F. Scott Fitzgerald".to_string(), 45 | publish_date: NaiveDate::from_ymd_opt(1925, 4, 10).unwrap(), 46 | }, 47 | Book { 48 | id: 2, 49 | title: "The Grapes of Wrath".to_string(), 50 | author: "John Steinbeck".to_string(), 51 | publish_date: NaiveDate::from_ymd_opt(1939, 4, 14).unwrap(), 52 | }, 53 | Book { 54 | id: 3, 55 | title: "Nineteen Eighty-Four".to_string(), 56 | author: "George Orwell".to_string(), 57 | publish_date: NaiveDate::from_ymd_opt(1949, 6, 8).unwrap(), 58 | }, 59 | Book { 60 | id: 4, 61 | title: "Ulysses".to_string(), 62 | author: "James Joyce".to_string(), 63 | publish_date: NaiveDate::from_ymd_opt(1922, 2, 2).unwrap(), 64 | }, 65 | ]); 66 | 67 | let on_change = move |evt: ChangeEvent| { 68 | rows.write()[evt.row_index] = evt.changed_row.get_untracked(); 69 | }; 70 | 71 | view! { 72 |
74 | 75 | 76 |
77 |
78 | 79 |
{move || format!("{:#?}", rows.get())}
80 | } 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /examples/editable/src/renderer.rs: -------------------------------------------------------------------------------- 1 | use crate::Book; 2 | use leptos::prelude::*; 3 | 4 | /// A renderer that shows an tag and emits the `on_change` event when the is changed. 5 | #[component] 6 | #[allow(unused_variables)] 7 | pub fn InputCellRenderer( 8 | /// The class attribute for the cell element. Generated by the classes provider. 9 | class: String, 10 | /// The value to display. 11 | value: Signal, 12 | /// Event handler called when the cell is changed. 13 | row: RwSignal, 14 | /// The index of the column. Starts at 0. 15 | index: usize, 16 | ) -> impl IntoView { 17 | let on_change = move |evt| { 18 | let value = event_target_value(&evt); 19 | 20 | let mut row = row.write(); 21 | 22 | match index { 23 | 1 => row.title = value, 24 | 2 => row.author = value, 25 | _ => unreachable!(), 26 | } 27 | }; 28 | 29 | view! { 30 | 31 | 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/editable/src/tailwind.rs: -------------------------------------------------------------------------------- 1 | use crate::{ColumnSort, TableClassesProvider}; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct TailwindClassesPreset; 5 | 6 | impl TableClassesProvider for TailwindClassesPreset { 7 | fn new() -> Self { 8 | Self 9 | } 10 | 11 | fn thead_row(&self, template_classes: &str) -> String { 12 | format!( 13 | "{} {}", 14 | "text-xs text-gray-700 uppercase bg-gray-200 dark:bg-gray-700 dark:text-gray-300", 15 | template_classes 16 | ) 17 | } 18 | 19 | fn thead_cell(&self, sort: ColumnSort, template_classes: &str) -> String { 20 | let sort_class = match sort { 21 | ColumnSort::None => "", 22 | _ => "text-black dark:text-white", 23 | }; 24 | 25 | format!( 26 | "cursor-pointer px-5 py-2 {} {}", 27 | sort_class, template_classes 28 | ) 29 | } 30 | 31 | fn thead_cell_inner(&self) -> String { 32 | "flex items-center after:content-[--sort-icon] after:pl-1 after:opacity-40 before:content-[--sort-priority] before:order-last before:pl-0.5 before:font-light before:opacity-40".to_string() 33 | } 34 | 35 | fn row(&self, row_index: usize, selected: bool, template_classes: &str) -> String { 36 | let bg_color = if row_index % 2 == 0 { 37 | if selected { 38 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 39 | } else { 40 | "bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800" 41 | } 42 | } else if selected { 43 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 44 | } else { 45 | "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700" 46 | }; 47 | 48 | format!( 49 | "{} {} {}", 50 | "border-b dark:border-gray-700", bg_color, template_classes 51 | ) 52 | } 53 | 54 | fn loading_cell(&self, _row_index: usize, _col_index: usize, prop_class: &str) -> String { 55 | format!("{} {}", "px-5 py-2", prop_class) 56 | } 57 | 58 | fn loading_cell_inner(&self, row_index: usize, _col_index: usize, prop_class: &str) -> String { 59 | let width = match row_index % 4 { 60 | 0 => "w-[calc(85%-2.5rem)]", 61 | 1 => "w-[calc(90%-2.5rem)]", 62 | 2 => "w-[calc(75%-2.5rem)]", 63 | _ => "w-[calc(60%-2.5rem)]", 64 | }; 65 | format!( 66 | "animate-pulse h-2 bg-gray-200 rounded-full dark:bg-gray-700 inline-block align-middle {} {}", 67 | width, prop_class 68 | ) 69 | } 70 | 71 | fn cell(&self, template_classes: &str) -> String { 72 | format!("{} {}", "px-5 py-2", template_classes) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/editable/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: { 4 | files: ["*.html", "./src/**/*.rs"], 5 | }, 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } -------------------------------------------------------------------------------- /examples/generic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "generic" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"] } 8 | leptos-struct-table = { path = "../..", features = ["chrono", "uuid"] } 9 | chrono = { version = "0.4" } 10 | uuid = { version= "1.8", features=["v4"]} 11 | console_error_panic_hook = "0.1" 12 | console_log = "1" 13 | log = "0.4" 14 | getrandom = { version = "0.2", features = ["js"] } 15 | 16 | [dev-dependencies] 17 | wasm-bindgen = "0.2" 18 | wasm-bindgen-test = "0.3.0" 19 | web-sys = "0.3" 20 | -------------------------------------------------------------------------------- /examples/generic/README.md: -------------------------------------------------------------------------------- 1 | ### A table example with a generic struct. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) 6 | as well as the wasm32-unknown-unknown target: 7 | 8 | ```bash 9 | cargo install trunk 10 | rustup target add wasm32-unknown-unknown 11 | ``` 12 | 13 | Then, to run this example, execute in a terminal: 14 | 15 | ```bash 16 | trunk serve --open 17 | ``` -------------------------------------------------------------------------------- /examples/generic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/generic/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Generic showcase example. 2 | 3 | use ::chrono::NaiveDate; 4 | use ::uuid::Uuid; 5 | use leptos::prelude::*; 6 | use leptos_struct_table::*; 7 | 8 | /// This generates the component BookTable 9 | #[derive(TableRow, Clone)] 10 | #[table(impl_vec_data_provider)] 11 | pub struct Book 12 | where 13 | // necessary trait bounds. `IntoView` is only necessary because we require it in 14 | // our custom renderer below, otherwise you could remove it here. 15 | // If you also make the table sortable then you might have to add `PartialOrd` as well. 16 | T: IntoView + Clone + Send + Sync + 'static, 17 | { 18 | /// Id of the entry. 19 | pub id: Uuid, 20 | /// Title of the book. 21 | pub title: String, 22 | /// Author of the book. 23 | pub author: String, 24 | /// Date when book has been published. 25 | pub publish_date: Option, 26 | /// Description of the book. Optional. 27 | #[table(none_value = "-")] 28 | pub description: Option, 29 | 30 | /// Generic field. You have to specify a custom renderer for a generic field. 31 | /// 32 | /// In case you need serde you also have to add 33 | /// ``` 34 | /// #[serde(bound(deserialize = "T: DeserializeOwned"))] 35 | /// ``` 36 | #[table(renderer = "CustomDataRenderer")] 37 | pub custom_data: T, 38 | } 39 | 40 | #[component] 41 | #[allow(unused_variables)] 42 | pub fn CustomDataRenderer( 43 | class: String, 44 | #[prop(into)] value: Signal, 45 | row: RwSignal>, 46 | index: usize, 47 | ) -> impl IntoView 48 | where 49 | T: IntoView + Clone + Send + Sync + 'static, 50 | { 51 | view! { 52 | {value} 53 | } 54 | } 55 | 56 | fn main() { 57 | _ = console_log::init_with_level(log::Level::Debug); 58 | console_error_panic_hook::set_once(); 59 | 60 | mount_to_body(|| { 61 | let rows = vec![ 62 | Book { 63 | id: Uuid::new_v4(), 64 | title: "The Great Gatsby".to_string(), 65 | author: "F. Scott Fitzgerald".to_string(), 66 | publish_date: Some(NaiveDate::from_ymd_opt(1925, 4, 10).unwrap()), 67 | description: Some( 68 | "A story of wealth, love, and the American Dream in the 1920s.".to_string(), 69 | ), 70 | custom_data: "custom data is a string here".to_string(), 71 | }, 72 | Book { 73 | id: Uuid::new_v4(), 74 | title: "The Grapes of Wrath".to_string(), 75 | author: "John Steinbeck".to_string(), 76 | publish_date: Some(NaiveDate::from_ymd_opt(1939, 4, 14).unwrap()), 77 | description: None, 78 | custom_data: "custom data is a string here".to_string(), 79 | }, 80 | Book { 81 | id: Uuid::new_v4(), 82 | title: "Nineteen Eighty-Four".to_string(), 83 | author: "George Orwell".to_string(), 84 | publish_date: Some(NaiveDate::from_ymd_opt(1949, 6, 8).unwrap()), 85 | description: None, 86 | custom_data: "custom data is a string here".to_string(), 87 | }, 88 | Book { 89 | id: Uuid::new_v4(), 90 | title: "Ulysses".to_string(), 91 | author: "James Joyce".to_string(), 92 | publish_date: Some(NaiveDate::from_ymd_opt(1922, 2, 2).unwrap()), 93 | description: None, 94 | custom_data: "still a string".to_string(), 95 | }, 96 | ]; 97 | 98 | view! { 99 | 100 | 101 |
102 | } 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /examples/getter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "getter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"]} 8 | leptos-struct-table = { path = "../..", features = ["chrono"] } 9 | chrono = { version = "0.4" } 10 | console_error_panic_hook = "0.1" 11 | console_log = "1" 12 | log = "0.4" 13 | 14 | [dev-dependencies] 15 | wasm-bindgen = "0.2" 16 | wasm-bindgen-test = "0.3.0" 17 | web-sys = "0.3" -------------------------------------------------------------------------------- /examples/getter/README.md: -------------------------------------------------------------------------------- 1 | ### A table example with a getter method. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) 6 | as well as the wasm32-unknown-unknown target: 7 | 8 | ```bash 9 | cargo install trunk 10 | rustup target add wasm32-unknown-unknown 11 | ``` 12 | 13 | Then, to run this example, execute in a terminal: 14 | 15 | ```bash 16 | trunk serve --open 17 | ``` -------------------------------------------------------------------------------- /examples/getter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/getter/src/main.rs: -------------------------------------------------------------------------------- 1 | use ::chrono::NaiveDate; 2 | use leptos::prelude::*; 3 | use leptos_struct_table::*; 4 | 5 | // This generates the component BookTable 6 | #[derive(TableRow, Clone)] 7 | #[table(sortable, impl_vec_data_provider)] 8 | pub struct Book { 9 | pub id: u32, 10 | pub title: String, 11 | 12 | // instead of accessing `item.publish_date` directly, we use a getter `item.get_publish_date()` 13 | #[table(getter = "get_publish_date")] 14 | pub publish_date: NaiveDate, 15 | 16 | #[table(skip)] 17 | pub author: Author, 18 | 19 | // specified that there is a getter method `author_name()` 20 | pub author_name: FieldGetter, 21 | } 22 | 23 | impl Book { 24 | // if no otherwise specified the getter method should have the same name as the `FieldGetter` field 25 | pub fn author_name(&self) -> String { 26 | format!("{} {}", self.author.first_name, self.author.last_name) 27 | } 28 | 29 | // getter for publish date 30 | pub fn get_publish_date(&self) -> NaiveDate { 31 | // do sth... 32 | self.publish_date 33 | } 34 | } 35 | 36 | #[derive(Clone)] 37 | pub struct Author { 38 | pub first_name: String, 39 | pub last_name: String, 40 | } 41 | 42 | fn main() { 43 | _ = console_log::init_with_level(log::Level::Debug); 44 | console_error_panic_hook::set_once(); 45 | 46 | mount_to_body(|| { 47 | let rows = vec![ 48 | Book { 49 | id: 1, 50 | title: "The Great Gatsby".to_string(), 51 | author: Author { 52 | first_name: "F. Scott".to_string(), 53 | last_name: "Fitzgerald".to_string(), 54 | }, 55 | publish_date: NaiveDate::from_ymd_opt(1925, 4, 10).unwrap(), 56 | author_name: Default::default(), 57 | }, 58 | Book { 59 | id: 2, 60 | title: "The Grapes of Wrath".to_string(), 61 | author: Author { 62 | first_name: "John".to_string(), 63 | last_name: "Steinbeck".to_string(), 64 | }, 65 | publish_date: NaiveDate::from_ymd_opt(1939, 4, 14).unwrap(), 66 | author_name: Default::default(), 67 | }, 68 | Book { 69 | id: 3, 70 | title: "Nineteen Eighty-Four".to_string(), 71 | author: Author { 72 | first_name: "George".to_string(), 73 | last_name: "Orwell".to_string(), 74 | }, 75 | publish_date: NaiveDate::from_ymd_opt(1949, 6, 8).unwrap(), 76 | author_name: Default::default(), 77 | }, 78 | Book { 79 | id: 4, 80 | title: "Ulysses".to_string(), 81 | author: Author { 82 | first_name: "James".to_string(), 83 | last_name: "Joyce".to_string(), 84 | }, 85 | publish_date: NaiveDate::from_ymd_opt(1922, 2, 2).unwrap(), 86 | author_name: Default::default(), 87 | }, 88 | ]; 89 | 90 | view! { 91 | 92 | 93 |
94 | } 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /examples/i18n/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "i18n" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"] } 8 | leptos-struct-table = { path = "../..", features = ["chrono", "uuid", "time", "i18n"] } 9 | leptos_i18n = { version = "0.5", features = ["csr"] } 10 | chrono = { version = "0.4", features = [] } 11 | console_error_panic_hook = "0.1" 12 | time = { version = "0.3" } 13 | uuid = { version = "1.8", features = ["v4"] } 14 | console_log = "1" 15 | log = "0.4" 16 | getrandom = { version = "0.2", features = ["js"] } 17 | 18 | [package.metadata.leptos-i18n] 19 | default = "en" 20 | locales = ["en", "de"] 21 | -------------------------------------------------------------------------------- /examples/i18n/README.md: -------------------------------------------------------------------------------- 1 | ### A simple table example showing how to integrate leptos-i18n to translate the titles. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) 6 | as well as the wasm32-unknown-unknown target: 7 | 8 | ```bash 9 | cargo install trunk 10 | rustup target add wasm32-unknown-unknown 11 | ``` 12 | 13 | Then, to run this example, execute in a terminal: 14 | 15 | ```bash 16 | trunk serve --open 17 | ``` -------------------------------------------------------------------------------- /examples/i18n/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/i18n/locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "book_table": { 3 | "title": "Titel", 4 | "author": "Autor", 5 | "publish_date": "Veröffentlichungsdatum", 6 | "read_date": "Lesedatum", 7 | "description": "Beschreibung" 8 | }, 9 | "click_to_change_lang": "Klicken um Sprache zu ändern" 10 | } 11 | -------------------------------------------------------------------------------- /examples/i18n/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "book_table": { 3 | "title": "Title", 4 | "author": "Author", 5 | "publish_date": "Publish Date", 6 | "read_date": "Read Date", 7 | "description": "Description" 8 | }, 9 | "click_to_change_lang": "Click to change language" 10 | } 11 | -------------------------------------------------------------------------------- /examples/i18n/src/main.rs: -------------------------------------------------------------------------------- 1 | use ::chrono::NaiveDate; 2 | use ::time::Date; 3 | use ::uuid::Uuid; 4 | use i18n::*; 5 | use leptos::prelude::*; 6 | use leptos_struct_table::*; 7 | 8 | leptos_i18n::load_locales!(); 9 | 10 | /// This generates the component BookTable 11 | #[derive(TableRow, Clone)] 12 | // You don't have to specify the i18n scope but it probably will save you some typing 13 | #[table(sortable, impl_vec_data_provider, i18n(scope = "book_table"))] 14 | pub struct Book { 15 | /// Id of the entry. 16 | #[table(i18n(skip))] 17 | pub id: Uuid, 18 | /// Title of the book. 19 | pub title: String, 20 | /// Author of the book. 21 | pub author: String, 22 | /// Date when book has been published. 23 | pub publish_date: Option, 24 | /// Date when book was read 25 | pub read_date: Option, 26 | /// Description of the book. Optional. 27 | #[table(none_value = "-", i18n(key = description))] 28 | pub desc: Option, 29 | /// Example on hidden member. 30 | #[table(skip)] 31 | pub hidden_field: String, 32 | } 33 | 34 | fn main() { 35 | _ = console_log::init_with_level(log::Level::Debug); 36 | console_error_panic_hook::set_once(); 37 | 38 | mount_to_body(|| { 39 | view! { 40 | 41 | 42 | 43 | } 44 | }) 45 | } 46 | 47 | #[component] 48 | pub fn App() -> impl IntoView { 49 | let rows = vec![ 50 | Book { 51 | id: Uuid::new_v4(), 52 | title: "The Great Gatsby".to_string(), 53 | author: "F. Scott Fitzgerald".to_string(), 54 | publish_date: Some(NaiveDate::from_ymd_opt(1925, 4, 10).unwrap()), 55 | read_date: Some(Date::from_calendar_date(2024, ::time::Month::January, 2).unwrap()), 56 | desc: Some("A story of wealth, love, and the American Dream in the 1920s.".to_string()), 57 | hidden_field: "hidden".to_string(), 58 | }, 59 | Book { 60 | id: Uuid::new_v4(), 61 | title: "The Grapes of Wrath".to_string(), 62 | author: "John Steinbeck".to_string(), 63 | publish_date: Some(NaiveDate::from_ymd_opt(1939, 4, 14).unwrap()), 64 | read_date: None, 65 | desc: None, 66 | hidden_field: "not visible in the table".to_string(), 67 | }, 68 | Book { 69 | id: Uuid::new_v4(), 70 | title: "Nineteen Eighty-Four".to_string(), 71 | author: "George Orwell".to_string(), 72 | publish_date: Some(NaiveDate::from_ymd_opt(1949, 6, 8).unwrap()), 73 | read_date: None, 74 | desc: None, 75 | hidden_field: "hidden".to_string(), 76 | }, 77 | Book { 78 | id: Uuid::new_v4(), 79 | title: "Ulysses".to_string(), 80 | author: "James Joyce".to_string(), 81 | publish_date: Some(NaiveDate::from_ymd_opt(1922, 2, 2).unwrap()), 82 | read_date: None, 83 | desc: None, 84 | hidden_field: "hidden".to_string(), 85 | }, 86 | ]; 87 | 88 | let i18n = use_i18n(); 89 | 90 | let on_switch = move |_| { 91 | let new_lang = match i18n.get_locale() { 92 | Locale::en => Locale::de, 93 | Locale::de => Locale::en, 94 | }; 95 | i18n.set_locale(new_lang); 96 | }; 97 | 98 | view! { 99 | 100 | 101 | 102 |
103 | } 104 | } 105 | -------------------------------------------------------------------------------- /examples/paginated_rest_datasource/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "paginated_rest_datasource" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"] } 8 | leptos-use = "0.15" 9 | leptos-struct-table = { path = "../.." } 10 | console_error_panic_hook = "0.1" 11 | console_log = "1" 12 | log = "0.4" 13 | serde = { version = "1", features = ["derive"] } 14 | gloo-net = { version = "0.6", features = ["http"] } 15 | -------------------------------------------------------------------------------- /examples/paginated_rest_datasource/README.md: -------------------------------------------------------------------------------- 1 | ### Example that shows how to use a paginated REST API as a data source for a table. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) 6 | as well as the wasm32-unknown-unknown target: 7 | 8 | ```bash 9 | cargo install trunk 10 | rustup target add wasm32-unknown-unknown 11 | ``` 12 | 13 | Then, to run this example, execute in a terminal: 14 | 15 | ```bash 16 | trunk serve --open 17 | ``` -------------------------------------------------------------------------------- /examples/paginated_rest_datasource/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/paginated_rest_datasource/src/data_provider.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{Brewery, MetaResponse}; 2 | use gloo_net::http::Request; 3 | use leptos::prelude::*; 4 | use leptos_struct_table::{ColumnSort, PaginatedTableDataProvider}; 5 | use std::collections::VecDeque; 6 | 7 | pub struct BreweryDataProvider { 8 | sorting: VecDeque<(usize, ColumnSort)>, 9 | pub search: RwSignal, 10 | } 11 | 12 | impl Default for BreweryDataProvider { 13 | fn default() -> Self { 14 | Self { 15 | sorting: VecDeque::new(), 16 | search: RwSignal::new("".to_string()), 17 | } 18 | } 19 | } 20 | 21 | impl BreweryDataProvider { 22 | fn url_sort_param_for_column(&self, column: usize) -> &'static str { 23 | match column { 24 | 0 => "name", 25 | 1 => "city", 26 | 2 => "country", 27 | _ => "", 28 | } 29 | } 30 | 31 | fn url_sort_param_for_sort_pair(&self, pair: &(usize, ColumnSort)) -> String { 32 | let col = self.url_sort_param_for_column(pair.0); 33 | 34 | let dir = match pair.1 { 35 | ColumnSort::Ascending => "asc", 36 | ColumnSort::Descending => "desc", 37 | ColumnSort::None => return "".to_string(), 38 | }; 39 | 40 | format!("&sort={}:{}", col, dir) 41 | } 42 | 43 | fn get_url(&self, page_index: usize) -> String { 44 | let mut sort = String::new(); 45 | for pair in &self.sorting { 46 | sort.push_str(&self.url_sort_param_for_sort_pair(pair)); 47 | } 48 | 49 | format!( 50 | "https://api.openbrewerydb.org/v1/breweries?by_name={}{sort}&page={}&per_page={}", 51 | self.search.get_untracked(), 52 | page_index + 1, 53 | Self::PAGE_ROW_COUNT, 54 | ) 55 | } 56 | } 57 | 58 | impl PaginatedTableDataProvider for BreweryDataProvider { 59 | const PAGE_ROW_COUNT: usize = 200; 60 | 61 | async fn get_page(&self, page_index: usize) -> Result, String> { 62 | if page_index >= 10000 / Self::PAGE_ROW_COUNT { 63 | return Ok(vec![]); 64 | } 65 | 66 | let url = self.get_url(page_index); 67 | 68 | let resp: Vec = Request::get(&url) 69 | .send() 70 | .await 71 | .map_err(|e| e.to_string())? 72 | .json() 73 | .await 74 | .map_err(|e| e.to_string())?; 75 | 76 | Ok(resp) 77 | } 78 | 79 | async fn row_count(&self) -> Option { 80 | let url = format!("https://api.openbrewerydb.org/v1/breweries/meta?by_name={}", self.search.get_untracked()); 81 | let resp: Option = Request::get(&url) 82 | .send() 83 | .await 84 | .map_err(|e| e.to_string()) 85 | .ok()? 86 | .json() 87 | .await 88 | .map_err(|e| e.to_string()) 89 | .ok()?; 90 | 91 | let count = resp.map(|r| r.total)?.parse::().ok()?; 92 | 93 | Some(count) 94 | } 95 | 96 | fn set_sorting(&mut self, sorting: &VecDeque<(usize, ColumnSort)>) { 97 | self.sorting = sorting.clone(); 98 | } 99 | 100 | fn track(&self) { 101 | // we depend on the search so we need to track it here 102 | self.search.track(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /examples/paginated_rest_datasource/src/main.rs: -------------------------------------------------------------------------------- 1 | mod data_provider; 2 | mod models; 3 | mod renderer; 4 | 5 | use crate::data_provider::BreweryDataProvider; 6 | use leptos::prelude::*; 7 | use leptos_struct_table::*; 8 | use leptos_use::use_debounce_fn_with_arg; 9 | 10 | #[component] 11 | pub fn App() -> impl IntoView { 12 | let rows = BreweryDataProvider::default(); 13 | 14 | let reload_controller = ReloadController::default(); 15 | 16 | let reload = move |_| { 17 | reload_controller.reload(); 18 | }; 19 | 20 | let container = NodeRef::new(); 21 | 22 | let (count, set_count) = signal(0); 23 | 24 | let on_input = use_debounce_fn_with_arg(move |value| rows.search.set(value), 300.0); 25 | 26 | view! { 27 |
28 |
29 | 30 | 42 | 0 }> 43 |
"Found " {count} " results"
44 |
45 |
46 |
47 | 48 | 56 |
57 |
58 |
59 | } 60 | } 61 | 62 | fn main() { 63 | _ = console_log::init_with_level(log::Level::Debug); 64 | console_error_panic_hook::set_once(); 65 | 66 | mount_to_body(|| { 67 | view! { } 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /examples/paginated_rest_datasource/src/models.rs: -------------------------------------------------------------------------------- 1 | use crate::renderer::ObjectLinkTableCellRenderer; 2 | use leptos_struct_table::{FieldGetter, TableRow}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(PartialEq, PartialOrd, Clone, Debug, Default)] 6 | pub struct Link { 7 | pub text: String, 8 | pub href: String, 9 | } 10 | 11 | #[derive(TableRow, Serialize, Deserialize, Clone, Debug)] 12 | #[table(sortable)] 13 | pub struct Brewery { 14 | #[table(skip)] 15 | pub id: String, 16 | 17 | pub name: String, 18 | 19 | pub city: String, 20 | 21 | pub country: String, 22 | 23 | #[table(skip)] 24 | pub website_url: Option, 25 | 26 | #[serde(skip_deserializing)] 27 | #[table(title = "Link", renderer = "ObjectLinkTableCellRenderer")] 28 | pub link: FieldGetter, 29 | } 30 | 31 | impl Brewery { 32 | pub fn link(&self) -> Link { 33 | Link { 34 | text: self.website_url.clone().unwrap_or("".to_string()), 35 | href: self.website_url.clone().unwrap_or("".to_string()), 36 | } 37 | } 38 | } 39 | 40 | #[derive(Deserialize, Debug)] 41 | pub struct MetaResponse { 42 | pub total: String, 43 | pub page: String, 44 | pub per_page: String, 45 | } -------------------------------------------------------------------------------- /examples/paginated_rest_datasource/src/renderer.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{Brewery, Link}; 2 | use leptos::prelude::*; 3 | 4 | #[component] 5 | #[allow(unused_variables)] 6 | pub fn ObjectLinkTableCellRenderer( 7 | class: String, 8 | #[prop(into)] value: Signal, 9 | row: RwSignal, 10 | index: usize, 11 | ) -> impl IntoView { 12 | view! { 13 | 14 | {value.get_untracked().text} }> 15 | {value.get_untracked().text} 16 | 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/paginated_rest_datasource/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | font-family: sans-serif; 3 | font-size: 11pt; 4 | height: 100vh; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | .container { 10 | display: flex; 11 | height: 100%; 12 | width: 100%; 13 | justify-content: stretch; 14 | flex-direction: column; 15 | } 16 | 17 | .top-bar { 18 | display: flex; 19 | align-items: center; 20 | justify-content: space-between; 21 | padding: 10px 15px; 22 | background: white; 23 | border-bottom: 1px solid silver; 24 | } 25 | 26 | #search { 27 | display: flex; 28 | border: 1px solid silver; 29 | border-radius: 1000px; 30 | align-items: center; 31 | padding-left: 7px; 32 | } 33 | 34 | #search > * { 35 | display: block; 36 | } 37 | 38 | #search > svg { 39 | height: 16px; 40 | opacity: 0.5; 41 | } 42 | 43 | input[type="search"] { 44 | min-width: 50%; 45 | border: 0 none; 46 | appearance: none; 47 | padding: 5px 15px 5px 5px; 48 | background: transparent; 49 | outline: none; 50 | } 51 | 52 | .table-container { 53 | flex: 1; 54 | overflow: auto; 55 | } 56 | 57 | table { 58 | table-layout: fixed; 59 | width: 100%; 60 | border-collapse: collapse; 61 | } 62 | 63 | tr:nth-child(2n+1) { 64 | background: rgba(255, 255, 255, 0.7); 65 | } 66 | 67 | th { 68 | background: white; 69 | position: sticky; 70 | top: 0; 71 | width: 20%; 72 | padding: 10px 15px; 73 | text-align: left; 74 | border-bottom: 1px solid silver; 75 | } 76 | 77 | th:first-child { 78 | width: 40%; 79 | } 80 | 81 | td { 82 | white-space: nowrap; 83 | overflow: hidden; 84 | text-overflow: ellipsis; 85 | width: 20%; 86 | padding: 10px 15px; 87 | } 88 | 89 | td:first-child { 90 | width: 40%; 91 | } 92 | 93 | .loading-skeleton { 94 | display: inline-block; 95 | animation: skeleton-loading 1s linear infinite alternate; 96 | height: 0.75rem; 97 | border-radius: 0.25rem; 98 | width: 80%; 99 | vertical-align: middle; 100 | } 101 | 102 | tr:nth-child(4n) .loading-skeleton { 103 | width: 60%; 104 | } 105 | 106 | tr:nth-child(4n+1) .loading-skeleton { 107 | width: 90%; 108 | } 109 | 110 | tr:nth-child(4n+2) .loading-skeleton { 111 | width: 40%; 112 | } 113 | 114 | @keyframes skeleton-loading { 115 | 0% { 116 | background-color: rgba(0, 0, 0, 0.15); 117 | } 118 | 100% { 119 | background-color: rgba(0, 0, 0, 0.05); 120 | } 121 | } -------------------------------------------------------------------------------- /examples/pagination/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pagination" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"] } 8 | leptos-struct-table = { path = "../.." } 9 | console_error_panic_hook = "0.1" 10 | console_log = "1" 11 | log = "0.4" 12 | serde = { version = "1", features = ["derive"] } 13 | gloo-net = { version = "0.6.0", features = ["http"] } 14 | 15 | [dev-dependencies] 16 | wasm-bindgen = "0.2" 17 | wasm-bindgen-test = "0.3.0" 18 | web-sys = "0.3" -------------------------------------------------------------------------------- /examples/pagination/README.md: -------------------------------------------------------------------------------- 1 | ### Example that shows how to use pagination with a table. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) 6 | as well as the wasm32-unknown-unknown target: 7 | 8 | ```bash 9 | cargo install trunk 10 | npm install -D tailwindcss 11 | rustup target add wasm32-unknown-unknown 12 | ``` 13 | 14 | Then, open two terminals. In the first one, run: 15 | 16 | ``` 17 | npx tailwindcss -i ./input.css -o ./style/output.css --watch 18 | ``` 19 | 20 | In the second one, run: 21 | 22 | ```bash 23 | trunk serve --open 24 | ``` -------------------------------------------------------------------------------- /examples/pagination/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/pagination/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /examples/pagination/src/data_provider.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{Brewery, MetaResponse}; 2 | use gloo_net::http::Request; 3 | use leptos_struct_table::{ColumnSort, PaginatedTableDataProvider}; 4 | use std::collections::VecDeque; 5 | 6 | pub struct BreweryDataProvider { 7 | sorting: VecDeque<(usize, ColumnSort)>, 8 | } 9 | 10 | impl Default for BreweryDataProvider { 11 | fn default() -> Self { 12 | Self { 13 | sorting: VecDeque::new(), 14 | } 15 | } 16 | } 17 | 18 | impl BreweryDataProvider { 19 | fn url_sort_param_for_column(&self, column: usize) -> &'static str { 20 | match column { 21 | 0 => "name", 22 | 1 => "brewery_type", 23 | 2 => "city", 24 | 3 => "country", 25 | _ => "", 26 | } 27 | } 28 | 29 | fn url_sort_param_for_sort_pair(&self, pair: &(usize, ColumnSort)) -> String { 30 | let col = self.url_sort_param_for_column(pair.0); 31 | 32 | let dir = match pair.1 { 33 | ColumnSort::Ascending => "asc", 34 | ColumnSort::Descending => "desc", 35 | ColumnSort::None => return "".to_string(), 36 | }; 37 | 38 | format!("sort={}:{}", col, dir) 39 | } 40 | 41 | fn get_url(&self, page_index: usize) -> String { 42 | let mut sort = String::new(); 43 | for pair in &self.sorting { 44 | sort.push_str(&self.url_sort_param_for_sort_pair(pair)); 45 | } 46 | 47 | format!( 48 | "https://api.openbrewerydb.org/v1/breweries?{sort}&page={}&per_page={}", 49 | page_index + 1, 50 | Self::PAGE_ROW_COUNT, 51 | ) 52 | } 53 | } 54 | 55 | impl PaginatedTableDataProvider for BreweryDataProvider { 56 | const PAGE_ROW_COUNT: usize = 200; 57 | 58 | async fn get_page(&self, page_index: usize) -> Result, String> { 59 | if page_index >= 10000 / Self::PAGE_ROW_COUNT { 60 | return Ok(vec![]); 61 | } 62 | 63 | let url = self.get_url(page_index); 64 | 65 | let resp: Vec = Request::get(&url) 66 | .send() 67 | .await 68 | .map_err(|e| e.to_string())? 69 | .json() 70 | .await 71 | .map_err(|e| e.to_string())?; 72 | 73 | Ok(resp) 74 | } 75 | 76 | async fn row_count(&self) -> Option { 77 | let resp: Option = Request::get("https://api.openbrewerydb.org/v1/breweries/meta") 78 | .send() 79 | .await 80 | .map_err(|e| e.to_string()) 81 | .ok()? 82 | .json() 83 | .await 84 | .map_err(|e| e.to_string()) 85 | .ok()?; 86 | 87 | let count = resp.map(|r| r.total)?.parse::().ok()?; 88 | 89 | Some(count) 90 | } 91 | 92 | fn set_sorting(&mut self, sorting: &VecDeque<(usize, ColumnSort)>) { 93 | self.sorting = sorting.clone(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /examples/pagination/src/main.rs: -------------------------------------------------------------------------------- 1 | mod data_provider; 2 | mod models; 3 | mod tailwind; 4 | 5 | use crate::data_provider::BreweryDataProvider; 6 | use leptos::prelude::*; 7 | use leptos_struct_table::*; 8 | 9 | #[component] 10 | pub fn App() -> impl IntoView { 11 | let rows = BreweryDataProvider::default(); 12 | 13 | let pagination_controller = PaginationController::default(); 14 | 15 | view! { 16 |
17 | 18 | 27 | 28 |
29 |
30 | 31 | 32 | } 33 | } 34 | 35 | #[component] 36 | pub fn Paginator(pagination_controller: PaginationController) -> impl IntoView { 37 | let current_page = pagination_controller.current_page; 38 | let page_count = pagination_controller.page_count(); 39 | 40 | let page_range = move || { 41 | let mut start = current_page.get().saturating_sub(2); 42 | 43 | let mut end = start + 5; 44 | 45 | if let Some(row_count) = page_count.get() { 46 | if end > row_count { 47 | end = row_count; 48 | start = end.saturating_sub(5); 49 | } 50 | } 51 | 52 | start..end 53 | }; 54 | 55 | view! { 56 | 93 | } 94 | } 95 | 96 | #[component] 97 | pub fn PageLink(page: usize, pagination_controller: PaginationController) -> impl IntoView { 98 | let is_selected = move || pagination_controller.current_page.get() == page; 99 | 100 | let class = move || { 101 | if is_selected() { 102 | "flex items-center justify-center px-3 h-8 text-blue-600 border border-gray-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white" 103 | } else { 104 | "flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white" 105 | } 106 | }; 107 | 108 | view! { 109 |
  • 110 | 119 | 120 | {page + 1} 121 | 122 |
  • 123 | } 124 | } 125 | 126 | fn main() { 127 | _ = console_log::init_with_level(log::Level::Debug); 128 | console_error_panic_hook::set_once(); 129 | 130 | mount_to_body(|| { 131 | view! { } 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /examples/pagination/src/models.rs: -------------------------------------------------------------------------------- 1 | use crate::tailwind::TailwindClassesPreset; 2 | use leptos_struct_table::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(TableRow, Serialize, Deserialize, Clone, Debug)] 6 | #[table(sortable, classes_provider = "TailwindClassesPreset")] 7 | pub struct Brewery { 8 | #[table(skip)] 9 | pub id: String, 10 | 11 | pub name: String, 12 | 13 | pub brewery_type: String, 14 | 15 | pub city: String, 16 | 17 | pub country: String, 18 | } 19 | 20 | #[derive(Deserialize, Debug)] 21 | pub struct MetaResponse { 22 | pub total: String, 23 | pub page: String, 24 | pub per_page: String, 25 | } -------------------------------------------------------------------------------- /examples/pagination/src/tailwind.rs: -------------------------------------------------------------------------------- 1 | use crate::{ColumnSort, TableClassesProvider}; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct TailwindClassesPreset; 5 | 6 | impl TableClassesProvider for TailwindClassesPreset { 7 | fn new() -> Self { 8 | Self 9 | } 10 | 11 | fn thead_row(&self, template_classes: &str) -> String { 12 | format!( 13 | "{} {}", 14 | "text-xs text-gray-700 uppercase bg-gray-200 dark:bg-gray-700 dark:text-gray-300", 15 | template_classes 16 | ) 17 | } 18 | 19 | fn thead_cell(&self, sort: ColumnSort, template_classes: &str) -> String { 20 | let sort_class = match sort { 21 | ColumnSort::None => "", 22 | _ => "text-black dark:text-white", 23 | }; 24 | 25 | format!( 26 | "cursor-pointer px-5 py-2 {} {}", 27 | sort_class, template_classes 28 | ) 29 | } 30 | 31 | fn thead_cell_inner(&self) -> String { 32 | "flex items-center after:content-[--sort-icon] after:pl-1 after:opacity-40 before:content-[--sort-priority] before:order-last before:pl-0.5 before:font-light before:opacity-40".to_string() 33 | } 34 | 35 | fn row(&self, row_index: usize, selected: bool, template_classes: &str) -> String { 36 | let bg_color = if row_index % 2 == 0 { 37 | if selected { 38 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 39 | } else { 40 | "bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800" 41 | } 42 | } else if selected { 43 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 44 | } else { 45 | "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700" 46 | }; 47 | 48 | format!( 49 | "{} {} {}", 50 | "border-b dark:border-gray-700", bg_color, template_classes 51 | ) 52 | } 53 | 54 | fn loading_cell(&self, _row_index: usize, _col_index: usize, prop_class: &str) -> String { 55 | format!("{} {}", "px-5 py-2", prop_class) 56 | } 57 | 58 | fn loading_cell_inner(&self, row_index: usize, _col_index: usize, prop_class: &str) -> String { 59 | let width = match row_index % 4 { 60 | 0 => "w-[calc(85%-2.5rem)]", 61 | 1 => "w-[calc(90%-2.5rem)]", 62 | 2 => "w-[calc(75%-2.5rem)]", 63 | _ => "w-[calc(60%-2.5rem)]", 64 | }; 65 | format!( 66 | "animate-pulse h-2 bg-gray-200 rounded-full dark:bg-gray-700 inline-block align-middle {} {}", 67 | width, prop_class 68 | ) 69 | } 70 | 71 | fn cell(&self, template_classes: &str) -> String { 72 | format!("{} {}", "px-5 py-2", template_classes) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/pagination/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: { 4 | files: ["*.html", "./src/**/*.rs"], 5 | }, 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } -------------------------------------------------------------------------------- /examples/selectable/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "selectable" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"]} 8 | leptos-struct-table = { path = "../..", features = ["chrono"] } 9 | chrono = { version = "0.4" } 10 | console_error_panic_hook = "0.1" 11 | console_log = "1" 12 | log = "0.4" 13 | 14 | [dev-dependencies] 15 | wasm-bindgen = "0.2" 16 | wasm-bindgen-test = "0.3.0" 17 | web-sys = "0.3" -------------------------------------------------------------------------------- /examples/selectable/README.md: -------------------------------------------------------------------------------- 1 | ### A table example with selectable rows and data that is stored as `Vec` Uses the Tailwind class provider. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | The way Tailwind works, is to scan the classes in the code. Due to this it is 6 | recommended to copy the file `src/class_providers/tailwind.rs` into your project as done in this example. 7 | 8 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation) 9 | as well as the wasm32-unknown-unknown target: 10 | 11 | ```bash 12 | cargo install trunk 13 | npm install -D tailwindcss 14 | rustup target add wasm32-unknown-unknown 15 | ``` 16 | 17 | Then, open two terminals. In the first one, run: 18 | 19 | ``` 20 | npx tailwindcss -i ./input.css -o ./style/output.css --watch 21 | ``` 22 | 23 | In the second one, run: 24 | 25 | ```bash 26 | trunk serve --open 27 | ``` -------------------------------------------------------------------------------- /examples/selectable/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/selectable/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /examples/selectable/src/main.rs: -------------------------------------------------------------------------------- 1 | mod tailwind; 2 | 3 | use ::chrono::NaiveDate; 4 | use leptos::prelude::*; 5 | use leptos_struct_table::*; 6 | use tailwind::TailwindClassesPreset; 7 | 8 | // This generates the component BookTable 9 | #[derive(TableRow, Clone)] 10 | #[table( 11 | sortable, 12 | classes_provider = "TailwindClassesPreset", 13 | impl_vec_data_provider 14 | )] 15 | pub struct Book { 16 | pub id: u32, 17 | pub title: String, 18 | pub author: String, 19 | #[table( 20 | cell_class = "text-red-600 dark:text-red-400", 21 | head_class = "text-red-700 dark:text-red-300" 22 | )] 23 | pub publish_date: NaiveDate, 24 | } 25 | 26 | fn main() { 27 | _ = console_log::init_with_level(log::Level::Debug); 28 | console_error_panic_hook::set_once(); 29 | 30 | mount_to_body(|| { 31 | let rows = vec![ 32 | Book { 33 | id: 1, 34 | title: "The Great Gatsby".to_string(), 35 | author: "F. Scott Fitzgerald".to_string(), 36 | publish_date: NaiveDate::from_ymd_opt(1925, 4, 10).unwrap(), 37 | }, 38 | Book { 39 | id: 2, 40 | title: "The Grapes of Wrath".to_string(), 41 | author: "John Steinbeck".to_string(), 42 | publish_date: NaiveDate::from_ymd_opt(1939, 4, 14).unwrap(), 43 | }, 44 | Book { 45 | id: 3, 46 | title: "Nineteen Eighty-Four".to_string(), 47 | author: "George Orwell".to_string(), 48 | publish_date: NaiveDate::from_ymd_opt(1949, 6, 8).unwrap(), 49 | }, 50 | Book { 51 | id: 4, 52 | title: "Ulysses".to_string(), 53 | author: "James Joyce".to_string(), 54 | publish_date: NaiveDate::from_ymd_opt(1922, 2, 2).unwrap(), 55 | }, 56 | ]; 57 | 58 | let selected_index = RwSignal::new(None); 59 | let (selected_row, set_selected_row) = signal(None); 60 | 61 | view! { 62 |
    63 | 64 | | { 70 | set_selected_row.write().replace(evt.row); 71 | }} 72 | sorting_mode=SortingMode::SingleColumn 73 | /> 74 |
    75 |
    76 | 77 | { move || selected_row.get().map(|selected_row| { 78 | let selected_row = selected_row.get(); 79 | 80 | view! { 81 |
    82 |
    83 |                             "          Id:  " {selected_row.id} "\n"
    84 |                             "       Title:  " {selected_row.title} "\n"
    85 |                             "      Author:  " {selected_row.author} "\n"
    86 |                             "Publish Date:  " {selected_row.publish_date.to_string()}
    87 |                         
    88 |
    89 | } 90 | }) } 91 | } 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /examples/selectable/src/tailwind.rs: -------------------------------------------------------------------------------- 1 | use crate::{ColumnSort, TableClassesProvider}; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct TailwindClassesPreset; 5 | 6 | impl TableClassesProvider for TailwindClassesPreset { 7 | fn new() -> Self { 8 | Self 9 | } 10 | 11 | fn thead_row(&self, template_classes: &str) -> String { 12 | format!( 13 | "{} {}", 14 | "text-xs text-gray-700 uppercase bg-gray-200 dark:bg-gray-700 dark:text-gray-300", 15 | template_classes 16 | ) 17 | } 18 | 19 | fn thead_cell(&self, sort: ColumnSort, template_classes: &str) -> String { 20 | let sort_class = match sort { 21 | ColumnSort::None => "", 22 | _ => "text-black dark:text-white", 23 | }; 24 | 25 | format!( 26 | "cursor-pointer px-5 py-2 {} {}", 27 | sort_class, template_classes 28 | ) 29 | } 30 | 31 | fn thead_cell_inner(&self) -> String { 32 | "flex items-center after:content-[--sort-icon] after:pl-1 after:opacity-40 before:content-[--sort-priority] before:order-last before:pl-0.5 before:font-light before:opacity-40".to_string() 33 | } 34 | 35 | fn row(&self, row_index: usize, selected: bool, template_classes: &str) -> String { 36 | let bg_color = if row_index % 2 == 0 { 37 | if selected { 38 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 39 | } else { 40 | "bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800" 41 | } 42 | } else if selected { 43 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 44 | } else { 45 | "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700" 46 | }; 47 | 48 | format!( 49 | "{} {} {}", 50 | "border-b dark:border-gray-700", bg_color, template_classes 51 | ) 52 | } 53 | 54 | fn loading_cell(&self, _row_index: usize, _col_index: usize, prop_class: &str) -> String { 55 | format!("{} {}", "px-5 py-2", prop_class) 56 | } 57 | 58 | fn loading_cell_inner(&self, row_index: usize, _col_index: usize, prop_class: &str) -> String { 59 | let width = match row_index % 4 { 60 | 0 => "w-[calc(85%-2.5rem)]", 61 | 1 => "w-[calc(90%-2.5rem)]", 62 | 2 => "w-[calc(75%-2.5rem)]", 63 | _ => "w-[calc(60%-2.5rem)]", 64 | }; 65 | format!( 66 | "animate-pulse h-2 bg-gray-200 rounded-full dark:bg-gray-700 inline-block align-middle {} {}", 67 | width, prop_class 68 | ) 69 | } 70 | 71 | fn cell(&self, template_classes: &str) -> String { 72 | format!("{} {}", "px-5 py-2", template_classes) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/selectable/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: { 4 | files: ["*.html", "./src/**/*.rs"], 5 | }, 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } -------------------------------------------------------------------------------- /examples/serverfn_sqlx/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | pkg 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # node e2e test tools and outputs 10 | node_modules/ 11 | test-results/ 12 | end2end/playwright-report/ 13 | playwright/.cache/ 14 | *.sqlite3-shm -------------------------------------------------------------------------------- /examples/serverfn_sqlx/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serverfn_sqlx" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [dependencies] 10 | axum = { version = "0.7", optional = true } 11 | console_error_panic_hook = "0.1" 12 | http = "1" 13 | leptos = { version = "0.7" } 14 | leptos-struct-table = { path = "../.." } 15 | leptos-use = { version = "0.15" } 16 | leptos_axum = { version = "0.7", optional = true } 17 | leptos_meta = { version = "0.7" } 18 | leptos_router = { version = "0.7" } 19 | serde = { version = "1.0.197", features = ["derive"] } 20 | sqlx = { version = "0.8", optional = true, features = ["sqlite", "runtime-tokio-rustls"] } 21 | thiserror = "2" 22 | tokio = { version = "1", features = ["rt-multi-thread"], optional = true } 23 | tower = { version = "0.4", optional = true } 24 | tower-http = { version = "0.5", features = ["fs"], optional = true } 25 | tracing = { version = "0.1", optional = true } 26 | wasm-bindgen = "0.2.99" 27 | 28 | [features] 29 | hydrate = ["leptos/hydrate"] 30 | ssr = [ 31 | "dep:axum", 32 | "dep:tokio", 33 | "dep:tower", 34 | "dep:tower-http", 35 | "dep:leptos_axum", 36 | "dep:sqlx", 37 | "leptos/ssr", 38 | "leptos_meta/ssr", 39 | "leptos_router/ssr", 40 | "leptos-use/ssr", 41 | "dep:tracing", 42 | ] 43 | 44 | # Defines a size-optimized profile for the WASM bundle in release mode 45 | [profile.wasm-release] 46 | inherits = "release" 47 | opt-level = 'z' 48 | lto = true 49 | codegen-units = 1 50 | panic = "abort" 51 | 52 | [package.metadata.leptos] 53 | # The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name 54 | output-name = "serverfn-sqlx" 55 | 56 | # The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. 57 | site-root = "target/site" 58 | 59 | # The site-root relative folder where all compiled output (JS, WASM and CSS) is written 60 | # Defaults to pkg 61 | site-pkg-dir = "pkg" 62 | 63 | # [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to //app.css 64 | style-file = "style/output.css" 65 | # Assets source dir. All files found here will be copied and synchronized to site-root. 66 | # The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. 67 | # 68 | # Optional. Env: LEPTOS_ASSETS_DIR. 69 | assets-dir = "public" 70 | 71 | # The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. 72 | site-addr = "127.0.0.1:3000" 73 | 74 | # The port to use for automatic reload monitoring 75 | reload-port = 3001 76 | 77 | 78 | # The browserlist query used for optimizing the CSS. 79 | browserquery = "defaults" 80 | 81 | # Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head 82 | watch = false 83 | 84 | # The environment Leptos will run in, usually either "DEV" or "PROD" 85 | env = "DEV" 86 | 87 | # The features to use when compiling the bin target 88 | # 89 | # Optional. Can be over-ridden with the command line parameter --bin-features 90 | bin-features = ["ssr"] 91 | 92 | # If the --no-default-features flag should be used when compiling the bin target 93 | # 94 | # Optional. Defaults to false. 95 | bin-default-features = false 96 | 97 | # The features to use when compiling the lib target 98 | # 99 | # Optional. Can be over-ridden with the command line parameter --lib-features 100 | lib-features = ["hydrate"] 101 | 102 | # If the --no-default-features flag should be used when compiling the lib target 103 | # 104 | # Optional. Defaults to false. 105 | lib-default-features = false 106 | 107 | # The profile to use for the lib target when compiling for release 108 | # 109 | # Optional. Defaults to "release". 110 | lib-profile-release = "wasm-release" 111 | 112 | tailwind-input-file = "input.css" 113 | -------------------------------------------------------------------------------- /examples/serverfn_sqlx/README.md: -------------------------------------------------------------------------------- 1 | ### A table example with a data source that uses server functions and sqlx. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | If you don't have `cargo-leptos` installed you can install it with 6 | 7 | ```bash 8 | cargo install cargo-leptos 9 | ``` 10 | 11 | To run the example execute 12 | 13 | ```bash 14 | cargo leptos watch 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/serverfn_sqlx/db.sqlite3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bf3c2e0d246802786cdd76a90e31e965fa8fa3a241af167e28ce57e994017020 3 | size 1478656 4 | -------------------------------------------------------------------------------- /examples/serverfn_sqlx/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/serverfn_sqlx/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synphonyte/leptos-struct-table/41c10e3ba36b606760ae1b8a447d53f05781cb9f/examples/serverfn_sqlx/public/favicon.ico -------------------------------------------------------------------------------- /examples/serverfn_sqlx/src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::data_provider::CustomerTableDataProvider; 2 | use leptos::prelude::*; 3 | use leptos_meta::*; 4 | use leptos_router::components::{FlatRoutes, Route, Router, RoutingProgress}; 5 | use leptos_router::path; 6 | use leptos_struct_table::*; 7 | use std::time::Duration; 8 | 9 | #[component] 10 | pub fn App() -> impl IntoView { 11 | // Provides context that manages stylesheets, titles, meta tags, etc. 12 | provide_meta_context(); 13 | let (is_routing, set_is_routing) = signal(false); 14 | 15 | view! { 16 | 17 | 18 | 19 | <Router set_is_routing> 20 | <div class="routing-progress"> 21 | <RoutingProgress is_routing max_time=Duration::from_millis(250) /> 22 | </div> 23 | <main> 24 | <FlatRoutes fallback=|| "Not Found"> 25 | <Route path=path!("") view=HomePage /> 26 | </FlatRoutes> 27 | </main> 28 | </Router> 29 | } 30 | } 31 | 32 | #[component] 33 | fn HomePage() -> impl IntoView { 34 | let scroll_container = NodeRef::new(); 35 | 36 | let rows = CustomerTableDataProvider::default(); 37 | 38 | let name = rows.name; 39 | 40 | view! { 41 | <div class="flex flex-col h-[100vh] bg-white"> 42 | <div class="border-b bg-slate-100 px-5 py-2"> 43 | <label class="relative block"> 44 | <span class="absolute inset-y-0 left-0 flex items-center pl-3"> 45 | <svg 46 | class="h-5 w-5 fill-black" 47 | xmlns="http://www.w3.org/2000/svg" 48 | x="0px" 49 | y="0px" 50 | width="30" 51 | height="30" 52 | viewBox="0 0 30 30" 53 | > 54 | <path d="M 13 3 C 7.4889971 3 3 7.4889971 3 13 C 3 18.511003 7.4889971 23 13 23 C 15.396508 23 17.597385 22.148986 19.322266 20.736328 L 25.292969 26.707031 A 1.0001 1.0001 0 1 0 26.707031 25.292969 L 20.736328 19.322266 C 22.148986 17.597385 23 15.396508 23 13 C 23 7.4889971 18.511003 3 13 3 z M 13 5 C 17.430123 5 21 8.5698774 21 13 C 21 17.430123 17.430123 21 13 21 C 8.5698774 21 5 17.430123 5 13 C 5 8.5698774 8.5698774 5 13 5 z"></path> 55 | </svg> 56 | </span> 57 | <input 58 | class="w-full bg-white placeholder:font-italitc border border-slate-300 rounded-full py-2 pl-10 pr-4 focus:outline-none" 59 | placeholder="Search by name or company" 60 | type="text" 61 | value=name 62 | on:change=move |e| name.set(event_target_value(&e)) 63 | /> 64 | </label> 65 | </div> 66 | <div node_ref=scroll_container class="overflow-auto grow min-h-0"> 67 | <table class="table-fixed text-sm text-left text-gray-500 dark:text-gray-400 w-full"> 68 | <TableContent rows scroll_container /> 69 | </table> 70 | </div> 71 | </div> 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/serverfn_sqlx/src/classes.rs: -------------------------------------------------------------------------------- 1 | use leptos_struct_table::{ColumnSort, TableClassesProvider}; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct ClassesPreset; 5 | 6 | impl TableClassesProvider for ClassesPreset { 7 | fn new() -> Self { 8 | Self 9 | } 10 | 11 | fn thead_row(&self, template_classes: &str) -> String { 12 | format!( 13 | "{} {}", 14 | "text-xs text-gray-700 uppercase dark:text-gray-300", template_classes 15 | ) 16 | } 17 | 18 | fn thead_cell(&self, sort: ColumnSort, template_classes: &str) -> String { 19 | let sort_class = match sort { 20 | ColumnSort::None => "", 21 | _ => "text-black dark:text-white", 22 | }; 23 | 24 | format!( 25 | "bg-gray-200 dark:bg-gray-700 cursor-pointer px-5 py-2 sticky top-0 whitespace-nowrap {} {}", 26 | sort_class, template_classes 27 | ) 28 | } 29 | 30 | fn thead_cell_inner(&self) -> String { 31 | "flex items-center after:content-[--sort-icon] after:pl-1 after:opacity-40 before:content-[--sort-priority] before:order-last before:pl-0.5 before:font-light before:opacity-40".to_string() 32 | } 33 | 34 | fn row(&self, row_index: usize, selected: bool, template_classes: &str) -> String { 35 | let bg_color = if row_index % 2 == 0 { 36 | if selected { 37 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 38 | } else { 39 | "bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800" 40 | } 41 | } else if selected { 42 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 43 | } else { 44 | "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700" 45 | }; 46 | 47 | format!( 48 | "{} {} {}", 49 | "border-b dark:border-gray-700", bg_color, template_classes 50 | ) 51 | } 52 | 53 | fn loading_cell(&self, _row_index: usize, _col_index: usize, prop_class: &str) -> String { 54 | format!("{} {}", "px-5 py-2", prop_class) 55 | } 56 | 57 | fn loading_cell_inner(&self, row_index: usize, _col_index: usize, prop_class: &str) -> String { 58 | let width = match row_index % 4 { 59 | 0 => "w-[calc(85%-2.5rem)]", 60 | 1 => "w-[calc(90%-2.5rem)]", 61 | 2 => "w-[calc(75%-2.5rem)]", 62 | _ => "w-[calc(60%-2.5rem)]", 63 | }; 64 | format!( 65 | "animate-pulse h-2 bg-gray-200 rounded-full dark:bg-gray-700 inline-block align-middle {} {}", 66 | width, prop_class 67 | ) 68 | } 69 | 70 | fn cell(&self, template_classes: &str) -> String { 71 | format!( 72 | "{} {}", 73 | "px-5 py-2 whitespace-nowrap overflow-hidden text-ellipsis", template_classes 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/serverfn_sqlx/src/data_provider.rs: -------------------------------------------------------------------------------- 1 | use crate::classes::ClassesPreset; 2 | use leptos::prelude::*; 3 | use leptos_struct_table::*; 4 | use serde::{Deserialize, Serialize}; 5 | #[cfg(feature = "ssr")] 6 | use sqlx::{QueryBuilder, Row}; 7 | use std::collections::VecDeque; 8 | use std::ops::Range; 9 | 10 | #[derive(TableRow, Clone, Serialize, Deserialize)] 11 | #[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] 12 | #[table(sortable, classes_provider = ClassesPreset)] 13 | pub struct Customer { 14 | pub customer_id: String, 15 | pub first_name: String, 16 | pub last_name: String, 17 | pub company: String, 18 | pub city: String, 19 | pub country: String, 20 | pub phone: String, 21 | pub email: String, 22 | pub website: String, 23 | } 24 | 25 | #[derive(Clone, Debug, Serialize, Deserialize)] 26 | pub struct CustomerQuery { 27 | #[serde(default)] 28 | sort: VecDeque<(usize, ColumnSort)>, 29 | range: Range<usize>, 30 | name: String, 31 | } 32 | 33 | #[server] 34 | pub async fn list_customers(query: CustomerQuery) -> Result<Vec<Customer>, ServerFnError<String>> { 35 | use crate::database::get_db; 36 | 37 | let CustomerQuery { sort, range, name } = query; 38 | 39 | let mut query = QueryBuilder::new("SELECT customer_id, first_name, last_name, company, city, country, phone, email, website FROM customers "); 40 | if !name.is_empty() { 41 | query.push("WHERE first_name LIKE concat('%', "); 42 | query.push_bind(&name); 43 | query.push(", '%') OR last_name LIKE concat('%', "); 44 | query.push_bind(&name); 45 | query.push(", '%') OR company LIKE concat('%', "); 46 | query.push_bind(&name); 47 | query.push(", '%') "); 48 | } 49 | 50 | if let Some(order) = Customer::sorting_to_sql(&sort) { 51 | query.push(order); 52 | } 53 | 54 | query.push(" LIMIT "); 55 | query.push_bind(range.len() as i64); 56 | query.push(" OFFSET "); 57 | query.push_bind(range.start as i64); 58 | 59 | query 60 | .build_query_as::<Customer>() 61 | .fetch_all(get_db()) 62 | .await 63 | .map_err(|e| ServerFnError::WrappedServerError(format!("{e:?}"))) 64 | } 65 | 66 | #[server] 67 | pub async fn customer_count() -> Result<usize, ServerFnError<String>> { 68 | use crate::database::get_db; 69 | 70 | let count: i64 = sqlx::query("SELECT COUNT(*) FROM customers") 71 | .fetch_one(get_db()) 72 | .await 73 | .map_err(|err| ServerFnError::WrappedServerError(format!("{err:?}")))? 74 | .get(0); 75 | 76 | Ok(count as usize) 77 | } 78 | 79 | #[derive(Default)] 80 | pub struct CustomerTableDataProvider { 81 | sort: VecDeque<(usize, ColumnSort)>, 82 | pub name: RwSignal<String>, 83 | } 84 | 85 | impl TableDataProvider<Customer> for CustomerTableDataProvider { 86 | async fn get_rows(&self, range: Range<usize>) -> Result<(Vec<Customer>, Range<usize>), String> { 87 | list_customers(CustomerQuery { 88 | name: self.name.get_untracked().trim().to_string(), 89 | sort: self.sort.clone(), 90 | range: range.clone(), 91 | }) 92 | .await 93 | .map(|rows| { 94 | let len = rows.len(); 95 | (rows, range.start..range.start + len) 96 | }) 97 | .map_err(|e| format!("{e:?}")) 98 | } 99 | 100 | async fn row_count(&self) -> Option<usize> { 101 | customer_count().await.ok() 102 | } 103 | 104 | fn set_sorting(&mut self, sorting: &VecDeque<(usize, ColumnSort)>) { 105 | self.sort = sorting.clone(); 106 | } 107 | 108 | fn track(&self) { 109 | self.name.track(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /examples/serverfn_sqlx/src/database.rs: -------------------------------------------------------------------------------- 1 | static DB: std::sync::OnceLock<sqlx::SqlitePool> = std::sync::OnceLock::new(); 2 | 3 | pub async fn init_db() { 4 | let pool = sqlx::SqlitePool::connect("sqlite:db.sqlite3") 5 | .await 6 | .expect("Could not make pool."); 7 | let _ = DB.set(pool); 8 | } 9 | 10 | pub fn get_db<'a>() -> &'a sqlx::SqlitePool { 11 | DB.get().expect("database unitialized") 12 | } 13 | -------------------------------------------------------------------------------- /examples/serverfn_sqlx/src/handlers.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | body::Body, 3 | http::{Request, Response, StatusCode, Uri}, 4 | response::IntoResponse, 5 | }; 6 | use tower::ServiceExt; 7 | use tower_http::services::ServeDir; 8 | 9 | pub async fn file_handler(uri: Uri) -> Result<Response<Body>, (StatusCode, String)> { 10 | let res = get_static_file(uri.clone(), "/pkg").await?; 11 | 12 | if res.status() == StatusCode::NOT_FOUND { 13 | // try with `.html` 14 | // TODO: handle if the Uri has query parameters 15 | match format!("{}.html", uri).parse() { 16 | Ok(uri_html) => get_static_file(uri_html, "/pkg").await, 17 | Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())), 18 | } 19 | } else { 20 | Ok(res) 21 | } 22 | } 23 | 24 | pub async fn get_static_file_handler(uri: Uri) -> Result<Response<Body>, (StatusCode, String)> { 25 | let res = get_static_file(uri.clone(), "/static").await?; 26 | 27 | if res.status() == StatusCode::NOT_FOUND { 28 | Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())) 29 | } else { 30 | Ok(res) 31 | } 32 | } 33 | 34 | async fn get_static_file(uri: Uri, base: &str) -> Result<Response<Body>, (StatusCode, String)> { 35 | let req = Request::builder().uri(&uri).body(Body::empty()).unwrap(); 36 | 37 | // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` 38 | // When run normally, the root should be the crate root 39 | if base == "/static" { 40 | match ServeDir::new("./static").oneshot(req).await { 41 | Ok(res) => Ok(res.into_response()), 42 | Err(err) => Err(( 43 | StatusCode::INTERNAL_SERVER_ERROR, 44 | format!("Something went wrong: {}", err), 45 | )), 46 | } 47 | } else if base == "/pkg" { 48 | match ServeDir::new("./pkg").oneshot(req).await { 49 | Ok(res) => Ok(res.into_response()), 50 | Err(err) => Err(( 51 | StatusCode::INTERNAL_SERVER_ERROR, 52 | format!("Something went wrong: {}", err), 53 | )), 54 | } 55 | } else { 56 | Err((StatusCode::NOT_FOUND, "Not Found".to_string())) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/serverfn_sqlx/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod classes; 3 | mod data_provider; 4 | #[cfg(feature = "ssr")] 5 | pub mod database; 6 | 7 | pub use crate::app::App; 8 | use leptos::prelude::*; 9 | use leptos_meta::MetaTags; 10 | 11 | pub fn shell(options: LeptosOptions) -> impl IntoView { 12 | view! { 13 | <!DOCTYPE html> 14 | <html lang="en"> 15 | <head> 16 | <meta charset="utf-8"/> 17 | <meta name="viewport" content="width=device-width, initial-scale=1"/> 18 | <AutoReload options=options.clone() /> 19 | <HydrationScripts options/> 20 | <MetaTags/> 21 | </head> 22 | <body> 23 | <App/> 24 | </body> 25 | </html> 26 | } 27 | } 28 | 29 | #[cfg(feature = "hydrate")] 30 | #[wasm_bindgen::prelude::wasm_bindgen] 31 | pub fn hydrate() { 32 | console_error_panic_hook::set_once(); 33 | hydrate_body(App); 34 | } 35 | -------------------------------------------------------------------------------- /examples/serverfn_sqlx/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "ssr")] 2 | #[tokio::main] 3 | async fn main() { 4 | use axum::Router; 5 | use leptos::config::get_configuration; 6 | use leptos_axum::{generate_route_list, LeptosRoutes}; 7 | use serverfn_sqlx::{shell, App}; 8 | 9 | serverfn_sqlx::database::init_db().await; 10 | 11 | let conf = get_configuration(Some("Cargo.toml")).unwrap(); 12 | let leptos_options = conf.leptos_options; 13 | let addr = leptos_options.site_addr; 14 | let routes = generate_route_list(App); 15 | 16 | // build our application with a route 17 | let app = Router::new() 18 | .leptos_routes(&leptos_options, routes, { 19 | let leptos_options = leptos_options.clone(); 20 | move || shell(leptos_options.clone()) 21 | }) 22 | .fallback(leptos_axum::file_and_error_handler(shell)) 23 | .with_state(leptos_options); 24 | 25 | let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); 26 | println!("listening on http://{}", &addr); 27 | axum::serve(listener, app.into_make_service()) 28 | .await 29 | .unwrap(); 30 | } 31 | 32 | #[cfg(not(feature = "ssr"))] 33 | pub fn main() { 34 | // no client-side main function 35 | // unless we want this to work with e.g., Trunk for a purely client-side app 36 | // see lib.rs for hydration function instead 37 | } 38 | -------------------------------------------------------------------------------- /examples/serverfn_sqlx/style/output.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synphonyte/leptos-struct-table/41c10e3ba36b606760ae1b8a447d53f05781cb9f/examples/serverfn_sqlx/style/output.css -------------------------------------------------------------------------------- /examples/serverfn_sqlx/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const colors = require( 'tailwindcss/colors' ); 3 | 4 | module.exports = { 5 | content: { 6 | files: [ '*.html', './src/**/*.rs' ], 7 | }, 8 | darkMode: 'class', 9 | plugins: [ 10 | require( '@tailwindcss/forms' ), 11 | ], 12 | }; -------------------------------------------------------------------------------- /examples/simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"] } 8 | leptos-struct-table = { path = "../..", features = ["chrono", "uuid", "time"] } 9 | chrono = { version = "0.4", features = [] } 10 | console_error_panic_hook = "0.1" 11 | time= { version="0.3" } 12 | uuid = { version="1.8", features=["v4"]} 13 | console_log = "1" 14 | log = "0.4" 15 | getrandom = { version = "0.2", features = ["js"] } 16 | 17 | [dev-dependencies] 18 | wasm-bindgen = "0.2" 19 | wasm-bindgen-test = "0.3.0" 20 | web-sys = "0.3" 21 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | ### A simple table example with just local data stored as `Vec<Book>`. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) 6 | as well as the wasm32-unknown-unknown target: 7 | 8 | ```bash 9 | cargo install trunk 10 | rustup target add wasm32-unknown-unknown 11 | ``` 12 | 13 | Then, to run this example, execute in a terminal: 14 | 15 | ```bash 16 | trunk serve --open 17 | ``` -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head></head> 4 | <body></body> 5 | </html> 6 | -------------------------------------------------------------------------------- /examples/simple/src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | //! Simple showcase example. 3 | 4 | use ::chrono::NaiveDate; 5 | use ::time::Date; 6 | use ::uuid::Uuid; 7 | use leptos::prelude::*; 8 | use leptos_struct_table::*; 9 | 10 | /// This generates the component BookTable 11 | #[derive(TableRow, Clone)] 12 | #[table(sortable, impl_vec_data_provider)] 13 | pub struct Book { 14 | /// Id of the entry. 15 | pub id: Uuid, 16 | /// Title of the book. 17 | pub title: String, 18 | /// Author of the book. 19 | pub author: String, 20 | /// Date when book has been published. 21 | pub publish_date: Option<NaiveDate>, 22 | /// Date when book was read 23 | pub read_date: Option<Date>, 24 | /// Description of the book. Optional. 25 | #[table(none_value = "-")] 26 | pub description: Option<String>, 27 | /// Example on hidden member. 28 | #[table(skip)] 29 | pub hidden_field: String, 30 | } 31 | 32 | fn main() { 33 | _ = console_log::init_with_level(log::Level::Debug); 34 | console_error_panic_hook::set_once(); 35 | 36 | mount_to_body(|| { 37 | let rows = vec![ 38 | Book { 39 | id: Uuid::new_v4(), 40 | title: "The Great Gatsby".to_string(), 41 | author: "F. Scott Fitzgerald".to_string(), 42 | publish_date: Some(NaiveDate::from_ymd_opt(1925, 4, 10).unwrap()), 43 | read_date: Some(Date::from_calendar_date(2024, ::time::Month::January, 2).unwrap()), 44 | description: Some( 45 | "A story of wealth, love, and the American Dream in the 1920s.".to_string(), 46 | ), 47 | hidden_field: "hidden".to_string(), 48 | }, 49 | Book { 50 | id: Uuid::new_v4(), 51 | title: "The Grapes of Wrath".to_string(), 52 | author: "John Steinbeck".to_string(), 53 | publish_date: Some(NaiveDate::from_ymd_opt(1939, 4, 14).unwrap()), 54 | read_date: None, 55 | description: None, 56 | hidden_field: "not visible in the table".to_string(), 57 | }, 58 | Book { 59 | id: Uuid::new_v4(), 60 | title: "Nineteen Eighty-Four".to_string(), 61 | author: "George Orwell".to_string(), 62 | publish_date: Some(NaiveDate::from_ymd_opt(1949, 6, 8).unwrap()), 63 | read_date: None, 64 | description: None, 65 | hidden_field: "hidden".to_string(), 66 | }, 67 | Book { 68 | id: Uuid::new_v4(), 69 | title: "Ulysses".to_string(), 70 | author: "James Joyce".to_string(), 71 | publish_date: Some(NaiveDate::from_ymd_opt(1922, 2, 2).unwrap()), 72 | read_date: None, 73 | description: None, 74 | hidden_field: "hidden".to_string(), 75 | }, 76 | ]; 77 | 78 | view! { 79 | <table> 80 | <TableContent rows=rows scroll_container="html" /> 81 | </table> 82 | } 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /examples/tailwind/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tailwind" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.7", features = ["csr"]} 8 | leptos-struct-table = { path = "../..", features = ["chrono"] } 9 | chrono = { version = "0.4" } 10 | console_error_panic_hook = "0.1" 11 | console_log = "1" 12 | log = "0.4" 13 | 14 | [dev-dependencies] 15 | wasm-bindgen = "0.2" 16 | wasm-bindgen-test = "0.3.0" 17 | web-sys = "0.3" -------------------------------------------------------------------------------- /examples/tailwind/README.md: -------------------------------------------------------------------------------- 1 | ### A simple table example with just local data stored as `Vec<Book>` Uses the Tailwind class provider. 2 | 3 | To make this example work, you must download / fork the whole repo because this is in the Cargo.toml: `leptos-struct-table = { path = "../.." }`. 4 | 5 | The way Tailwind works, is to scan the classes in the code. Due to this it is 6 | recommended to copy the file `src/class_providers/tailwind.rs` into your project as done in this example. 7 | 8 | If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation) 9 | as well as the wasm32-unknown-unknown target: 10 | 11 | ```bash 12 | cargo install trunk 13 | npm install -D tailwindcss 14 | rustup target add wasm32-unknown-unknown 15 | ``` 16 | 17 | Then, open two terminals. In the first one, run: 18 | 19 | ``` 20 | npx tailwindcss -i ./input.css -o ./style/output.css --watch 21 | ``` 22 | 23 | In the second one, run: 24 | 25 | ```bash 26 | trunk serve --open 27 | ``` -------------------------------------------------------------------------------- /examples/tailwind/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <link data-trunk rel="css" href="style/output.css"> 5 | </head> 6 | <body></body> 7 | </html> 8 | -------------------------------------------------------------------------------- /examples/tailwind/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /examples/tailwind/src/main.rs: -------------------------------------------------------------------------------- 1 | mod tailwind; 2 | 3 | use ::chrono::NaiveDate; 4 | use leptos::prelude::*; 5 | use leptos_struct_table::*; 6 | use tailwind::TailwindClassesPreset; 7 | 8 | // This generates the component BookTable 9 | #[derive(TableRow, Clone)] 10 | #[table( 11 | sortable, 12 | classes_provider = "TailwindClassesPreset", 13 | impl_vec_data_provider 14 | )] 15 | pub struct Book { 16 | pub id: u32, 17 | pub title: String, 18 | pub author: String, 19 | #[table( 20 | cell_class = "text-red-600 dark:text-red-400", 21 | head_class = "text-red-700 dark:text-red-300" 22 | )] 23 | pub publish_date: NaiveDate, 24 | } 25 | 26 | fn main() { 27 | _ = console_log::init_with_level(log::Level::Debug); 28 | console_error_panic_hook::set_once(); 29 | 30 | mount_to_body(|| { 31 | let rows = vec![ 32 | Book { 33 | id: 1, 34 | title: "The Great Gatsby".to_string(), 35 | author: "F. Scott Fitzgerald".to_string(), 36 | publish_date: NaiveDate::from_ymd_opt(1925, 4, 10).unwrap(), 37 | }, 38 | Book { 39 | id: 2, 40 | title: "The Grapes of Wrath".to_string(), 41 | author: "John Steinbeck".to_string(), 42 | publish_date: NaiveDate::from_ymd_opt(1939, 4, 14).unwrap(), 43 | }, 44 | Book { 45 | id: 3, 46 | title: "Nineteen Eighty-Four".to_string(), 47 | author: "George Orwell".to_string(), 48 | publish_date: NaiveDate::from_ymd_opt(1949, 6, 8).unwrap(), 49 | }, 50 | Book { 51 | id: 4, 52 | title: "Ulysses".to_string(), 53 | author: "James Joyce".to_string(), 54 | publish_date: NaiveDate::from_ymd_opt(1922, 2, 2).unwrap(), 55 | }, 56 | ]; 57 | 58 | view! { 59 | <div class="rounded-md overflow-clip m-10 border dark:border-gray-700 float-left".to_string()> 60 | <table class="text-sm text-left text-gray-500 dark:text-gray-400 mb-[-1px]"> 61 | <TableContent rows=rows scroll_container="html" /> 62 | </table> 63 | </div> 64 | } 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /examples/tailwind/src/tailwind.rs: -------------------------------------------------------------------------------- 1 | use crate::{ColumnSort, TableClassesProvider}; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct TailwindClassesPreset; 5 | 6 | impl TableClassesProvider for TailwindClassesPreset { 7 | fn new() -> Self { 8 | Self 9 | } 10 | 11 | fn thead_row(&self, template_classes: &str) -> String { 12 | format!( 13 | "{} {}", 14 | "text-xs text-gray-700 uppercase bg-gray-200 dark:bg-gray-700 dark:text-gray-300", 15 | template_classes 16 | ) 17 | } 18 | 19 | fn thead_cell(&self, sort: ColumnSort, template_classes: &str) -> String { 20 | let sort_class = match sort { 21 | ColumnSort::None => "", 22 | _ => "text-black dark:text-white", 23 | }; 24 | 25 | format!( 26 | "cursor-pointer px-5 py-2 {} {}", 27 | sort_class, template_classes 28 | ) 29 | } 30 | 31 | fn thead_cell_inner(&self) -> String { 32 | "flex items-center after:content-[--sort-icon] after:pl-1 after:opacity-40 before:content-[--sort-priority] before:order-last before:pl-0.5 before:font-light before:opacity-40".to_string() 33 | } 34 | 35 | fn row(&self, row_index: usize, selected: bool, template_classes: &str) -> String { 36 | let bg_color = if row_index % 2 == 0 { 37 | if selected { 38 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 39 | } else { 40 | "bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800" 41 | } 42 | } else if selected { 43 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 44 | } else { 45 | "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700" 46 | }; 47 | 48 | format!( 49 | "{} {} {}", 50 | "border-b dark:border-gray-700", bg_color, template_classes 51 | ) 52 | } 53 | 54 | fn loading_cell(&self, _row_index: usize, _col_index: usize, prop_class: &str) -> String { 55 | format!("{} {}", "px-5 py-2", prop_class) 56 | } 57 | 58 | fn loading_cell_inner(&self, row_index: usize, _col_index: usize, prop_class: &str) -> String { 59 | let width = match row_index % 4 { 60 | 0 => "w-[calc(85%-2.5rem)]", 61 | 1 => "w-[calc(90%-2.5rem)]", 62 | 2 => "w-[calc(75%-2.5rem)]", 63 | _ => "w-[calc(60%-2.5rem)]", 64 | }; 65 | format!( 66 | "animate-pulse h-2 bg-gray-200 rounded-full dark:bg-gray-700 inline-block align-middle {} {}", 67 | width, prop_class 68 | ) 69 | } 70 | 71 | fn cell(&self, template_classes: &str) -> String { 72 | format!("{} {}", "px-5 py-2", template_classes) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/tailwind/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: { 4 | files: ["*.html", "./src/**/*.rs"], 5 | }, 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } -------------------------------------------------------------------------------- /hero.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synphonyte/leptos-struct-table/41c10e3ba36b606760ae1b8a447d53f05781cb9f/hero.afdesign -------------------------------------------------------------------------------- /hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synphonyte/leptos-struct-table/41c10e3ba36b606760ae1b8a447d53f05781cb9f/hero.webp -------------------------------------------------------------------------------- /src/cell_value.rs: -------------------------------------------------------------------------------- 1 | use leptos::prelude::*; 2 | 3 | #[derive(Default, Clone, Copy)] 4 | pub struct NumberRenderOptions { 5 | /// Specifies the number of digits to display after the decimal point 6 | pub precision: Option<usize>, 7 | } 8 | 9 | /// A value that can be rendered as part of a table, required for types if the [`crate::DefaultTableCellRenderer()`] is used 10 | pub trait CellValue<M: ?Sized = ()> { 11 | /// Formatting options for this cell value type, needs to implement default and have public named fields, 12 | /// the empty tuple: () is fine if no formatting options can be accepted. 13 | type RenderOptions: Default + Clone + Send + Sync + 'static; 14 | 15 | /// This is called to actually render the value. The parameter `options` is filled by the `#[table(format(...))]` macro attribute or `Default::default()` if omitted. 16 | fn render_value(self, options: Self::RenderOptions) -> impl IntoView; 17 | } 18 | 19 | impl<V> CellValue<()> for V 20 | where 21 | V: IntoView, 22 | { 23 | type RenderOptions = (); 24 | 25 | fn render_value(self, _options: Self::RenderOptions) -> impl IntoView { 26 | self 27 | } 28 | } 29 | 30 | macro_rules! viewable_primitive { 31 | ($($child_type:ty),* $(,)?) => { 32 | $( 33 | impl CellValue<$child_type> for $child_type { 34 | type RenderOptions = (); 35 | 36 | #[inline(always)] 37 | fn render_value(self, _options: Self::RenderOptions) -> impl IntoView { 38 | self.to_string() 39 | } 40 | } 41 | )* 42 | }; 43 | } 44 | 45 | viewable_primitive![ 46 | &String, 47 | char, 48 | bool, 49 | std::net::IpAddr, 50 | std::net::SocketAddr, 51 | std::net::SocketAddrV4, 52 | std::net::SocketAddrV6, 53 | std::net::Ipv4Addr, 54 | std::net::Ipv6Addr, 55 | std::char::ToUppercase, 56 | std::char::ToLowercase, 57 | std::num::NonZeroI8, 58 | std::num::NonZeroU8, 59 | std::num::NonZeroI16, 60 | std::num::NonZeroU16, 61 | std::num::NonZeroI32, 62 | std::num::NonZeroU32, 63 | std::num::NonZeroI64, 64 | std::num::NonZeroU64, 65 | std::num::NonZeroI128, 66 | std::num::NonZeroU128, 67 | std::num::NonZeroIsize, 68 | std::num::NonZeroUsize, 69 | std::panic::Location<'_>, 70 | ]; 71 | 72 | macro_rules! viewable_number_primitive { 73 | ($($child_type:ty),* $(,)?) => { 74 | $( 75 | impl CellValue<$child_type> for $child_type { 76 | type RenderOptions = NumberRenderOptions; 77 | 78 | #[inline(always)] 79 | fn render_value(self, options: Self::RenderOptions) -> impl IntoView { 80 | if let Some(value) = options.precision.as_ref() { 81 | view! { 82 | <>{format!("{:.value$}", self)}</> 83 | } 84 | } 85 | else { 86 | view! { 87 | <>{self.to_string()}</> 88 | } 89 | } 90 | } 91 | } 92 | )* 93 | }; 94 | } 95 | 96 | viewable_number_primitive![ 97 | usize, u8, u16, u32, u64, u128, isize, i8, i16, i32, i64, i128, f32, f64, 98 | ]; 99 | -------------------------------------------------------------------------------- /src/chrono.rs: -------------------------------------------------------------------------------- 1 | //! Support for [::chrono] crate. 2 | 3 | use crate::*; 4 | use ::chrono::{NaiveDate, NaiveDateTime, NaiveTime}; 5 | use leptos::prelude::*; 6 | 7 | #[derive(Clone, Default)] 8 | pub struct RenderChronoOptions { 9 | /// Specifies a format string, See [`::chrono::format::strftime`] for more information. 10 | pub string: Option<String>, 11 | } 12 | 13 | macro_rules! chrono_cell_value_impl { 14 | ( 15 | $(#[$outer:meta])* 16 | $ty:ty 17 | ) => { 18 | $(#[$outer])* 19 | impl CellValue<$ty> for $ty { 20 | type RenderOptions = RenderChronoOptions; 21 | 22 | fn render_value(self, options: Self::RenderOptions) -> impl IntoView { 23 | if let Some(value) = options.string.as_ref() { 24 | self.format(&value).to_string() 25 | } else { 26 | self.to_string() 27 | } 28 | } 29 | } 30 | }; 31 | } 32 | 33 | chrono_cell_value_impl!( 34 | /// Implementation for [`NaiveDate`] to work with the [`TableRow`] derive and the [`DefaultTableCellRenderer`] 35 | /// ``` 36 | /// # use leptos_struct_table::*; 37 | /// # use leptos::prelude::*; 38 | /// # use ::chrono::NaiveDate; 39 | /// #[derive(TableRow, Clone)] 40 | /// #[table] 41 | /// struct SomeStruct { 42 | /// #[table(format(string = "%Y-%m-%d"))] 43 | /// my_field: NaiveDate 44 | /// } 45 | /// ``` 46 | NaiveDate 47 | ); 48 | 49 | chrono_cell_value_impl!( 50 | /// Implementation for [`NaiveDateTime`] to work with the [`TableRow`] derive and the [`DefaultTableCellRenderer`] 51 | /// ``` 52 | /// # use leptos_struct_table::*; 53 | /// # use leptos::prelude::*; 54 | /// # use ::chrono::NaiveDateTime; 55 | /// #[derive(TableRow, Clone)] 56 | /// #[table] 57 | /// struct SomeStruct { 58 | /// #[table(format(string = "%Y-%m-%d %H:%M:%S"))] 59 | /// my_field: NaiveDateTime 60 | /// } 61 | /// ``` 62 | NaiveDateTime 63 | ); 64 | 65 | chrono_cell_value_impl!( 66 | /// Implementation for [`NaiveTime`] to work with the [`TableRow`] derive and the [`DefaultTableCellRenderer`] 67 | /// ``` 68 | /// # use leptos_struct_table::*; 69 | /// # use leptos::prelude::*; 70 | /// # use ::chrono::NaiveTime; 71 | /// #[derive(TableRow, Clone)] 72 | /// #[table] 73 | /// struct SomeStruct { 74 | /// #[table(format(string = "%H:%M:%S"))] 75 | /// my_field: NaiveTime 76 | /// } 77 | /// ``` 78 | NaiveTime 79 | ); 80 | -------------------------------------------------------------------------------- /src/class_providers/bootstrap.rs: -------------------------------------------------------------------------------- 1 | use crate::TableClassesProvider; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct BootstrapClassesPreset; 5 | 6 | impl TableClassesProvider for BootstrapClassesPreset { 7 | fn new() -> Self { 8 | Self 9 | } 10 | 11 | fn row(&self, _: usize, selected: bool, template_classes: &str) -> String { 12 | let active = if selected { "table-active" } else { "" }; 13 | 14 | format!("{} {}", active, template_classes) 15 | } 16 | 17 | // TODO : skeleton loading 18 | } 19 | -------------------------------------------------------------------------------- /src/class_providers/mod.rs: -------------------------------------------------------------------------------- 1 | mod bootstrap; 2 | mod tailwind; 3 | 4 | use crate::ColumnSort; 5 | pub use bootstrap::*; 6 | pub use tailwind::*; 7 | 8 | /// A trait for providing classes for the table. 9 | pub trait TableClassesProvider { 10 | /// Create a new instance of the class provider. 11 | fn new() -> Self; 12 | 13 | /// Get the class attribute for the thead. 14 | /// The `prop_class` parameter contains the classes specified in the 15 | /// `thead_class` prop of the [`TableContent`] component. 16 | fn thead(&self, prop_class: &str) -> String { 17 | prop_class.to_string() 18 | } 19 | 20 | /// Get the classes for the thead row. 21 | /// The `prop_class` parameter contains the classes specified in the 22 | /// `thead_row_class` prop of the [`TableContent`] component. 23 | fn thead_row(&self, prop_class: &str) -> String { 24 | prop_class.to_string() 25 | } 26 | 27 | /// Get the classes for the thead cells. 28 | /// The `sort` parameter contains the sort state of the column. 29 | /// The `macro_class` parameter contains the classes specified in the `head_class` macro attribute of the field. 30 | fn thead_cell(&self, sort: ColumnSort, macro_class: &str) -> String { 31 | format!("{} {}", sort.as_class(), macro_class) 32 | } 33 | 34 | /// Get the classes for the thead cells' inner element. 35 | fn thead_cell_inner(&self) -> String { 36 | "".to_string() 37 | } 38 | 39 | /// Get the classes for the tbody. 40 | /// The `prop_class` parameter contains the classes specified in the 41 | /// `tbody_class` prop of the [`TableContent`] component. 42 | fn tbody(&self, prop_class: &str) -> String { 43 | prop_class.to_string() 44 | } 45 | 46 | #[allow(unused_variables)] 47 | /// Get the classes for the body rows. 48 | /// The `row_index` parameter contains the index of the row. The first row has index 0. 49 | /// The `selected` parameter indicates whether the row is selected. 50 | /// The `prop_class` parameter contains the classes specified in the `row_class` 51 | /// prop of the [`TableContent`] component. 52 | fn row(&self, row_index: usize, selected: bool, prop_class: &str) -> String { 53 | prop_class.to_string() + if selected { " selected" } else { "" } 54 | } 55 | 56 | #[allow(unused_variables)] 57 | /// Get the classes for the elements inside of the cells of rows that are currently 58 | /// being loaded. 59 | /// The `prop_class` parameter contains the classes specified in the 60 | /// `loading_cell_class` prop of the [`TableContent`] component. 61 | fn loading_cell(&self, row_index: usize, col_index: usize, prop_class: &str) -> String { 62 | prop_class.to_string() 63 | } 64 | 65 | #[allow(unused_variables)] 66 | /// Get the classes for the elements inside of the cells of rows that are currently 67 | /// being loaded. Usually this will be some loading indicator like a sceleton bar. 68 | /// The `prop_class` parameter contains the classes specified in the 69 | /// `loading_cell_inner_class` prop of the [`TableContent`] component. 70 | fn loading_cell_inner(&self, row_index: usize, col_index: usize, prop_class: &str) -> String { 71 | prop_class.to_string() 72 | } 73 | 74 | /// Get the classes for the body cells. 75 | /// The `macro_class` parameter contains the classes specified in the `class` macro attribute of the field. 76 | fn cell(&self, macro_class: &str) -> String { 77 | macro_class.to_string() 78 | } 79 | } 80 | 81 | #[derive(Copy, Clone)] 82 | pub struct DummyTableClassesProvider; 83 | 84 | impl TableClassesProvider for DummyTableClassesProvider { 85 | fn new() -> Self { 86 | Self 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/class_providers/tailwind.rs: -------------------------------------------------------------------------------- 1 | use crate::{ColumnSort, TableClassesProvider}; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct TailwindClassesPreset; 5 | 6 | impl TableClassesProvider for TailwindClassesPreset { 7 | fn new() -> Self { 8 | Self 9 | } 10 | 11 | fn thead_row(&self, template_classes: &str) -> String { 12 | format!( 13 | "{} {}", 14 | "text-xs text-gray-700 uppercase bg-gray-200 dark:bg-gray-700 dark:text-gray-300", 15 | template_classes 16 | ) 17 | } 18 | 19 | fn thead_cell(&self, sort: ColumnSort, template_classes: &str) -> String { 20 | let sort_class = match sort { 21 | ColumnSort::None => "", 22 | _ => "text-black dark:text-white", 23 | }; 24 | 25 | format!( 26 | "cursor-pointer px-5 py-2 {} {}", 27 | sort_class, template_classes 28 | ) 29 | } 30 | 31 | fn thead_cell_inner(&self) -> String { 32 | "flex items-center after:content-[--sort-icon] after:pl-1 after:opacity-40 before:content-[--sort-priority] before:order-last before:pl-0.5 before:font-light before:opacity-40".to_string() 33 | } 34 | 35 | fn row(&self, row_index: usize, selected: bool, template_classes: &str) -> String { 36 | let bg_color = if row_index % 2 == 0 { 37 | if selected { 38 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 39 | } else { 40 | "bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800" 41 | } 42 | } else if selected { 43 | "bg-sky-300 text-gray-700 dark:bg-sky-700 dark:text-gray-400" 44 | } else { 45 | "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700" 46 | }; 47 | 48 | format!( 49 | "{} {} {}", 50 | "border-b dark:border-gray-700", bg_color, template_classes 51 | ) 52 | } 53 | 54 | fn loading_cell(&self, _row_index: usize, _col_index: usize, prop_class: &str) -> String { 55 | format!("{} {}", "px-5 py-2", prop_class) 56 | } 57 | 58 | fn loading_cell_inner(&self, row_index: usize, _col_index: usize, prop_class: &str) -> String { 59 | let width = match row_index % 4 { 60 | 0 => "w-[calc(85%-2.5rem)]", 61 | 1 => "w-[calc(90%-2.5rem)]", 62 | 2 => "w-[calc(75%-2.5rem)]", 63 | _ => "w-[calc(60%-2.5rem)]", 64 | }; 65 | format!( 66 | "animate-pulse h-2 bg-gray-200 rounded-full dark:bg-gray-700 inline-block align-middle {} {}", 67 | width, prop_class 68 | ) 69 | } 70 | 71 | fn cell(&self, template_classes: &str) -> String { 72 | format!("{} {}", "px-5 py-2", template_classes) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/components/cell.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | 3 | use crate::CellValue; 4 | use std::marker::PhantomData; 5 | 6 | use leptos::prelude::*; 7 | 8 | /// The default cell renderer. Uses the `<td>` element. 9 | #[component] 10 | pub fn DefaultTableCellRenderer<Row, T, M>( 11 | /// The class attribute for the cell element. Generated by the classes provider. 12 | class: String, 13 | /// The value to display. 14 | value: Signal<T>, 15 | /// Event handler called when the cell is changed. In this default renderer this will never happen. 16 | row: RwSignal<Row>, 17 | /// The index of the column. Starts at 0. 18 | index: usize, 19 | options: T::RenderOptions, 20 | #[prop(optional)] _marker: PhantomData<M>, 21 | ) -> impl IntoView 22 | where 23 | Row: Send + Sync + 'static, 24 | T: CellValue<M> + Send + Sync + Clone + 'static, 25 | M: 'static, 26 | { 27 | view! { 28 | <td class=class>{move || value.get().render_value(options.clone())}</td> 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/mod.rs: -------------------------------------------------------------------------------- 1 | mod cell; 2 | mod renderer_fn; 3 | mod row; 4 | mod table_content; 5 | mod tbody; 6 | mod thead; 7 | 8 | pub use cell::*; 9 | pub use row::*; 10 | pub use table_content::*; 11 | pub use tbody::*; 12 | pub use thead::*; 13 | 14 | #[macro_export] 15 | macro_rules! wrapper_render_fn { 16 | ( 17 | #[$doc_name:meta] 18 | $name:ident, 19 | $tag:ident, 20 | $(#[$additional_doc:meta])* 21 | ) => { 22 | /// Default 23 | #[$doc_name] 24 | /// renderer. Please note that this is **NOT** a `#[component]`. 25 | /// 26 | /// # Arguments 27 | /// 28 | /// * `content` - The content of the renderer. It's like the children of this view. 29 | /// * `class` - The class attribute that is passed to the root element 30 | $(#[$additional_doc])* 31 | #[allow(non_snake_case)] 32 | pub fn $name(content: AnyView, class: Signal<String>) -> impl IntoView { 33 | view! { 34 | <$tag class=class> 35 | {content} 36 | </$tag> 37 | } 38 | } 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/renderer_fn.rs: -------------------------------------------------------------------------------- 1 | macro_rules! renderer_fn { 2 | ( 3 | $name:ident<$($ty:ident),*>($($arg_name:ident: $arg_ty:ty),*) 4 | where $($clause:tt)* 5 | ) => { 6 | #[derive(Clone)] 7 | pub struct $name<$($ty),*> ( 8 | Arc<dyn Fn($($arg_ty),*) -> AnyView + Sync + Send + 'static>, 9 | ) 10 | where $($clause)*; 11 | 12 | impl<F, Ret, $($ty),*> From<F> for $name<$($ty),*> 13 | where 14 | F: Fn($($arg_ty),*) -> Ret + Sync + Send + 'static, 15 | Ret: IntoView + 'static, 16 | $($clause)* 17 | { 18 | fn from(f: F) -> Self { 19 | Self(Arc::new(move |$($arg_name),*| { 20 | f($($arg_name),*).into_any() 21 | })) 22 | } 23 | } 24 | 25 | impl<$($ty),*> $name <$($ty),*> 26 | where $($clause)* 27 | { 28 | pub fn run(&self, $($arg_name: $arg_ty),*) -> AnyView { 29 | (self.0)($($arg_name),*) 30 | } 31 | } 32 | }; 33 | 34 | ( 35 | $name:ident<$($ty:ident),*>($($arg_name:ident: $arg_ty:ty),*) 36 | default $default:ident 37 | where $($clause:tt)* 38 | ) => { 39 | renderer_fn!( 40 | $name<$($ty),*>($($arg_name: $arg_ty),*) 41 | where $($clause)* 42 | ); 43 | 44 | impl<$($ty),*> Default for $name<$($ty),*> 45 | where $($clause)* 46 | { 47 | fn default() -> Self { 48 | Self(Arc::new(move |$($arg_name),*| { 49 | $default($($arg_name),*).into_any() 50 | })) 51 | } 52 | } 53 | }; 54 | 55 | ( 56 | $name:ident<$($ty:ident),*>($($arg_name:ident: $arg_ty:ty),*) 57 | default $default:ident 58 | ) => { 59 | renderer_fn!( 60 | $name<$($ty),*>($($arg_name: $arg_ty),*) 61 | default $default 62 | where 63 | ); 64 | }; 65 | 66 | ( 67 | $name:ident($($arg_name:ident: $arg_ty:ty),*) 68 | default $default:ident 69 | ) => { 70 | renderer_fn!( 71 | $name<>($($arg_name: $arg_ty),*) 72 | default $default 73 | ); 74 | }; 75 | 76 | ( 77 | $name:ident<$($ty:ident),*>($($arg_name:ident: $arg_ty:ty),*) 78 | ) => { 79 | renderer_fn!( 80 | $name<$($ty),*>($($arg_name: $arg_ty),*) 81 | where 82 | ); 83 | }; 84 | 85 | ( 86 | $name:ident($($arg_name:ident: $arg_ty:ty),*) 87 | ) => { 88 | renderer_fn!( 89 | $name<>($($arg_name: $arg_ty),*) 90 | where 91 | ); 92 | }; 93 | } 94 | 95 | pub(crate) use renderer_fn; 96 | -------------------------------------------------------------------------------- /src/components/row.rs: -------------------------------------------------------------------------------- 1 | use crate::table_row::TableRow; 2 | use crate::EventHandler; 3 | use leptos::prelude::*; 4 | 5 | /// The default table row renderer. Uses the `<tr>` element. Please note that this 6 | /// is **NOT** a `#[component]`. 7 | #[allow(unused_variables)] 8 | pub fn DefaultTableRowRenderer<Row>( 9 | // The class attribute for the row element. Generated by the classes provider. 10 | class: Signal<String>, 11 | // The row to render. 12 | row: RwSignal<Row>, 13 | // The index of the row. Starts at 0 for the first body row. 14 | index: usize, 15 | // The selected state of the row. True, when the row is selected. 16 | selected: Signal<bool>, 17 | // Event handler callback when this row is selected 18 | on_select: EventHandler<web_sys::MouseEvent>, 19 | ) -> impl IntoView 20 | where 21 | Row: TableRow + 'static, 22 | { 23 | view! { 24 | <tr class=class on:click=move |mouse_event| on_select.run(mouse_event)> 25 | {TableRow::render_row(row, index)} 26 | </tr> 27 | } 28 | } 29 | 30 | /// The default row placeholder renderer which is just a div that is set to the 31 | /// appropriate height. This is used in place of rows that are not shown 32 | /// before and after the currently visible rows. 33 | pub fn DefaultRowPlaceholderRenderer(height: Signal<f64>) -> impl IntoView { 34 | view! { <tr style:height=move || format!("{}px", height.get()) style="display: block"></tr> } 35 | } 36 | 37 | /// The default error row renderer which just displays the error message when 38 | /// a row fails to load, i.e. when [`TableDataProvider::get_rows`] returns an `Err(..)`. 39 | #[allow(unused_variables)] 40 | pub fn DefaultErrorRowRenderer(err: String, index: usize, col_count: usize) -> impl IntoView { 41 | view! { <tr><td colspan=col_count>{err}</td></tr> } 42 | } 43 | 44 | /// The default loading row renderer which just displays a loading indicator. 45 | #[allow(unused_variables, unstable_name_collisions)] 46 | pub fn DefaultLoadingRowRenderer( 47 | class: Signal<String>, 48 | get_cell_class: Callback<(usize,), String>, 49 | get_inner_cell_class: Callback<(usize,), String>, 50 | index: usize, 51 | col_count: usize, 52 | ) -> impl IntoView { 53 | view! { 54 | <tr class=class> 55 | { 56 | (0..col_count).map(|col_index| view! { 57 | <td class=get_cell_class.run((col_index,))> 58 | <div class=get_inner_cell_class.run((col_index,))></div> 59 | " " 60 | </td> 61 | }).collect_view() 62 | } 63 | </tr> 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/tbody.rs: -------------------------------------------------------------------------------- 1 | use crate::BodyRef; 2 | use leptos::prelude::*; 3 | 4 | /// Default tbody renderer. Please note that this is **NOT** a `#[component]`. 5 | /// 6 | /// # Arguments 7 | /// 8 | /// * `content` - The content of the renderer. It's like the children of this view. 9 | /// * `class` - The class attribute that is passed to the root element 10 | /// * `node_ref` - The `NodeRef` referencing the root tbody element. 11 | /// 12 | /// This render function has to render exactly one root element. 13 | #[allow(non_snake_case)] 14 | pub fn DefaultTableBodyRenderer( 15 | content: impl IntoView, 16 | class: Signal<String>, 17 | body_ref: BodyRef, 18 | ) -> impl IntoView { 19 | view! { 20 | <tbody class=class use:body_ref> 21 | {content} 22 | </tbody> 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/thead.rs: -------------------------------------------------------------------------------- 1 | use crate::wrapper_render_fn; 2 | use crate::{ColumnSort, TableHeadEvent}; 3 | use leptos::prelude::*; 4 | 5 | wrapper_render_fn!( 6 | /// thead 7 | DefaultTableHeadRenderer, 8 | thead, 9 | ); 10 | 11 | wrapper_render_fn!( 12 | /// thead row 13 | DefaultTableHeadRowRenderer, 14 | tr, 15 | ); 16 | 17 | /// The default table header renderer. Renders roughly 18 | /// ```html 19 | /// <th> 20 | /// <span>Title</span> 21 | /// </th> 22 | /// ``` 23 | #[component] 24 | pub fn DefaultTableHeaderCellRenderer<F>( 25 | /// The class attribute for the head element. Generated by the classes provider. 26 | #[prop(into)] 27 | class: Signal<String>, 28 | /// The class attribute for the inner element. Generated by the classes provider. 29 | #[prop(into)] 30 | inner_class: String, 31 | /// The index of the column. Starts at 0 for the first column. The order of the columns is the same as the order of the fields in the struct. 32 | index: usize, 33 | /// The sort priority of the column. `None` if the column is not sorted. `0` means the column is the primary sort column. 34 | #[prop(into)] 35 | sort_priority: Signal<Option<usize>>, 36 | /// The sort direction of the column. See [`ColumnSort`]. 37 | #[prop(into)] 38 | sort_direction: Signal<ColumnSort>, 39 | /// The event handler for the click event. Has to be called with [`TableHeadEvent`]. 40 | on_click: F, 41 | children: Children, 42 | ) -> impl IntoView 43 | where 44 | F: Fn(TableHeadEvent) + 'static, 45 | { 46 | let style = default_th_sorting_style(sort_priority, sort_direction); 47 | 48 | view! { 49 | <th class=class 50 | on:click=move |mouse_event| on_click(TableHeadEvent { 51 | index, 52 | mouse_event, 53 | }) 54 | style=style 55 | > 56 | <span class=inner_class> 57 | {children()} 58 | </span> 59 | </th> 60 | } 61 | } 62 | 63 | /// You can use this function to implement your own custom table header cell renderer. 64 | /// 65 | /// See the implementation of [`DefaultTableHeaderCellRenderer`]. 66 | pub fn default_th_sorting_style( 67 | sort_priority: Signal<Option<usize>>, 68 | sort_direction: Signal<ColumnSort>, 69 | ) -> Signal<String> { 70 | Signal::derive(move || { 71 | let sort = match sort_direction.get() { 72 | ColumnSort::Ascending => "--sort-icon: '▲';", 73 | ColumnSort::Descending => "--sort-icon: '▼';", 74 | ColumnSort::None => "--sort-icon: '';", 75 | }; 76 | 77 | let priority = match sort_priority.get() { 78 | Some(priority) => format!("--sort-priority: '{}';", priority + 1), 79 | None => "--sort-priority: '';".to_string(), 80 | }; 81 | 82 | format!("{} {}", sort, &priority) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /src/data_provider.rs: -------------------------------------------------------------------------------- 1 | #![allow(async_fn_in_trait)] 2 | 3 | use crate::ColumnSort; 4 | use std::collections::VecDeque; 5 | use std::fmt::Debug; 6 | use std::ops::Range; 7 | 8 | /// The trait that provides data for the `<TableContent>` component. 9 | /// Anything that is passed to the `rows` prop must implement this trait. 10 | /// 11 | /// If you add `#[table(impl_vec_data_provider)]` to your row struct, 12 | /// this is automatically implemented for `Vec<Row>`. 13 | /// This way a simple list of items can be passed to the table. 14 | /// 15 | /// This is also automatically implemented for any struct that implements 16 | /// [`PaginatedTableDataProvider`] or [`ExactTableDataProvider`]. 17 | /// The first is a more convenient way of connecting to a paginated data source and the second is 18 | /// more convenient if you know you're always going to return exactly the requested range (except maybe 19 | /// at the end of the data). 20 | pub trait TableDataProvider<Row, Err: Debug = String> { 21 | /// If Some(...), data will be loaded in chunks of this size. This is useful for paginated data sources. 22 | /// If you have such a paginated data source, you probably want to implement `PaginatedTableDataProvider` 23 | /// instead of this trait. 24 | const CHUNK_SIZE: Option<usize> = None; 25 | 26 | /// Get all data rows for the table specified by the range. This method is called when the table is rendered. 27 | /// The range is determined by the visible rows and used to virtualize the table. 28 | /// The parameter `range` is only determined by visibility and may be out of bounds. It is the 29 | /// responsibility of the implementation to handle this case. Use [get_vec_range_clamped] to get a 30 | /// range that is clamped to the length of the vector. 31 | /// 32 | /// It returns a `Vec` of all rows loaded and the range that these rows cover. Depending on 33 | /// the data source you might not be able to load exactly the requested range; that's why 34 | /// the actual loaded range is returned in addition to the rows. You should always return 35 | /// at least the range that is requested or more. If you return less rows than requested, 36 | /// it is assumed that the data source is done and there are no more rows to load. 37 | /// 38 | /// In the case of an error the returned error `String` is going to be displayed in a 39 | /// in place of the failed rows. 40 | async fn get_rows(&self, range: Range<usize>) -> Result<(Vec<Row>, Range<usize>), Err>; 41 | 42 | /// The total number of rows in the table. Returns `None` if unknown (which is the default). 43 | async fn row_count(&self) -> Option<usize> { 44 | None 45 | } 46 | 47 | /// Set the sorting of the table. The sorting is a list of column names and the sort order sorted by priority. 48 | /// The first entry in the list is the most important one. 49 | /// The default implementation does nothing. 50 | /// For example: `[(0, ColumnSort::Ascending), (1, ColumnSort::Descending)]` 51 | /// will sort by name first and then by age. 52 | /// Please note that after calling this method, data will be reloaded through [`get_rows`](TableDataProvider::get_rows). 53 | #[allow(unused_variables)] 54 | fn set_sorting(&mut self, sorting: &VecDeque<(usize, ColumnSort)>) { 55 | // by default do nothing 56 | } 57 | 58 | /// Call `.track()` in this method on all signals that loading data relies on. 59 | /// For example a search of filters. Please check the [paginated_rest_datasource example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/paginated_rest_datasource/src/data_provider.rs) 60 | fn track(&self) { 61 | // by default do nothing 62 | } 63 | } 64 | 65 | /// A paginated data source. This is meant to provide a more convenient way 66 | /// of connecting to a paginated data source instead of implementing [`TableDataProvider`] directly. 67 | /// 68 | /// If you implement this for your struct, [`TableDataProvider`] is automatically implemented for you. 69 | /// 70 | /// > Please note that this is independent from using [`DisplayStrategy::Pagination`] with [`TableContent`]. 71 | /// > You do not have implement this trait if you're using pagination and you vice versa if you're not using pagination 72 | /// > you can still implement this trait. And in case if you use this trait together with pagination the 73 | /// > display row count can be different from the `PAGE_ROW_COUNT`. 74 | pub trait PaginatedTableDataProvider<Row, Err: Debug = String> { 75 | /// How many rows per page 76 | const PAGE_ROW_COUNT: usize; 77 | 78 | /// Get all data rows for the table specified by the page index (starts a 0). 79 | /// 80 | /// If you return less than `PAGE_ROW_COUNT` rows, it is assumed that the end of the 81 | /// data has been reached. 82 | async fn get_page(&self, page_index: usize) -> Result<Vec<Row>, Err>; 83 | 84 | /// The total number of rows in the table. Returns `None` if unknown (which is the default). 85 | /// 86 | /// By default this is computed from the [`page_count`] method. But if your data source 87 | /// tells you the number of rows instead of the number of pages you should override this method. 88 | async fn row_count(&self) -> Option<usize> { 89 | self.page_count().await.map(|pc| pc * Self::PAGE_ROW_COUNT) 90 | } 91 | 92 | /// The total number of pages in the data source. Returns `None` if unknown (which is the default). 93 | /// 94 | /// If your data source gives you the number of rows instead of the number of pages 95 | /// you should implement [`row_count`] instead of this method. 96 | async fn page_count(&self) -> Option<usize> { 97 | None 98 | } 99 | 100 | /// Same as [`TableDataProvider::set_sorting`] 101 | #[allow(unused_variables)] 102 | fn set_sorting(&mut self, sorting: &VecDeque<(usize, ColumnSort)>) { 103 | // by default do nothing 104 | } 105 | 106 | /// Same as [`TableDataProvider::track`] 107 | fn track(&self) { 108 | // by default do nothing 109 | } 110 | } 111 | 112 | impl<Row, Err, D> TableDataProvider<Row, Err> for D 113 | where 114 | D: PaginatedTableDataProvider<Row, Err>, 115 | Err: Debug, 116 | { 117 | const CHUNK_SIZE: Option<usize> = Some(D::PAGE_ROW_COUNT); 118 | 119 | async fn get_rows(&self, range: Range<usize>) -> Result<(Vec<Row>, Range<usize>), Err> { 120 | let Range { start, end } = range; 121 | 122 | debug_assert_eq!(start % D::PAGE_ROW_COUNT, 0); 123 | debug_assert_eq!(end - start, D::PAGE_ROW_COUNT); 124 | 125 | self.get_page(start / D::PAGE_ROW_COUNT).await.map(|rows| { 126 | let len = rows.len(); 127 | (rows, start..start + len) 128 | }) 129 | } 130 | 131 | async fn row_count(&self) -> Option<usize> { 132 | PaginatedTableDataProvider::<Row, Err>::row_count(self).await 133 | } 134 | 135 | fn set_sorting(&mut self, sorting: &VecDeque<(usize, ColumnSort)>) { 136 | PaginatedTableDataProvider::<Row, Err>::set_sorting(self, sorting) 137 | } 138 | 139 | fn track(&self) { 140 | PaginatedTableDataProvider::<Row, Err>::track(self) 141 | } 142 | } 143 | 144 | /// Return `vec[range.start..range.end]` where `range` is clamped to the length of `vec`. 145 | pub fn get_vec_range_clamped<T: Clone>(vec: &[T], range: Range<usize>) -> (Vec<T>, Range<usize>) { 146 | if vec.is_empty() { 147 | return (vec![], 0..0); 148 | } 149 | 150 | let start = range.start.min(vec.len() - 1); 151 | let end = range.end.min(vec.len()); 152 | 153 | let return_range = start..end; 154 | 155 | (vec[return_range.clone()].to_vec(), return_range) 156 | } 157 | -------------------------------------------------------------------------------- /src/display_strategy.rs: -------------------------------------------------------------------------------- 1 | use leptos::prelude::*; 2 | 3 | /// The display acceleration strategy. Defaults to `Virtualization`. 4 | #[derive(Copy, Clone, Default)] 5 | pub enum DisplayStrategy { 6 | /// Only visible rows (plus some extra) will be displayed but the scrollbar 7 | /// will seem as if all rows are there. 8 | /// 9 | /// If the data provider doesn't know how many rows there are (i.e. [`TableDataProvider::row_count`] 10 | /// returns `None`), this will be the same as `InfiniteScroll`. 11 | #[default] 12 | Virtualization, 13 | 14 | /// Only the amount of rows specified is shown. Once the user scrolls down, 15 | /// more rows will be loaded. The scrollbar handle will shrink progressively 16 | /// as more and more rows are loaded. 17 | InfiniteScroll, 18 | 19 | // TODO : LoadMore(usize), 20 | /// Only the amount of rows specified is shown at a time. You can use the 21 | /// `controller` to manipulate which page of rows is shown. 22 | /// Scrolling will have no effect on what rows are loaded. 23 | /// 24 | /// > Please note that this will work wether your data source implements 25 | /// > [`PaginatedTableDataProvider`] or [`TableDataProvider`] directly. 26 | /// > Also `row_count` can be different from `PaginatedTableDataProvider::PAGE_ROW_COUNT`. 27 | Pagination { 28 | row_count: usize, 29 | controller: PaginationController, 30 | }, 31 | } 32 | 33 | impl DisplayStrategy { 34 | pub(crate) fn set_row_count(&self, row_count: usize) { 35 | match self { 36 | Self::Pagination { 37 | row_count: page_row_count, 38 | controller, 39 | } => { 40 | controller 41 | .page_count_signal 42 | .set(Some(row_count / *page_row_count + 1)); 43 | } 44 | _ => { 45 | // do nothing 46 | } 47 | } 48 | } 49 | } 50 | 51 | /// Allows to control what page is displayed as well as reading the page count and current page 52 | #[derive(Copy, Clone)] 53 | pub struct PaginationController { 54 | /// The current page. The first page is `0`. 55 | pub current_page: RwSignal<usize>, 56 | page_count_signal: RwSignal<Option<usize>>, 57 | } 58 | 59 | impl Default for PaginationController { 60 | fn default() -> Self { 61 | Self { 62 | // the value here doesn't really matter. We'll react only to changes later 63 | current_page: RwSignal::new(0), 64 | page_count_signal: RwSignal::new(None), 65 | } 66 | } 67 | } 68 | 69 | impl PaginationController { 70 | /// Call this to go to the next page 71 | pub fn next(&self) { 72 | self.current_page.set(self.current_page.get_untracked() + 1); 73 | } 74 | 75 | /// Call this to go to the previous page 76 | pub fn previous(&self) { 77 | self.current_page 78 | .set(self.current_page.get_untracked().saturating_sub(1)); 79 | } 80 | 81 | /// Returns a `Signal` of the page count once loaded. Depending on your table data provider 82 | /// this might not be available and thus always be `None`. 83 | pub fn page_count(&self) -> Signal<Option<usize>> { 84 | self.page_count_signal.into() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use leptos::ev::MouseEvent; 2 | use leptos::prelude::*; 3 | use std::sync::Arc; 4 | 5 | /// The event provided to the `on_change` prop of the table component 6 | #[derive(Debug)] 7 | pub struct ChangeEvent<Row: Send + Sync + 'static> { 8 | /// The index of the table row that contains the cell that was changed. Starts at 0. 9 | pub row_index: usize, 10 | /// The the row that was changed. 11 | pub changed_row: Signal<Row>, 12 | } 13 | 14 | impl<Row: Send + Sync + 'static> Clone for ChangeEvent<Row> { 15 | fn clone(&self) -> Self { 16 | *self 17 | } 18 | } 19 | 20 | impl<Row: Send + Sync + 'static> Copy for ChangeEvent<Row> {} 21 | 22 | /// The event provided to the `on_selection_change` prop of the table component 23 | #[derive(Debug)] 24 | pub struct SelectionChangeEvent<Row: Send + Sync + 'static> { 25 | /// `true` is the row was selected, `false` if it was de-selected. 26 | pub selected: bool, 27 | /// The index of the row that was de-/selected. 28 | pub row_index: usize, 29 | /// The row that was de-/selected. 30 | pub row: Signal<Row>, 31 | } 32 | 33 | impl<Row: Send + Sync + 'static> Clone for SelectionChangeEvent<Row> { 34 | fn clone(&self) -> Self { 35 | *self 36 | } 37 | } 38 | 39 | impl<Row: Send + Sync + 'static> Copy for SelectionChangeEvent<Row> {} 40 | 41 | /// Event emitted when a table head cell is clicked. 42 | #[derive(Debug)] 43 | pub struct TableHeadEvent { 44 | /// The index of the column. Starts at 0 for the first column. 45 | /// The order of the columns is the same as the order of the fields in the struct. 46 | pub index: usize, 47 | /// The mouse event that triggered the event. 48 | pub mouse_event: MouseEvent, 49 | } 50 | 51 | macro_rules! impl_default_arc_fn { 52 | ( 53 | $(#[$meta:meta])* 54 | $name:ident<$($ty:ident),*>($($arg_name:ident: $arg_ty:ty),*) 55 | $(-> $ret_ty:ty)? 56 | $({ default $default_return:expr })? 57 | ) => { 58 | $(#[$meta])* 59 | #[derive(Clone)] 60 | pub struct $name<$($ty),*>(Arc<dyn Fn($($arg_ty),*) $(-> $ret_ty)? + Send + Sync>); 61 | 62 | impl<$($ty),*> Default for $name<$($ty),*> { 63 | fn default() -> Self { 64 | #[allow(unused_variables)] 65 | Self(Arc::new(|$($arg_name: $arg_ty),*| { 66 | $($default_return)? 67 | })) 68 | } 69 | } 70 | 71 | impl<F, $($ty),*> From<F> for $name<$($ty),*> 72 | where F: Fn($($arg_ty),*) $(-> $ret_ty)? + Send + Sync + 'static 73 | { 74 | fn from(f: F) -> Self { Self(Arc::new(f)) } 75 | } 76 | 77 | impl<$($ty),*> $name<$($ty),*> { 78 | pub fn run(&self, $($arg_name: $arg_ty),*) $(-> $ret_ty)? { 79 | (self.0)($($arg_name),*) 80 | } 81 | } 82 | } 83 | } 84 | 85 | impl_default_arc_fn!( 86 | /// New type wrapper of a closure that takes a parameter `T`. This allows the event handler props 87 | /// to be optional while being able to take a simple closure. 88 | EventHandler<T>(event: T) 89 | ); 90 | 91 | pub(crate) use impl_default_arc_fn; 92 | -------------------------------------------------------------------------------- /src/loaded_rows.rs: -------------------------------------------------------------------------------- 1 | use leptos::prelude::*; 2 | use std::ops::{Index, Range}; 3 | 4 | pub enum RowState<T: Send + Sync + 'static> { 5 | /// The row is not yet loaded and a placeholder is displayed if the row is visible in the viewport. 6 | Placeholder, 7 | /// The row is loading and a placeholder is displayed if the row is visible in the viewport. 8 | Loading, 9 | /// The row has been loaded. 10 | Loaded(RwSignal<T>), 11 | /// The row failed to load. This error is shown in the row if it's visible in the viewport. 12 | Error(String), 13 | } 14 | 15 | impl<T: Send + Sync + 'static> Clone for RowState<T> { 16 | fn clone(&self) -> Self { 17 | match self { 18 | RowState::Placeholder => RowState::Placeholder, 19 | RowState::Loading => RowState::Loading, 20 | RowState::Loaded(signal) => RowState::Loaded(*signal), 21 | RowState::Error(error) => RowState::Error(error.clone()), 22 | } 23 | } 24 | } 25 | 26 | impl<T: Send + Sync + 'static> std::fmt::Debug for RowState<T> { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | match self { 29 | RowState::Placeholder => write!(f, "Placeholder"), 30 | RowState::Loading => write!(f, "Loading"), 31 | RowState::Loaded(_) => write!(f, "Loaded"), 32 | RowState::Error(e) => write!(f, "Error({})", e), 33 | } 34 | } 35 | } 36 | 37 | /// This is basically a cache for rows and used by [`TableContent`] internally to track 38 | /// which rows are already loaded, which are still loading and which are missing. 39 | pub struct LoadedRows<T: Send + Sync + 'static> { 40 | rows: Vec<RowState<T>>, 41 | } 42 | 43 | impl<T: Send + Sync + 'static> LoadedRows<T> { 44 | pub fn new() -> Self { 45 | Self { rows: vec![] } 46 | } 47 | 48 | #[inline] 49 | pub fn len(&self) -> usize { 50 | self.rows.len() 51 | } 52 | 53 | #[inline] 54 | pub fn resize(&mut self, len: usize) { 55 | self.rows.resize(len, RowState::Placeholder); 56 | } 57 | 58 | pub fn write_loading(&mut self, range: Range<usize>) { 59 | if range.end > self.rows.len() { 60 | self.rows.resize(range.end, RowState::Placeholder); 61 | } 62 | 63 | for row in &mut self.rows[range] { 64 | *row = RowState::Loading; 65 | } 66 | } 67 | 68 | pub fn write_loaded( 69 | &mut self, 70 | loading_result: Result<(Vec<T>, Range<usize>), String>, 71 | missing_range: Range<usize>, 72 | ) { 73 | match loading_result { 74 | Ok((rows, range)) => { 75 | if range.end > self.rows.len() { 76 | self.rows.resize(range.end, RowState::Placeholder); 77 | } 78 | 79 | for (self_row, loaded_row) in self.rows[range].iter_mut().zip(rows) { 80 | *self_row = RowState::Loaded(RwSignal::new(loaded_row)); 81 | } 82 | } 83 | Err(error) => { 84 | let range = missing_range.start..missing_range.end.min(self.rows.len()); 85 | if range.start >= range.end { 86 | return; 87 | } 88 | 89 | for row in &mut self.rows[range] { 90 | *row = RowState::Error(error.clone()); 91 | } 92 | } 93 | } 94 | } 95 | 96 | #[inline] 97 | pub fn missing_range(&self, range: Range<usize>) -> Option<Range<usize>> { 98 | let do_load_predicate = |row| matches!(row, &RowState::Placeholder); 99 | 100 | let slice = &self.rows[range.clone()]; 101 | 102 | let start = slice.iter().position(do_load_predicate)?; 103 | let end = slice.iter().rposition(do_load_predicate)?; 104 | 105 | let start = start + range.start; 106 | let end = end + range.start + 1; 107 | 108 | Some(start..end) 109 | } 110 | 111 | #[inline] 112 | pub fn clear(&mut self) { 113 | self.rows.fill(RowState::Placeholder); 114 | } 115 | } 116 | 117 | impl<T: Sync + Send> Index<Range<usize>> for LoadedRows<T> { 118 | type Output = [RowState<T>]; 119 | 120 | #[inline] 121 | fn index(&self, index: Range<usize>) -> &Self::Output { 122 | &self.rows[index] 123 | } 124 | } 125 | 126 | impl<T: Send + Sync> Index<usize> for LoadedRows<T> { 127 | type Output = RowState<T>; 128 | 129 | #[inline] 130 | fn index(&self, index: usize) -> &Self::Output { 131 | &self.rows[index] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/reload_controller.rs: -------------------------------------------------------------------------------- 1 | use leptos::prelude::*; 2 | 3 | /// You can pass this to a [`TableContent`] component's `reload_controller` prop to trigger a reload. 4 | /// 5 | /// See the [paginated_rest_datasource example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/paginated_rest_datasource/src/main.rs) 6 | /// for how to use. 7 | #[derive(Copy, Clone)] 8 | pub struct ReloadController(Trigger); 9 | 10 | impl Default for ReloadController { 11 | fn default() -> Self { 12 | Self(Trigger::new()) 13 | } 14 | } 15 | 16 | impl ReloadController { 17 | pub fn reload(&self) { 18 | self.0.notify(); 19 | } 20 | 21 | pub fn track(&self) { 22 | self.0.track(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/row_reader.rs: -------------------------------------------------------------------------------- 1 | use crate::loaded_rows::RowState; 2 | use std::cell::RefCell; 3 | use std::rc::Rc; 4 | 5 | /// Allows you to read the cached state of rows from inside the table component which handles 6 | /// loading and caching automatically. 7 | #[derive(Clone)] 8 | pub struct RowReader<Row: Send + Sync + 'static> { 9 | pub(crate) get_loaded_rows: LoadedRowsGetter<Row>, 10 | } 11 | 12 | pub type LoadedRowsGetter<Row> = Rc<RefCell<Box<dyn Fn(usize) -> RowState<Row>>>>; 13 | 14 | impl<Row: Send + Sync + 'static> Default for RowReader<Row> { 15 | fn default() -> Self { 16 | Self { 17 | get_loaded_rows: Rc::new(RefCell::new(Box::new(|_| RowState::Placeholder))), 18 | } 19 | } 20 | } 21 | 22 | impl<Row: Send + Sync + 'static> RowReader<Row> { 23 | /// Returns the cached state of the row at the given index 24 | pub fn cached_row(&self, index: usize) -> RowState<Row> { 25 | (*self.get_loaded_rows.borrow())(index) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/rust_decimal.rs: -------------------------------------------------------------------------------- 1 | //! Support for [::rust_decimal] crate. 2 | use crate::*; 3 | use ::rust_decimal::Decimal; 4 | use leptos::prelude::*; 5 | 6 | #[derive(Clone, Default)] 7 | pub struct DecimalNumberRenderOptions { 8 | /// Specifies the number of digits to display after the decimal point 9 | pub precision: Option<usize>, 10 | } 11 | /// Implementation for [`Decimal`] to work with the [`TableRow`] derive and the [`DefaultTableCellRenderer`] 12 | /// ``` 13 | /// # use leptos_struct_table::*; 14 | /// # use leptos::prelude::*; 15 | /// # use ::rust_decimal::Decimal; 16 | /// #[derive(TableRow, Clone)] 17 | /// #[table] 18 | /// struct SomeStruct { 19 | /// #[table(format(precision = 2usize))] 20 | /// my_field: Decimal 21 | /// } 22 | /// ``` 23 | impl CellValue<Decimal> for Decimal { 24 | type RenderOptions = DecimalNumberRenderOptions; 25 | fn render_value(self, options: Self::RenderOptions) -> impl IntoView { 26 | if let Some(value) = options.precision.as_ref() { 27 | format!("{:.value$}", self) 28 | } else { 29 | self.to_string() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/selection.rs: -------------------------------------------------------------------------------- 1 | use leptos::prelude::*; 2 | use std::collections::HashSet; 3 | 4 | /// Type of selection together with the `RwSignal` to hold the selection 5 | #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] 6 | pub enum Selection { 7 | /// No selection possible (the default). 8 | #[default] 9 | None, 10 | 11 | /// Allow only one row to be selected at a time. `None` if no rows are selected. 12 | /// `Some(<row index>)` if a row is selected. 13 | Single(RwSignal<Option<usize>>), 14 | 15 | /// Allow multiple rows to be selected at a time. Each entry in the `Vec` 16 | /// is the index of a selected row. 17 | Multiple(RwSignal<HashSet<usize>>), 18 | } 19 | 20 | impl Selection { 21 | /// Clear the selection 22 | pub fn clear(&self) { 23 | match self { 24 | Selection::None => {} 25 | Selection::Single(selected_index) => { 26 | selected_index.set(None); 27 | } 28 | Selection::Multiple(selected_indices) => { 29 | selected_indices.write().clear(); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/sorting.rs: -------------------------------------------------------------------------------- 1 | use crate::{ColumnSort, TableHeadEvent}; 2 | use std::collections::VecDeque; 3 | 4 | /// Sorting mode 5 | #[derive(PartialEq, Eq, Clone, Copy, Debug, Default)] 6 | pub enum SortingMode { 7 | /// The table can be sorted by only one single column at a time 8 | SingleColumn, 9 | 10 | /// The table can be sorted by multiple columns ordered by priority 11 | #[default] 12 | MultiColumn, 13 | } 14 | 15 | impl SortingMode { 16 | pub fn update_sorting_from_event( 17 | &self, 18 | sorting: &mut VecDeque<(usize, ColumnSort)>, 19 | event: TableHeadEvent, 20 | ) { 21 | let (i, (_, mut sort)) = sorting 22 | .iter() 23 | .enumerate() 24 | .find(|(_, (col_index, _))| col_index == &event.index) 25 | .unwrap_or((0, &(event.index, ColumnSort::None))); 26 | 27 | if i == 0 || sort == ColumnSort::None { 28 | sort = match sort { 29 | ColumnSort::None => ColumnSort::Ascending, 30 | ColumnSort::Ascending => ColumnSort::Descending, 31 | ColumnSort::Descending => ColumnSort::None, 32 | }; 33 | } 34 | 35 | *sorting = sorting 36 | .clone() 37 | .into_iter() 38 | .filter(|(col_index, sort)| *col_index != event.index && *sort != ColumnSort::None) 39 | .collect(); 40 | 41 | if sort != ColumnSort::None { 42 | sorting.push_front((event.index, sort)); 43 | } 44 | 45 | if self == &SortingMode::SingleColumn { 46 | sorting.truncate(1); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/table_row.rs: -------------------------------------------------------------------------------- 1 | use crate::{ColumnSort, TableClassesProvider, TableHeadEvent}; 2 | use leptos::prelude::*; 3 | use std::collections::VecDeque; 4 | 5 | /// This trait has to implemented in order for [`TableContent`] to be able to render rows and the head row of the table. 6 | /// Usually this is done by `#[derive(TableRow)]`. 7 | /// 8 | /// Please see the [simple example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/simple/src/main.rs) 9 | /// for how to use. 10 | pub trait TableRow: Sized { 11 | type ClassesProvider: TableClassesProvider + Copy; 12 | 13 | /// How many columns this row has (i.e. the number of fields in the struct) 14 | const COLUMN_COUNT: usize; 15 | 16 | /// Renders the inner of one row of the table using the cell renderers. 17 | /// This produces the children that go into the `row_renderer` given to [`TableContent`]. 18 | /// 19 | /// This render function has to render exactly one root element. 20 | fn render_row(row: RwSignal<Self>, index: usize) -> impl IntoView; 21 | 22 | /// Render the head row of the table. 23 | fn render_head_row<F>( 24 | sorting: Signal<VecDeque<(usize, ColumnSort)>>, 25 | on_head_click: F, 26 | ) -> impl IntoView 27 | where 28 | F: Fn(TableHeadEvent) + Clone + 'static; 29 | 30 | /// The name of the column (= struct field name) at the given index. This can be used to implement 31 | /// sorting in a database. It takes the `#[table(skip)]` attributes into account. `col_index` 32 | /// refers to the index of the field in the struct while ignoring skipped ones. 33 | /// 34 | /// For example: 35 | /// ``` 36 | /// # use leptos_struct_table::*; 37 | /// # use leptos::prelude::*; 38 | /// # 39 | /// #[derive(TableRow)] 40 | /// struct Person { 41 | /// #[table(skip)] 42 | /// id: i64, // -> ignored 43 | /// 44 | /// name: String, // -> col_index = 0 45 | /// 46 | /// #[table(skip)] 47 | /// internal: usize, // -> ignored 48 | /// 49 | /// age: u16, // -> col_index = 1 50 | /// } 51 | /// 52 | /// assert_eq!(Person::col_name(0), "name"); 53 | /// assert_eq!(Person::col_name(1), "age"); 54 | /// ``` 55 | fn col_name(col_index: usize) -> &'static str; 56 | 57 | /// Converts the given sorting to an SQL statement. 58 | /// Return `None` when there is nothing to be sorted otherwise `Some("ORDER BY ...")`. 59 | /// Uses [`Self::col_name`] to get the column names for sorting. 60 | fn sorting_to_sql(sorting: &VecDeque<(usize, ColumnSort)>) -> Option<String> { 61 | let mut sort = vec![]; 62 | 63 | for (col, col_sort) in sorting { 64 | if let Some(col_sort) = col_sort.as_sql() { 65 | sort.push(format!("{} {}", Self::col_name(*col), col_sort)) 66 | } 67 | } 68 | 69 | if sort.is_empty() { 70 | return None; 71 | } 72 | 73 | Some(format!("ORDER BY {}", sort.join(", "))) 74 | } 75 | } 76 | 77 | pub fn get_sorting_for_column( 78 | col_index: usize, 79 | sorting: Signal<VecDeque<(usize, ColumnSort)>>, 80 | ) -> ColumnSort { 81 | sorting 82 | .read() 83 | .iter() 84 | .find(|(col, _)| *col == col_index) 85 | .map(|(_, sort)| *sort) 86 | .unwrap_or(ColumnSort::None) 87 | } 88 | -------------------------------------------------------------------------------- /src/time.rs: -------------------------------------------------------------------------------- 1 | //! Support for [::time] crate. 2 | 3 | use crate::*; 4 | use ::time::format_description; 5 | use ::time::{Date, OffsetDateTime, PrimitiveDateTime, Time}; 6 | use leptos::prelude::*; 7 | 8 | #[derive(Clone, Default)] 9 | pub struct RenderTimeOptions { 10 | /// Specifies a format string see [the time book](https://time-rs.github.io/book/api/format-description.html). 11 | pub string: Option<String>, 12 | } 13 | 14 | /// Implementation for [`Date`] to work with the [`TableRow`] derive and the [`DefaultTableCellRenderer`] 15 | /// ``` 16 | /// # use leptos_struct_table::*; 17 | /// # use leptos::prelude::*; 18 | /// # use ::time::Date; 19 | /// #[derive(TableRow, Clone)] 20 | /// #[table] 21 | /// struct SomeStruct { 22 | /// #[table(format(string = "[year]-[month]-[day]"))] 23 | /// my_field: Date 24 | /// } 25 | /// ``` 26 | impl CellValue<Date> for Date { 27 | type RenderOptions = RenderTimeOptions; 28 | 29 | fn render_value(self, options: Self::RenderOptions) -> impl IntoView { 30 | if let Some(value) = options.string.as_ref() { 31 | let format = format_description::parse(value) 32 | .expect("Unable to construct a format description given the format string"); 33 | self.format(&format) 34 | .expect("Unable to format given the format description") 35 | } else { 36 | self.to_string() 37 | } 38 | } 39 | } 40 | /// Implementation for [`Time`] to work with the [`TableRow`] derive and the [`DefaultTableCellRenderer`] 41 | /// ``` 42 | /// # use leptos_struct_table::*; 43 | /// # use leptos::prelude::*; 44 | /// # use ::time::Time; 45 | /// #[derive(TableRow, Clone)] 46 | /// #[table] 47 | /// struct SomeStruct { 48 | /// #[table(format(string = "[hour]:[minute]:[second]"))] 49 | /// my_field: Time 50 | /// } 51 | /// ``` 52 | impl CellValue<Time> for Time { 53 | type RenderOptions = RenderTimeOptions; 54 | 55 | fn render_value(self, options: Self::RenderOptions) -> impl IntoView { 56 | if let Some(value) = options.string.as_ref() { 57 | let format = format_description::parse(value) 58 | .expect("Unable to construct a format description given the format string"); 59 | self.format(&format) 60 | .expect("Unable to format given the format description") 61 | } else { 62 | self.to_string() 63 | } 64 | } 65 | } 66 | 67 | /// Implementation for [`PrimitiveDateTime`] to work with the [`TableRow`] derive and the [`DefaultTableCellRenderer`] 68 | /// ``` 69 | /// # use leptos_struct_table::*; 70 | /// # use leptos::prelude::*; 71 | /// # use ::time::PrimitiveDateTime; 72 | /// #[derive(TableRow, Clone)] 73 | /// #[table] 74 | /// struct SomeStruct { 75 | /// #[table(format(string = "[year]-[month]-[day] [hour]:[minute]:[second]"))] 76 | /// my_field: PrimitiveDateTime 77 | /// } 78 | /// ``` 79 | impl CellValue<PrimitiveDateTime> for PrimitiveDateTime { 80 | type RenderOptions = RenderTimeOptions; 81 | 82 | fn render_value(self, options: Self::RenderOptions) -> impl IntoView { 83 | if let Some(value) = options.string.as_ref() { 84 | let format = format_description::parse(value) 85 | .expect("Unable to construct a format description given the format string"); 86 | self.format(&format) 87 | .expect("Unable to format given the format description") 88 | } else { 89 | self.to_string() 90 | } 91 | } 92 | } 93 | 94 | /// Implementation for [`OffsetDateTime`] to work with the [`TableRow`] derive and the [`DefaultTableCellRenderer`] 95 | /// ``` 96 | /// # use leptos_struct_table::*; 97 | /// # use leptos::prelude::*; 98 | /// # use ::time::OffsetDateTime; 99 | /// #[derive(TableRow, Clone)] 100 | /// #[table] 101 | /// struct SomeStruct { 102 | /// #[table(format(string = "[year]-[month]-[day] [hour]:[minute]:[second] Z[offset_hour]"))] 103 | /// my_field: OffsetDateTime 104 | /// } 105 | /// ``` 106 | impl CellValue<OffsetDateTime> for OffsetDateTime { 107 | type RenderOptions = RenderTimeOptions; 108 | 109 | fn render_value(self, options: Self::RenderOptions) -> impl IntoView { 110 | if let Some(value) = options.string.as_ref() { 111 | let format = format_description::parse(value) 112 | .expect("Unable to construct a format description given the format string"); 113 | self.format(&format) 114 | .expect("Unable to format given the format description") 115 | } else { 116 | self.to_string() 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/uuid.rs: -------------------------------------------------------------------------------- 1 | //! Support for [uuid::Uuid] type. 2 | use crate::*; 3 | use ::uuid::Uuid; 4 | use leptos::prelude::*; 5 | 6 | /// Implementation for [`Uuid`] to work with the [`TableRow`] derive and the [`DefaultTableCellRenderer`] 7 | /// ``` 8 | /// # use leptos_struct_table::*; 9 | /// # use leptos::prelude::*; 10 | /// # use uuid::Uuid; 11 | /// #[derive(TableRow, Clone)] 12 | /// #[table] 13 | /// struct SomeStruct { 14 | /// my_field: Uuid 15 | /// } 16 | /// ``` 17 | impl CellValue<Uuid> for Uuid { 18 | type RenderOptions = (); 19 | 20 | fn render_value(self, _options: Self::RenderOptions) -> impl IntoView { 21 | self.to_string() 22 | } 23 | } 24 | --------------------------------------------------------------------------------