Compare commits

...

167 Commits

Author SHA1 Message Date
Greg Johnston
e416815591 clippy warning 2023-01-16 20:08:27 -05:00
Greg Johnston
81bdd6788f Fix hydration in release mode if _0-0-0 is a marker, not an element 2023-01-16 20:08:07 -05:00
Greg Johnston
ae0a243cc0 Fix meta doctests 2023-01-16 12:08:13 -05:00
Greg Johnston
7893ff8b55 Fix SSR doctests 2023-01-16 10:36:18 -05:00
Greg Johnston
6130e708ce cargo fmt 2023-01-16 09:26:40 -05:00
Greg Johnston
d049d2f36b Use comments instead of element markers for hydration -- fixes issue #320 2023-01-16 09:21:28 -05:00
Greg Johnston
61ca6465df Merge pull request #318 from benwis/main
Switch get_configuration calls to None and add a note in the docs for get_configuration()
2023-01-16 07:23:55 -05:00
benwis
10a833d763 Switch get_configuration calls to None and add a note in the docs for
uses of get_configuration()
2023-01-15 12:50:25 -08:00
Greg Johnston
17982e8ac5 Merge pull request #316 from leptos-rs/death-to-suspense-hydration-mismatches
Death to suspense hydration mismatches
2023-01-14 15:19:12 -05:00
Greg Johnston
6cfb6227f5 Merge pull request #315 from leptos-rs/add-code-of-conduct-1
Create CODE_OF_CONDUCT.md
2023-01-14 14:14:29 -05:00
Greg Johnston
159852b8d7 clippy 2023-01-14 14:12:52 -05:00
Greg Johnston
6f95713b59 Fix <Suspense/> hydration 2023-01-14 14:10:19 -05:00
Greg Johnston
e17afd4559 Handle custom elements correctly 2023-01-14 14:09:23 -05:00
Greg Johnston
e0aa1e245b Create CODE_OF_CONDUCT.md 2023-01-14 12:04:11 -05:00
Greg Johnston
7951a6e9cf Merge pull request #314 from leptos-rs/create-slice-doctest
Fix doctest in `create_slice` and edit doc comment slightly
2023-01-14 09:50:29 -05:00
Greg Johnston
1f39299303 Fix doctest in create_slice and edit doc comment slightly 2023-01-14 08:17:27 -05:00
Greg Johnston
2be4610233 Update README.md 2023-01-14 08:03:57 -05:00
Greg Johnston
af254e9b61 Merge pull request #312 from TaKO8Ki/use-rust-cache
Use rust-cache in CI
2023-01-14 07:59:18 -05:00
Takayuki Maeda
4aae8a5088 use rust cache 2023-01-14 20:07:09 +09:00
Greg Johnston
7ff044cef6 Merge pull request #308 from Indrazar/main
Update Generated API URL on Windows Attempt #2
2023-01-13 07:30:03 -05:00
Greg Johnston
11122b575e Merge pull request #310 from akesson/doc-examples-fixes
Doc fix + search & replace url
2023-01-13 07:29:47 -05:00
Greg Johnston
a62ee4031b Merge branch 'main' into doc-examples-fixes 2023-01-13 07:29:42 -05:00
Greg Johnston
19b43607a1 Merge pull request #309 from dzfrias/patch-1 2023-01-13 07:27:46 -05:00
hakesson
884297706a Search https://github.com/gbj/ and replace with https://github.com/leptos-rs/ 2023-01-13 09:03:11 +01:00
hakesson
fb4c208609 Should point to counters (plural) 2023-01-13 09:00:35 +01:00
Diego Frias
5701e74efb Fix link to counters_stable in README 2023-01-12 20:26:11 -08:00
indrazar
2afe8e202a update url for Windows directories attempt 2 2023-01-12 22:07:55 -05:00
Greg Johnston
08ec473304 Merge pull request #296 from Gentle/create_slice
create_slice in leptos_reactive
2023-01-12 13:26:51 -05:00
Greg Johnston
3ae0880db4 Merge pull request #305 from leptos-rs/0.1.0
Version `0.1.0` release
2023-01-12 12:53:26 -05:00
Ramon Klass
9180aaad7e create_slice: actual documentation 2023-01-12 16:51:37 +01:00
Ramon Klass
89be6bc68e removed Debug from create_slice 2023-01-12 16:30:56 +01:00
Ramon Klass
56426170b0 remove Debug from create_memo 2023-01-12 16:30:56 +01:00
Ramon Klass
0571ea4103 create_slice for RwSignal with 2 closures 2023-01-12 16:30:56 +01:00
Greg Johnston
cdf709fb09 0.1.0 2023-01-12 09:57:08 -05:00
Greg Johnston
7eaa36812d Merge pull request #301 from leptos-rs/ssr-inner-html
Fix SSR of elements with `inner_html`
2023-01-11 21:59:45 -05:00
Greg Johnston
2ef36c65fd cargo fmt 2023-01-11 20:58:26 -05:00
Greg Johnston
a16540ccc5 Merge pull request #302 from martinfrances107/invalid_toml
Minor: "leptos.workspace = true" is invalid.
2023-01-11 20:48:51 -05:00
Greg Johnston
613c7b32a1 Merge pull request #300 from akesson/pre-resolved-data 2023-01-11 13:03:39 -05:00
Martin
8f2a731c9f Minor formatting fix. 2023-01-11 17:44:56 +00:00
Martin
1621b86d8f Minor: "leptos.workspace = true" is invalid. 2023-01-11 17:09:10 +00:00
Greg Johnston
b1ac17995d Fix SSR of elements with inner_html 2023-01-11 07:47:24 -05:00
hakesson
6471af8b89 Let data_fn return DataResponse 2023-01-11 12:08:44 +01:00
hakesson
abf54b832e Improve naming and doc 2023-01-11 09:56:58 +01:00
hakesson
91e839c71a data_fn returns Result 2023-01-11 09:08:07 +01:00
Greg Johnston
b944b17e6d Update Leptos_logo_pref_dark_RGB.svg 2023-01-10 21:56:30 -05:00
Greg Johnston
54c1abb4b7 Merge pull request #298 from leptos-rs/dark-mode-logo
Dark mode logo
2023-01-10 21:52:15 -05:00
Greg Johnston
c24f33aeb2 Dark-mode logo 2023-01-10 21:51:59 -05:00
Greg Johnston
9e83acfe63 Make cargo fmt happy 2023-01-10 21:18:03 -05:00
Greg Johnston
46254a18f3 Improve router feature warning 2023-01-10 21:10:27 -05:00
Greg Johnston
7d7a96d9bc Switch logo on color-scheme change 2023-01-10 13:51:52 -05:00
hakesson
339c920b19 Make data_fn take cloned HttpRequest 2023-01-10 18:55:45 +01:00
Greg Johnston
ecc24fa65d Create FUNDING.yml
Add GitHub sponsor info
2023-01-10 12:52:39 -05:00
hakesson
3f036ee321 Fix refactoring error 2023-01-10 18:48:05 +01:00
hakesson
4e00ec2348 Add leptos_data_routes 2023-01-10 18:27:35 +01:00
hakesson
9c59b720b7 Improve fmt 2023-01-10 18:26:11 +01:00
hakesson
da4340894f Extract fn provide_contexts 2023-01-10 18:18:25 +01:00
hakesson
02f5c3891c Extract fn leptos_corrected_path 2023-01-10 18:06:24 +01:00
hakesson
087e67466f Extract fn html_parts & stream_app 2023-01-10 18:03:11 +01:00
Greg Johnston
1925c5bbe5 Merge pull request #294 from martinfrances107/workflow_cargo_fmt
Policy change ( part 2 ) Added rule of enforcing cargo fmt.
2023-01-10 09:37:38 -05:00
Greg Johnston
1613616008 Merge branch 'main' into workflow_cargo_fmt 2023-01-10 09:36:53 -05:00
Greg Johnston
8a01880ade Merge pull request #293 from benwis/remove-deps
Remove a couple extra deps from the axum integration
2023-01-10 09:36:15 -05:00
Greg Johnston
180ab87ff9 Merge pull request #282 from akesson/workspace-multi-projects
Use envs for workspace config
2023-01-10 09:35:41 -05:00
hakesson
e324fb6e76 Default site-root to '.' 2023-01-10 10:33:53 +01:00
hakesson
0547b4f846 Add missing semi-colon 2023-01-10 10:01:23 +01:00
henrik
75659ce674 Merge branch 'main' into workspace-multi-projects 2023-01-10 09:59:44 +01:00
Martin
190cb162ad Added rustfmt to setup for actions-rs/toolchain. 2023-01-10 08:52:55 +00:00
hakesson
1f556cefb0 Default to serve root 2023-01-10 09:52:21 +01:00
hakesson
6a68ef67f3 Add LEPTOS prefix to OUTPUT_NAME in all files 2023-01-10 09:26:10 +01:00
Martin
23bbd90c81 Policy change ( part 2 ) added rule of enforcing cargo fmt. 2023-01-10 08:20:02 +00:00
benwis
27b8553076 Remove a couple extra deps from the axum integration 2023-01-09 23:46:07 -08:00
hakesson
5bfeb93e3d Make Cargo.toml optional 2023-01-10 08:26:22 +01:00
hakesson
dd9ae1b7b1 Add LEPTOS prefix to OUTPUT_NAME 2023-01-10 08:25:43 +01:00
Greg Johnston
ad34a5d9c6 Update name to leptos-rs/leptos 2023-01-09 21:59:13 -05:00
Greg Johnston
ace5e7cbba Add missing hackernews_axum makefile 2023-01-09 21:59:00 -05:00
Greg Johnston
c8f0988e53 Merge pull request #292 from gbj/router-warnings
Implement `state` and `replace` correctly in `leptos_router` and clear warnings
2023-01-09 21:11:28 -05:00
Greg Johnston
64f0f8879b Implement state and replace correctly in leptos_router and clear warnings 2023-01-09 21:10:42 -05:00
Greg Johnston
b8cafeb650 Merge pull request #289 from gbj/forbid-unsafe
Forbid `unsafe` code in all packages
2023-01-09 20:45:28 -05:00
Greg Johnston
992b218ffe Merge pull request #291 from gbj/correct-axum-query-handling
Correct Axum query handling
2023-01-09 20:44:58 -05:00
Greg Johnston
5df89b0d25 Fix query parsing in Axum integration 2023-01-09 20:44:06 -05:00
Greg Johnston
c050456a47 Use a runtime warning about SVG <a/> instead of a macro warning on all ambiguous tags 2023-01-09 20:31:51 -05:00
Greg Johnston
8a8d7cbe1b Fix forbid_unsafe in a frustratingly-stupid way 2023-01-09 20:07:16 -05:00
Greg Johnston
f5f345e623 Merge pull request #286 from benwis/favicons
FAVICON!
2023-01-09 19:57:27 -05:00
Greg Johnston
4df3687463 Forbid unsafe code in all packages 2023-01-09 19:48:51 -05:00
Greg Johnston
f6622448e9 Update README.md 2023-01-09 19:38:02 -05:00
Greg Johnston
78825401c5 Merge pull request #288 from gbj/new-logo
New logo
2023-01-09 19:34:10 -05:00
Greg Johnston
f2b7ad6244 New logo 2023-01-09 19:33:44 -05:00
Greg Johnston
43f107d9bd New logo 2023-01-09 19:33:38 -05:00
Greg Johnston
a2612ca1fc Merge pull request #275 from martinfrances107/cargo_fmt
Policy change: Workflow now enforce "cargo fmt".
2023-01-09 19:20:57 -05:00
Greg Johnston
a000c84e1a Fix scope disposal code in Router (closes issue #240) 2023-01-09 19:17:37 -05:00
benwis
ee647cba1c Add Favicons to all the examples and standardize on the public folder for public assets 2023-01-09 15:27:52 -08:00
Greg Johnston
1377b823e2 Merge pull request #285 from martinfrances107/version_numbers
simple_logger use version 4.0.0 everywhere.
2023-01-09 18:26:57 -05:00
Martin
4d21f5ac63 simple_logger use version 4.0.0 everywhere. 2023-01-09 21:43:48 +00:00
hakesson
1ec603ee58 Use envs for workspace config 2023-01-09 20:06:43 +01:00
Martin
c56806713e Keeping up with changes to main. 2023-01-09 12:47:50 +00:00
Martin
2f6aa6753d Removed workflow "Cargo fmt" test. 2023-01-09 12:44:35 +00:00
Martin
2544687acd Moved the order of check. 2023-01-09 12:44:35 +00:00
Martin
3d25e86c23 Policy change: Workflow now enforce "cargo fmt". 2023-01-09 12:44:30 +00:00
Greg Johnston
28ec3a6cda Merge pull request #281 from martinfrances107/hackernews_axum
Minor: Clippy fixes related to hackernew_axum.
2023-01-09 07:02:43 -05:00
Greg Johnston
8b92a561a3 Merge pull request #269 from benwis/generated_routes
Generate Routes and pass them to Actix/Axum
2023-01-09 07:02:11 -05:00
Martin
e490c0423f Minor: Clippy fixes related to hackernew_axum. 2023-01-09 09:54:39 +00:00
benwis
b6579a040a Warning Squashing 2023-01-08 19:41:22 -08:00
benwis
01e024b726 One more time! 2023-01-08 19:38:45 -08:00
benwis
6603c44ce2 Minor fixes and revisions 2023-01-08 19:36:24 -08:00
Greg Johnston
977f11b180 Merge pull request #280 from gbj/prevent-panic-in-ssr
Closes #278: Prevent `create_signal_from_stream` from panicking on SSR
2023-01-08 19:52:48 -05:00
Greg Johnston
fb34b29ccf Merge pull request #279 from DPM97/default_prop_values
default prop values in prop macro
2023-01-08 19:52:38 -05:00
benwis
6576d8eda1 Change site_root default to ".". Hope that cargo-leptos has that check 2023-01-08 14:54:32 -08:00
benwis
6b729f9131 Fix hackernews_axum example 2023-01-08 14:51:41 -08:00
benwis
dc60c35b58 Rewrite file handlers for Axum, and update all examples to use the new
generated routes. Fix a few issues in the integrations, and reduce the
number of warnings
2023-01-08 14:18:51 -08:00
Greg Johnston
32ec9cc57e Prevent create_signal_from_stream from panicking on SSR 2023-01-08 17:09:04 -05:00
Dylan Maloy
35601d8284 update abort_opt_message 2023-01-08 16:47:02 -05:00
Dylan Maloy
49bc7d2a27 init 2023-01-08 16:35:43 -05:00
Greg Johnston
aa7c7367dc Merge pull request #277 from martinfrances107/clippy_hackernews
examples/hackernews - Cargo clippy fixes.
2023-01-08 14:10:57 -05:00
Greg Johnston
70808c5262 Merge pull request #272 from DPM97/component_lifetimes
fix component macro lifetime parsing
2023-01-08 14:07:00 -05:00
Martin
67503a108d BugFix. 2023-01-08 15:28:30 +00:00
Martin
ef52a01838 examples/hackernews - Cargo clippy fixes. 2023-01-08 13:33:53 +00:00
Greg Johnston
4cacfe98d8 Merge pull request #273 from martinfrances107/needless_borrow
Minor: Removed Clippy::needless_borrrow issues.
2023-01-08 07:27:02 -05:00
Greg Johnston
52e653316e Merge pull request #270 from gbj/chorse
Chores
2023-01-08 07:26:14 -05:00
Greg Johnston
bf5b6ca9c2 Merge pull request #264 from martinfrances107/Clipp_removed_clone_round_2
Removing clone call where possible(round2).
2023-01-08 07:26:00 -05:00
Martin
8875939a27 Minor: Removed Clippy::needless_borrrow issues. 2023-01-08 10:42:25 +00:00
Martin
b9a83277d9 Removing clone call where possible(round2). 2023-01-08 09:57:19 +00:00
benwis
dad84b5867 Merge branch 'main' into generated_routes 2023-01-07 21:17:18 -08:00
Dylan Maloy
1f29d29947 init 2023-01-08 00:15:00 -05:00
benwis
1b8175e2fa Add missing tokio dep for RwLock 2023-01-07 20:27:05 -08:00
Greg Johnston
dbe3ec015c Fix warning for unused variable. 2023-01-07 22:06:02 -05:00
Greg Johnston
492fa6c6d3 Fix link to Stream in docs. 2023-01-07 22:05:44 -05:00
Greg Johnston
343e8c8abe Update macro docs to reflect newly-available class syntax. 2023-01-07 22:02:23 -05:00
Greg Johnston
656d20cb65 Don't panic in proc macro, use proc_macro_error instead. 2023-01-07 21:32:52 -05:00
Greg Johnston
a0a66b75dd Allow complex class names with ("class-[name]-42", value) syntax. 2023-01-07 21:29:26 -05:00
Greg Johnston
085ba3506c Merge pull request #267 from gbj/adjust-tracing
Adjust tracing
2023-01-07 20:31:54 -05:00
Greg Johnston
63f3780eda Merge pull request #268 from gbj/fnonce-children
`children` should take `FnOnce(Scope) -> Fragment`, to ease need of c…
2023-01-07 20:31:29 -05:00
benwis
c41cf879d1 Formatting 2023-01-07 15:44:35 -08:00
benwis
b34f2070d3 Remove extraneous route 2023-01-07 15:09:49 -08:00
benwis
7fa21defa6 Merge remote-tracking branch 'origin/generated_routes' into generated_routes 2023-01-07 15:08:12 -08:00
benwis
bdd9abc04d Removing some missed code and changing the stylesheet 2023-01-07 15:06:21 -08:00
Ben Wishovich
1d25134213 Merge branch 'main' into generated_routes 2023-01-07 14:58:26 -08:00
benwis
de73622949 Change Axum's "" matching 2023-01-07 14:53:38 -08:00
benwis
5d3cfc6483 Actix seems to be working now, plus applied Henrik's path recommendations 2023-01-07 14:49:25 -08:00
Greg Johnston
6fbbd09000 <Suspense/> and <Transition/> should take Fn for children because they need to use it multiple times 2023-01-07 17:09:50 -05:00
Greg Johnston
f2842cf14e children should take FnOnce(Scope) -> Fragment, to ease need of cloning etc. 2023-01-07 17:04:58 -05:00
Greg Johnston
a6b6864bc5 Merge branch 'adjust-tracing' of https://github.com/gbj/leptos into adjust-tracing 2023-01-07 16:27:33 -05:00
Greg Johnston
063b946cd4 Remove unnecessary log 2023-01-07 16:27:25 -05:00
Greg Johnston
5a2c9ea345 Merge branch 'main' into adjust-tracing 2023-01-07 16:26:20 -05:00
Greg Johnston
808d87598b Tracing for events and elements. 2023-01-07 16:20:00 -05:00
Greg Johnston
0956c48b1e Merge pull request #266 from gbj/fix-meta-panic
Adjust default features for `meta` and `router`
2023-01-07 14:44:23 -05:00
Greg Johnston
8915e2615b Adjust default features for meta and router 2023-01-07 14:43:32 -05:00
Greg Johnston
7f47134058 Merge pull request #265 from martinfrances107/needless_borrowed_reference
Clippy: Minor needless_borrowed_reference.
2023-01-07 14:21:58 -05:00
Greg Johnston
af7b93fa1e Merge pull request #128 from akesson/workspace-features
Workspace features
2023-01-07 14:19:56 -05:00
Greg Johnston
ed940f577a Add tracing for event handlers 2023-01-07 13:32:40 -05:00
Martin
916f30a07b Clippy: Minor needless_borrowed_reference. 2023-01-07 18:28:42 +00:00
Greg Johnston
e01c565de1 Improve tracing formatting 2023-01-07 13:16:30 -05:00
Greg Johnston
dffe195cdc Fix two warnings 2023-01-07 13:16:20 -05:00
Greg Johnston
a5e2587555 Merge pull request #261 from martinfrances107/Clippy_removed_clone_where_possible
Clippy: Removed stray calls to .clone().
2023-01-07 12:47:06 -05:00
Greg Johnston
af8889fab2 Merge pull request #262 from martinfrances107/uninlined_format_args
Minor: Clippy format!() all variables now inlined.
2023-01-07 12:42:56 -05:00
Greg Johnston
267c1cfc34 Merge pull request #263 from martinfrances107/if_let_some
Removed clippy::single_match issue.
2023-01-07 12:40:55 -05:00
Greg Johnston
3498378e60 Merge pull request #260 from jquesada2016/into_signal_traits
added `IntoSignal` and `IntoSignalSetter` helper traits
2023-01-07 12:40:15 -05:00
hakesson
f8c680d14d Integrations with workspace dependencies 2023-01-07 18:05:35 +01:00
hakesson
b852e459a9 Unify workspace dependencies 2023-01-07 18:00:37 +01:00
hakesson
681f10ec8d Workspace-based versioning 2023-01-07 17:35:02 +01:00
Martin
1d480791a1 Removed clippy::single_match issue. 2023-01-07 16:08:17 +00:00
Martin
7acc309f66 Minor: Clippy format!() all variables now inlined. 2023-01-07 15:46:47 +00:00
Martin
9527de15ed Removed stray calls to .clone(). 2023-01-07 14:53:59 +00:00
Jose Quesada
aeb25a715a added IntoSignal and IntoSignalSetter helper traits 2023-01-07 08:20:27 -06:00
Greg Johnston
46e91a538c Merge branch 'main' of https://github.com/gbj/leptos 2023-01-07 08:32:45 -05:00
Greg Johnston
1fe526c99c Remove erroneous log 2023-01-07 08:32:39 -05:00
Greg Johnston
6b05918807 Merge pull request #257 from gbj/ci-disk-space
Improve CI disk space usage
2023-01-07 07:44:10 -05:00
benwis
677e4f2540 Leptos can now generate routes and provide them to the Axum router. More
testing and Actix version to come
2023-01-06 19:52:38 -08:00
benwis
63b1837315 First pass of method to generate routelist 2023-01-06 14:08:45 -08:00
132 changed files with 2206 additions and 1188 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: gbj

View File

@@ -28,6 +28,7 @@ jobs:
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
@@ -35,16 +36,10 @@ jobs:
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- name: Cargo cache
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ matrix.rust }}-${{ hashFiles('**/Cargo.lock') }}
- name: Run Rustfmt
run: cargo fmt -- --check
- uses: Swatinem/rust-cache@v2
- name: Run tests with all features
run: cargo make ci

51
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,51 @@
# Contributor Covenant Code of Conduct
_This Code of Conduct is based on the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct)
and the [Bevy Code of Conduct](https://raw.githubusercontent.com/bevyengine/bevy/main/CODE_OF_CONDUCT.md),
which are adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling)
and the [Contributor Covenant](https://www.contributor-covenant.org)._
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
We are a community of people learning and exploring how to build better web applications
with Rust. When interacting with one another, please remember that there are no experts and there are
no stupid questions. Assume the best in other people's communication, and take a step back if
you find yourself getting defensive.
Please note the following guidelines as well:
* Please avoid using overtly sexual aliases or other nicknames that might detract from a friendly, safe and welcoming environment for all.
* Please be kind and courteous. Theres no need to be mean or rude.
* Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
* Please keep unstructured critique to a minimum. If you have solid ideas you want to experiment with, make a fork and see how it works.
* We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behavior. We interpret the term “harassment” as including the definition in the [Citizen Code of Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md); if you have any lack of clarity about what might be included in that concept, please read their definition. In particular, we dont tolerate behavior that excludes people in socially marginalized groups.
* Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact the maintainers immediately. Whether youre a regular contributor or a newcomer, we care about making this community a safe place for you and weve got your back.
* Do not make casual mention of slavery or indentured servitude and/or false comparisons of one's occupation or situation to slavery. Please consider using or asking about alternate terminology when referring to such metaphors in technology.
* Likewise any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome.
## Moderation
These are the policies for upholding [our communitys standards of conduct](#our-standards). If you feel that a thread needs moderation, please contact the maintainers.
1. Remarks that violate the community standards of conduct, including hateful, hurtful, oppressive, or exclusionary remarks, are not allowed. (Cursing is allowed, but never targeting another user, and never in a hateful manner).
2. Remarks that maintainers find inappropriate, whether listed in the code of conduct or not, are also not allowed.
3. Maintainers will first respond to such remarks with a warning.
4. If the warning is unheeded, the user will be “kicked,” i.e., kicked out of the communication channel to cool off.
5. If the user comes back and continues to make trouble, they will be banned, i.e., indefinitely excluded.
6. Maintainers may choose at their discretion to un-ban the user if it was a first offense and they offer the offended party a genuine apology.
7. If a maintainer bans someone and you think it was unjustified, please take it up with that maintainer, or with a different maintainer, in private. Complaints about bans in-channel are not allowed.
8. Maintainers are held to a higher standard than other community members. If a maintainer creates an inappropriate situation, they should expect less leeway than others.
The enforcement policies in the code of conduct apply to all official venues, including Discord channels, GitHub repositories, and all other forums.

View File

@@ -23,10 +23,23 @@ members = [
]
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.1.0"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.1.0" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.0" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.0" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.0" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.0" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.0" }
leptos_router = { path = "./router", version = "0.1.0" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.0" }
[profile.release]
codegen-units = 1
lto = true
opt-level = 'z'
[workspace.metadata.cargo-all-features]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@@ -1,6 +1,7 @@
**NOTE: We're in the middle of merging changes and making fixes to support our upcoming `0.1.0` release. Some of the examples may be in a broken state. You can continue using the `0.0` releases with no issues.**
<img src="https://raw.githubusercontent.com/gbj/leptos/main/docs/logos/logo.svg" alt="Leptos Logo" style="width: 100%; height: auto; display: block; margin: auto;">
<picture>
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_pref_dark_RGB.svg" media="(prefers-color-scheme: dark)">
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
</picture>
[![crates.io](https://img.shields.io/crates/v/leptos.svg)](https://crates.io/crates/leptos)
[![docs.rs](https://docs.rs/leptos/badge.svg)](https://docs.rs/leptos)
@@ -58,12 +59,11 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
Here are some resources for learning more about Leptos:
- [Examples](https://github.com/gbj/leptos/tree/main/examples)
- [Examples](https://github.com/leptos-rs/leptos/tree/main/examples)
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
- [Common Bugs](https://github.com/gbj/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
- Leptos Guide (in progress)
## `nightly` Note
Most of the examples assume youre using `nightly` Rust.
@@ -80,13 +80,13 @@ If youre on `stable`, note the following:
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0-alpha", features = ["stable"] }`
2. `nightly` enables the function call syntax for accessing and setting signals. If youre using `stable`,
youll just call `.get()`, `.set()`, or `.update()` manually. Check out the
[`counters-stable` example](https://github.com/gbj/leptos/blob/main/examples/counters-stable/src/main.rs)
youll just call `.get()`, `.set()`, or `.update()` manually. Check out the
[`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
for examples of the correct API.
## `cargo-leptos`
[`cargo-leptos`](https://github.com/akesson/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our [starter template](https://github.com/leptos-rs/start).
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our [starter template](https://github.com/leptos-rs/start).
```bash
cargo install cargo-leptos
@@ -106,7 +106,7 @@ Sure! Obviously the `view` macro is for generating DOM nodes but you can use the
- Use event listeners to update signals
- Create effects to update the UI
I've put together a [very simple GTK example](https://github.com/gbj/leptos/blob/main/examples/gtk/src/main.rs) so you can see what I mean.
I've put together a [very simple GTK example](https://github.com/leptos-rs/leptos/blob/main/examples/gtk/src/main.rs) so you can see what I mean.
### How is this different from Yew/Dioxus?
@@ -124,7 +124,6 @@ There are some practical differences that make a significant difference:
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
- **Template node cloning:** Leptos's `view` macro compiles to a static HTML string and a set of instructions of how to assign its reactive values. This means that at runtime, Leptos can clone a `<template>` node rather than calling `document.createElement()` to create DOM nodes. This is a _significantly_ faster way of rendering components.
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:

View File

@@ -1,6 +1,6 @@
# Introduction
This book is intended as an introduction to the [Leptos](https://github.com/gbj/leptos) Web framework. Together, well build a simple todo app—first as a client-side app, then as a full-stack app.
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework. Together, well build a simple todo app—first as a client-side app, then as a full-stack app.
The guide doesnt assume you know anything about fine-grained reactivity or the details of modern Web frameworks. It does assume you are familiar with the Rust programming language, HTML, CSS, and the DOM and other Web APIs.

View File

@@ -1,8 +1,8 @@
# Getting Started
> The code for this chapter can be found [here](https://github.com/gbj/leptos/tree/main/docs/book/project/ch02_getting_started).
> The code for this chapter can be found [here](https://github.com/leptos-rs/leptos/tree/main/docs/book/project/ch02_getting_started).
The easiest way to get started using Leptos is to use [Trunk](https://trunkrs.dev/), as many of our [examples](https://github.com/gbj/leptos/tree/main/examples) do. (Trunk is a simple build tool that includes a dev server.)
The easiest way to get started using Leptos is to use [Trunk](https://trunkrs.dev/), as many of our [examples](https://github.com/leptos-rs/leptos/tree/main/examples) do. (Trunk is a simple build tool that includes a dev server.)
If you dont already have it installed, you can install Trunk by running

View File

@@ -1,6 +1,6 @@
# Templating: Building User Interfaces
> The code for this chapter can be found [here](https://github.com/gbj/leptos/tree/main/docs/book/project/ch03_building_ui).
> The code for this chapter can be found [here](https://github.com/leptos-rs/leptos/tree/main/docs/book/project/ch03_building_ui).
## RSX and the `view!` macro

BIN
docs/logos/Leptos_logo_RGB.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

64
docs/logos/Leptos_logo_RGB.svg Executable file
View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 437.4294 209.6185" style="enable-background:new 0 0 437.4294 209.6185;" xml:space="preserve">
<path style="fill:none;" d="M130.0327,79.3931c-11.4854-0.23-22.52,9.3486-24.5034,21.0117l49.1157,0.0293
c-2.1729-10.418-11.1821-21.0449-24.1987-21.0449C130.3081,79.3892,130.1714,79.3907,130.0327,79.3931z"/>
<path style="fill:#181139;" d="M95.1109,128.1089H58.6797V65.6861c0-1.5234-0.8169-2.4331-2.1855-2.4331h-3.1187
c-1.3159,0-2.2349,1.0005-2.2349,2.4331v67.4297c0,1.4521,0.8145,2.2852,2.2349,2.2852h41.7353c1.4844,0,2.4819-0.9375,2.4819-2.333
v-2.7744C97.5928,128.9253,96.6651,128.1089,95.1109,128.1089z"/>
<path style="fill:#181139;" d="M146.4561,77.1739c-4.8252-3.001-10.3037-4.5249-16.2837-4.5288c-0.0068,0-0.0137,0-0.0205,0
c-5.7349,0-11.1377,1.4639-16.0566,4.3511c-4.916,2.8853-8.8721,6.8364-11.7593,11.7456
c-2.8975,4.9248-4.3687,10.332-4.3721,16.0713c-0.0034,5.7188,1.4966,11.0654,4.4565,15.8887
c2.9893,4.9209,6.8789,8.7334,11.8887,11.6514c4.8657,2.8633,10.2397,4.3174,15.9717,4.3203c0.0073,0,0.0146,0,0.022,0
c8.123,0,14.7441-2.5869,21.4683-8.3906c0.5493-0.4805,0.8516-1.1201,0.8516-1.8008c0.001-0.6074-0.1743-1.1035-0.5205-1.4756
l-1.3569-1.8428l-0.0732-0.0859c-0.2637-0.2637-0.6929-0.6152-1.3716-0.6152c-0.6421,0-1.2549,0.2217-1.7124,0.6143
c-1.9346,1.585-3.5459,2.8008-4.7969,3.6182c-1.7979,1.208-5.8218,3.2314-12.5986,3.2314c-0.0073,0-0.0142,0-0.021,0
c-0.1357,0.0029-0.269,0.0039-0.4043,0.0039c-12.2642,0-23.4736-10.3262-24.5088-22.4814l53.0127,0.0322c0.0015,0,0.0024,0,0.0034,0
c2.2373,0,3.4697-1.1621,3.4712-3.2715c0.0034-5.2588-1.3574-10.3945-4.0464-15.2705
C155.0015,84.0953,151.2188,80.1363,146.4561,77.1739z M154.6451,100.4341l-49.1157-0.0293
c1.9834-11.6631,13.0181-21.2417,24.5034-21.0117c0.1387-0.0024,0.2754-0.0039,0.4136-0.0039
C143.4629,79.3892,152.4722,90.0162,154.6451,100.4341z"/>
<path style="fill:#181139;" d="M204.0386,136.6382c5.7319,0,11.1069-1.4502,15.9746-4.3115
c4.938-2.9014,8.75-6.7129,11.6533-11.6533c2.8608-4.8672,4.311-10.2578,4.311-16.0244c0-5.7324-1.4502-11.1064-4.311-15.9746
c-2.9019-4.9385-6.7134-8.75-11.6533-11.6533c-4.8687-2.8618-10.2437-4.3125-15.9746-4.3125
c-9.938,0-19.2021,4.7583-24.3516,12.3174v-9.438c0-0.5946-0.1465-1.0788-0.411-1.4511c-0.3815-0.5369-1.0157-0.834-1.8727-0.834
h-2.6738c-1.4521,0-2.2852,0.833-2.2852,2.2852v5.6964v46.4791v23.9676c0,1.2568,0.7808,2.0371,2.0371,2.0371h3.3667
c0.9209,0,1.6421-0.6992,1.6421-1.5908v-17.098v-10.984C185.0884,131.8892,194.2749,136.6382,204.0386,136.6382z M186.6358,122.5591
c-4.9346-4.9346-7.6831-11.4932-7.542-18.0254c-0.1367-6.3506,2.5439-12.751,7.3545-17.5605
c4.8521-4.8521,11.3037-7.5547,17.7383-7.417c4.3691,0,8.4863,1.1465,12.2314,3.4043c3.7344,2.2979,6.7456,5.4053,8.9492,9.2354
c2.1699,3.9072,3.2695,8.0967,3.2695,12.4697c0.1396,6.4619-2.5967,12.9844-7.5083,17.8955
c-4.7617,4.7617-11.0469,7.3857-17.2544,7.2803C197.6856,129.9712,191.396,127.3208,186.6358,122.5591z"/>
<path style="fill:#181139;" d="M241.8955,80.3975h7.5669v42.0259c0,6.8174,4.5674,12.1309,11.0825,12.9189
c0.6836,0.1055,1.8379,0.1572,3.5303,0.1572c2.0078,0,3.0273-0.3535,3.0273-2.2842v-2.377c0-1.7891-1.334-2.0371-2.7568-2.0371
c0,0-0.001,0-0.002,0l-1.7871-0.0488c-2.0117-0.0439-3.4883-0.7627-4.3896-2.1367c-0.9697-1.4805-1.4619-3.1738-1.4619-5.0352
V80.3975h10.0928c1.3076,0,2.2852-1.3628,2.2852-2.5815v-1.9312c0-1.3999-0.8359-2.2354-2.2354-2.2354h-10.1426V60.6861
c0-1.4619-0.7969-2.4829-1.9375-2.4829c-0.1865,0-0.4121,0-0.6392,0.0884l-2.6489,0.6865
c-1.2109,0.3682-2.0171,0.9263-2.0171,2.4507v12.2207h-7.5669c-1.4185,0-2.335,0.897-2.335,2.2852v1.8813
C239.5606,79.2393,240.6079,80.3975,241.8955,80.3975z"/>
<path style="fill:#181139;" d="M379.1182,106.2691c-4.0488-2.9219-8.8545-5.0293-14.291-6.2646
c-6.5049-1.3975-13.4473-5.2129-13.3203-10.3066c0-7.5225,6.6367-10.1914,12.3203-10.1914c5.3574,0,10.2207,3.002,13.001,8.0146
c0.6729,1.2861,1.4785,1.9375,2.3955,1.9375c0.3311,0,0.7061-0.1113,0.9922-0.2832l2.2021-1.1523
c0.5947-0.3408,0.9229-0.9414,0.9229-1.6924c0-0.5205-0.0908-0.9541-0.2617-1.292c-3.6367-8.2466-10.0967-12.4282-19.2021-12.4282
c-11.7305,0-19.6123,6.9263-19.6123,17.2349c0,4.3125,1.8438,7.9746,5.4756,10.8809c3.4482,2.7979,7.9121,4.8623,13.2705,6.1377
c4.5859,1.085,8.3193,2.5654,11.0977,4.4023c1.4159,0.9354,2.4412,2.0535,3.106,3.3672c0.6053,1.1962,0.9135,2.5535,0.9135,4.1005
c0.0742,2.3857-0.79,4.5176-2.5684,6.3389c-3.1445,3.2178-8.4053,4.6689-12.0205,4.6689c-0.0361,0-0.0723,0-0.1074,0
c-3.4268,0-6.4893-0.8438-9.1035-2.5068c-2.5918-1.6484-4.2363-3.8076-5.0293-6.6064c-0.3203-1.0996-0.751-2.1738-2.1553-2.1738
c-0.0742,0-0.2109,0.0146-0.4062,0.0449c-0.1133,0.0166-0.2559,0.0381-0.5088,0.0742l-1.8818,0.4463l-0.1045,0.0332
c-1.0244,0.4082-1.6113,1.1846-1.6113,2.1309c0,0.2285,0.0625,0.6592,0.2178,1.1094c1.9707,8.5801,10.2432,14.3447,20.5732,14.3447
c0.125,0.002,0.249,0.002,0.374,0.002c6.5947,0,12.6748-2.3193,16.7275-6.3945c3.1895-3.208,4.8311-7.2363,4.748-11.6357
c0-2.8187-0.6185-5.3109-1.8062-7.481C382.4437,109.2624,381.0062,107.631,379.1182,106.2691z"/>
<path style="fill:#EF3939;" d="M348.9043,45.7325c0-6.3157-3.2826-11.8699-8.2238-15.0756
c-2.811-1.8237-6.1537-2.8947-9.7469-2.8947c-9.9092,0-17.9707,8.0615-17.9707,17.9702c0,4.7659,1.8775,9.0925,4.9157,12.3123
c-3.6619,4.3709-6.6334,9.3336-8.7663,14.7186c-1.5873-0.2422-3.2123-0.3683-4.8662-0.3683
c-17.7158,0-32.1289,14.4131-32.1289,32.1289c0,14.6854,9.9077,27.0922,23.3869,30.9101
c-6.7762,17.3461-23.6572,29.6719-43.3742,29.6719c-16.8195,0-31.583-8.9662-39.7656-22.369
c-2.4778,0.5446-5.0429,0.8519-7.6721,0.9023c9.0226,16.99,26.8969,28.5917,47.4377,28.5917
c23.2646,0,43.1121-14.8788,50.5461-35.6179c0.5204,0.0251,1.0435,0.0398,1.5701,0.0398c17.7158,0,32.1289-14.4131,32.1289-32.1289
c0-13.557-8.4446-25.1712-20.3465-29.8811c1.9001-4.5678,4.5115-8.7646,7.6888-12.4641c0.9996,0.4404,2.0479,0.785,3.1324,1.0384
c1.3144,0.3071,2.6773,0.486,4.0839,0.486C340.8428,63.7032,348.9043,55.6416,348.9043,45.7325z M304.2461,129.5279
c-13.7871,0-25.0039-11.2168-25.0039-25.0039s11.2168-25.0039,25.0039-25.0039S329.25,90.7369,329.25,104.524
S318.0332,129.5279,304.2461,129.5279z M330.9336,34.8872c0.645,0,1.2737,0.0671,1.8881,0.1755
c5.0818,0.8974,8.9576,5.3347,8.9576,10.6697c0,5.9805-4.8652,10.8457-10.8457,10.8457s-10.8457-4.8652-10.8457-10.8457
c0-1.3967,0.2746-2.7282,0.7576-3.9555C322.4306,37.7496,326.35,34.8872,330.9336,34.8872z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 437.4294 209.6185" style="enable-background:new 0 0 437.4294 209.6185;" xml:space="preserve">
<path d="M95.1109,128.1089H58.6797V65.6861c0-1.5234-0.8169-2.4331-2.1855-2.4331h-3.1187c-1.3159,0-2.2349,1.0005-2.2349,2.4331
v67.4297c0,1.4521,0.8145,2.2852,2.2349,2.2852h41.7353c1.4844,0,2.4819-0.9375,2.4819-2.333v-2.7744
C97.5928,128.9253,96.6651,128.1089,95.1109,128.1089z"/>
<path d="M146.4561,77.1739c-4.8252-3.001-10.3037-4.5249-16.2837-4.5288c-0.0068,0-0.0137,0-0.0205,0
c-5.7349,0-11.1377,1.4639-16.0566,4.3511c-4.916,2.8853-8.8721,6.8364-11.7593,11.7456
c-2.8975,4.9248-4.3687,10.332-4.3721,16.0713c-0.0034,5.7188,1.4966,11.0654,4.4565,15.8887
c2.9893,4.9209,6.8789,8.7334,11.8887,11.6514c4.8657,2.8633,10.2397,4.3174,15.9717,4.3203c0.0073,0,0.0146,0,0.022,0
c8.123,0,14.7441-2.5869,21.4683-8.3906c0.5493-0.4805,0.8516-1.1201,0.8516-1.8008c0.001-0.6074-0.1743-1.1035-0.5205-1.4756
l-1.3569-1.8428l-0.0732-0.0859c-0.2637-0.2637-0.6929-0.6152-1.3716-0.6152c-0.6421,0-1.2549,0.2217-1.7124,0.6143
c-1.9346,1.585-3.5459,2.8008-4.7969,3.6182c-1.7979,1.208-5.8218,3.2314-12.5986,3.2314c-0.0073,0-0.0142,0-0.021,0
c-0.1357,0.0029-0.269,0.0039-0.4043,0.0039c-12.2642,0-23.4736-10.3262-24.5088-22.4814l53.0127,0.0322c0.0015,0,0.0024,0,0.0034,0
c2.2373,0,3.4697-1.1621,3.4712-3.2715c0.0034-5.2588-1.3574-10.3945-4.0464-15.2705
C155.0015,84.0953,151.2188,80.1363,146.4561,77.1739z M154.6451,100.4341l-49.1157-0.0293
c1.9834-11.6631,13.0181-21.2417,24.5034-21.0117c0.1387-0.0024,0.2754-0.0039,0.4136-0.0039
C143.4629,79.3892,152.4722,90.0162,154.6451,100.4341z"/>
<path d="M204.0386,136.6382c5.7319,0,11.1069-1.4502,15.9746-4.3115c4.938-2.9014,8.75-6.7129,11.6533-11.6533
c2.8608-4.8672,4.311-10.2578,4.311-16.0244c0-5.7324-1.4502-11.1064-4.311-15.9746c-2.9019-4.9385-6.7134-8.75-11.6533-11.6533
c-4.8687-2.8618-10.2437-4.3125-15.9746-4.3125c-9.938,0-19.2021,4.7583-24.3516,12.3174v-9.438
c0-0.5946-0.1465-1.0788-0.411-1.4511c-0.3815-0.5369-1.0157-0.834-1.8727-0.834h-2.6738c-1.4521,0-2.2852,0.833-2.2852,2.2852
v5.6964v46.4791v23.9676c0,1.2568,0.7808,2.0371,2.0371,2.0371h3.3667c0.9209,0,1.6421-0.6992,1.6421-1.5908v-17.098v-10.984
C185.0884,131.8892,194.2749,136.6382,204.0386,136.6382z M186.6358,122.5591c-4.9346-4.9346-7.6831-11.4932-7.542-18.0254
c-0.1367-6.3506,2.5439-12.751,7.3545-17.5605c4.8521-4.8521,11.3037-7.5547,17.7383-7.417c4.3691,0,8.4863,1.1465,12.2314,3.4043
c3.7344,2.2979,6.7456,5.4053,8.9492,9.2354c2.1699,3.9072,3.2695,8.0967,3.2695,12.4697
c0.1396,6.4619-2.5967,12.9844-7.5083,17.8955c-4.7617,4.7617-11.0469,7.3857-17.2544,7.2803
C197.6856,129.9712,191.396,127.3208,186.6358,122.5591z"/>
<path d="M241.8955,80.3975h7.5669v42.0259c0,6.8174,4.5674,12.1309,11.0825,12.9189c0.6836,0.1055,1.8379,0.1572,3.5303,0.1572
c2.0078,0,3.0273-0.3535,3.0273-2.2842v-2.377c0-1.7891-1.334-2.0371-2.7568-2.0371c0,0-0.001,0-0.002,0l-1.7871-0.0488
c-2.0117-0.0439-3.4883-0.7627-4.3896-2.1367c-0.9697-1.4805-1.4619-3.1738-1.4619-5.0352V80.3975h10.0928
c1.3076,0,2.2852-1.3628,2.2852-2.5815v-1.9312c0-1.3999-0.8359-2.2354-2.2354-2.2354h-10.1426V60.6861
c0-1.4619-0.7969-2.4829-1.9375-2.4829c-0.1865,0-0.4121,0-0.6392,0.0884l-2.6489,0.6865
c-1.2109,0.3682-2.0171,0.9263-2.0171,2.4507v12.2207h-7.5669c-1.4185,0-2.335,0.897-2.335,2.2852v1.8813
C239.5606,79.2393,240.6079,80.3975,241.8955,80.3975z"/>
<path d="M379.1182,106.2691c-4.0488-2.9219-8.8545-5.0293-14.291-6.2646c-6.5049-1.3975-13.4473-5.2129-13.3203-10.3066
c0-7.5225,6.6367-10.1914,12.3203-10.1914c5.3574,0,10.2207,3.002,13.001,8.0146c0.6729,1.2861,1.4785,1.9375,2.3955,1.9375
c0.3311,0,0.7061-0.1113,0.9922-0.2832l2.2021-1.1523c0.5947-0.3408,0.9229-0.9414,0.9229-1.6924
c0-0.5205-0.0908-0.9541-0.2617-1.292c-3.6367-8.2466-10.0967-12.4282-19.2021-12.4282c-11.7305,0-19.6123,6.9263-19.6123,17.2349
c0,4.3125,1.8438,7.9746,5.4756,10.8809c3.4482,2.7979,7.9121,4.8623,13.2705,6.1377c4.5859,1.085,8.3193,2.5654,11.0977,4.4023
c1.4159,0.9354,2.4412,2.0535,3.106,3.3672c0.6053,1.1962,0.9135,2.5535,0.9135,4.1005c0.0742,2.3857-0.79,4.5176-2.5684,6.3389
c-3.1445,3.2178-8.4053,4.6689-12.0205,4.6689c-0.0361,0-0.0723,0-0.1074,0c-3.4268,0-6.4893-0.8438-9.1035-2.5068
c-2.5918-1.6484-4.2363-3.8076-5.0293-6.6064c-0.3203-1.0996-0.751-2.1738-2.1553-2.1738c-0.0742,0-0.2109,0.0146-0.4062,0.0449
c-0.1133,0.0166-0.2559,0.0381-0.5088,0.0742l-1.8818,0.4463l-0.1045,0.0332c-1.0244,0.4082-1.6113,1.1846-1.6113,2.1309
c0,0.2285,0.0625,0.6592,0.2178,1.1094c1.9707,8.5801,10.2432,14.3447,20.5732,14.3447c0.125,0.002,0.249,0.002,0.374,0.002
c6.5947,0,12.6748-2.3193,16.7275-6.3945c3.1895-3.208,4.8311-7.2363,4.748-11.6357c0-2.8187-0.6185-5.3109-1.8062-7.481
C382.4437,109.2624,381.0062,107.631,379.1182,106.2691z"/>
<path d="M348.9043,45.7325c0-6.3157-3.2826-11.8699-8.2238-15.0756c-2.811-1.8237-6.1537-2.8947-9.7469-2.8947
c-9.9092,0-17.9707,8.0615-17.9707,17.9702c0,4.7659,1.8775,9.0925,4.9157,12.3123c-3.6619,4.3709-6.6334,9.3336-8.7663,14.7186
c-1.5873-0.2422-3.2123-0.3683-4.8662-0.3683c-17.7158,0-32.1289,14.4131-32.1289,32.1289c0,14.6854,9.9077,27.0922,23.3869,30.9101
c-6.7762,17.3461-23.6572,29.6719-43.3742,29.6719c-16.8195,0-31.583-8.9662-39.7656-22.369
c-2.4778,0.5446-5.0429,0.8519-7.6721,0.9023c9.0226,16.99,26.8969,28.5917,47.4377,28.5917
c23.2646,0,43.1121-14.8788,50.5461-35.6179c0.5204,0.0251,1.0435,0.0398,1.5701,0.0398c17.7158,0,32.1289-14.4131,32.1289-32.1289
c0-13.557-8.4446-25.1712-20.3465-29.8811c1.9001-4.5678,4.5115-8.7646,7.6888-12.4641c0.9996,0.4404,2.0479,0.785,3.1324,1.0384
c1.3144,0.3071,2.6773,0.486,4.0839,0.486C340.8428,63.7032,348.9043,55.6416,348.9043,45.7325z M304.2461,129.5279
c-13.7871,0-25.0039-11.2168-25.0039-25.0039s11.2168-25.0039,25.0039-25.0039S329.25,90.7369,329.25,104.524
S318.0332,129.5279,304.2461,129.5279z M330.9336,34.8872c0.645,0,1.2737,0.0671,1.8881,0.1755
c5.0818,0.8974,8.9576,5.3347,8.9576,10.6697c0,5.9805-4.8652,10.8457-10.8457,10.8457s-10.8457-4.8652-10.8457-10.8457
c0-1.3967,0.2746-2.7282,0.7576-3.9555C322.4306,37.7496,326.35,34.8872,330.9336,34.8872z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 437.4294 209.6185" style="enable-background:new 0 0 437.4294 209.6185;" xml:space="preserve">
<path style="fill:#FFFFFF;" d="M95.1109,128.1089H58.6797V65.6861c0-1.5234-0.8169-2.4331-2.1855-2.4331h-3.1187
c-1.3159,0-2.2349,1.0005-2.2349,2.4331v67.4297c0,1.4521,0.8145,2.2852,2.2349,2.2852h41.7353c1.4844,0,2.4819-0.9375,2.4819-2.333
v-2.7744C97.5928,128.9253,96.6651,128.1089,95.1109,128.1089z"/>
<path style="fill:#FFFFFF;" d="M146.4561,77.1739c-4.8252-3.001-10.3037-4.5249-16.2837-4.5288c-0.0068,0-0.0137,0-0.0205,0
c-5.7349,0-11.1377,1.4639-16.0566,4.3511c-4.916,2.8853-8.8721,6.8364-11.7593,11.7456
c-2.8975,4.9248-4.3687,10.332-4.3721,16.0713c-0.0034,5.7188,1.4966,11.0654,4.4565,15.8887
c2.9893,4.9209,6.8789,8.7334,11.8887,11.6514c4.8657,2.8633,10.2397,4.3174,15.9717,4.3203c0.0073,0,0.0146,0,0.022,0
c8.123,0,14.7441-2.5869,21.4683-8.3906c0.5493-0.4805,0.8516-1.1201,0.8516-1.8008c0.001-0.6074-0.1743-1.1035-0.5205-1.4756
l-1.3569-1.8428l-0.0732-0.0859c-0.2637-0.2637-0.6929-0.6152-1.3716-0.6152c-0.6421,0-1.2549,0.2217-1.7124,0.6143
c-1.9346,1.585-3.5459,2.8008-4.7969,3.6182c-1.7979,1.208-5.8218,3.2314-12.5986,3.2314c-0.0073,0-0.0142,0-0.021,0
c-0.1357,0.0029-0.269,0.0039-0.4043,0.0039c-12.2642,0-23.4736-10.3262-24.5088-22.4814l53.0127,0.0322c0.0015,0,0.0024,0,0.0034,0
c2.2373,0,3.4697-1.1621,3.4712-3.2715c0.0034-5.2588-1.3574-10.3945-4.0464-15.2705
C155.0015,84.0953,151.2188,80.1363,146.4561,77.1739z M154.6451,100.4341l-49.1157-0.0293
c1.9834-11.6631,13.0181-21.2417,24.5034-21.0117c0.1387-0.0024,0.2754-0.0039,0.4136-0.0039
C143.4629,79.3892,152.4722,90.0162,154.6451,100.4341z"/>
<path style="fill:#FFFFFF;" d="M204.0386,136.6382c5.7319,0,11.1069-1.4502,15.9746-4.3115
c4.938-2.9014,8.75-6.7129,11.6533-11.6533c2.8608-4.8672,4.311-10.2578,4.311-16.0244c0-5.7324-1.4502-11.1064-4.311-15.9746
c-2.9019-4.9385-6.7134-8.75-11.6533-11.6533c-4.8687-2.8618-10.2437-4.3125-15.9746-4.3125
c-9.938,0-19.2021,4.7583-24.3516,12.3174v-9.438c0-0.5946-0.1465-1.0788-0.411-1.4511c-0.3815-0.5369-1.0157-0.834-1.8727-0.834
h-2.6738c-1.4521,0-2.2852,0.833-2.2852,2.2852v5.6964v46.4791v23.9676c0,1.2568,0.7808,2.0371,2.0371,2.0371h3.3667
c0.9209,0,1.6421-0.6992,1.6421-1.5908v-17.098v-10.984C185.0884,131.8892,194.2749,136.6382,204.0386,136.6382z M186.6358,122.5591
c-4.9346-4.9346-7.6831-11.4932-7.542-18.0254c-0.1367-6.3506,2.5439-12.751,7.3545-17.5605
c4.8521-4.8521,11.3037-7.5547,17.7383-7.417c4.3691,0,8.4863,1.1465,12.2314,3.4043c3.7344,2.2979,6.7456,5.4053,8.9492,9.2354
c2.1699,3.9072,3.2695,8.0967,3.2695,12.4697c0.1396,6.4619-2.5967,12.9844-7.5083,17.8955
c-4.7617,4.7617-11.0469,7.3857-17.2544,7.2803C197.6856,129.9712,191.396,127.3208,186.6358,122.5591z"/>
<path style="fill:#FFFFFF;" d="M241.8955,80.3975h7.5669v42.0259c0,6.8174,4.5674,12.1309,11.0825,12.9189
c0.6836,0.1055,1.8379,0.1572,3.5303,0.1572c2.0078,0,3.0273-0.3535,3.0273-2.2842v-2.377c0-1.7891-1.334-2.0371-2.7568-2.0371
c0,0-0.001,0-0.002,0l-1.7871-0.0488c-2.0117-0.0439-3.4883-0.7627-4.3896-2.1367c-0.9697-1.4805-1.4619-3.1738-1.4619-5.0352
V80.3975h10.0928c1.3076,0,2.2852-1.3628,2.2852-2.5815v-1.9312c0-1.3999-0.8359-2.2354-2.2354-2.2354h-10.1426V60.6861
c0-1.4619-0.7969-2.4829-1.9375-2.4829c-0.1865,0-0.4121,0-0.6392,0.0884l-2.6489,0.6865
c-1.2109,0.3682-2.0171,0.9263-2.0171,2.4507v12.2207h-7.5669c-1.4185,0-2.335,0.897-2.335,2.2852v1.8813
C239.5606,79.2393,240.6079,80.3975,241.8955,80.3975z"/>
<path style="fill:#FFFFFF;" d="M379.1182,106.2691c-4.0488-2.9219-8.8545-5.0293-14.291-6.2646
c-6.5049-1.3975-13.4473-5.2129-13.3203-10.3066c0-7.5225,6.6367-10.1914,12.3203-10.1914c5.3574,0,10.2207,3.002,13.001,8.0146
c0.6729,1.2861,1.4785,1.9375,2.3955,1.9375c0.3311,0,0.7061-0.1113,0.9922-0.2832l2.2021-1.1523
c0.5947-0.3408,0.9229-0.9414,0.9229-1.6924c0-0.5205-0.0908-0.9541-0.2617-1.292c-3.6367-8.2466-10.0967-12.4282-19.2021-12.4282
c-11.7305,0-19.6123,6.9263-19.6123,17.2349c0,4.3125,1.8438,7.9746,5.4756,10.8809c3.4482,2.7979,7.9121,4.8623,13.2705,6.1377
c4.5859,1.085,8.3193,2.5654,11.0977,4.4023c1.4159,0.9354,2.4412,2.0535,3.106,3.3672c0.6053,1.1962,0.9135,2.5535,0.9135,4.1005
c0.0742,2.3857-0.79,4.5176-2.5684,6.3389c-3.1445,3.2178-8.4053,4.6689-12.0205,4.6689c-0.0361,0-0.0723,0-0.1074,0
c-3.4268,0-6.4893-0.8438-9.1035-2.5068c-2.5918-1.6484-4.2363-3.8076-5.0293-6.6064c-0.3203-1.0996-0.751-2.1738-2.1553-2.1738
c-0.0742,0-0.2109,0.0146-0.4062,0.0449c-0.1133,0.0166-0.2559,0.0381-0.5088,0.0742l-1.8818,0.4463l-0.1045,0.0332
c-1.0244,0.4082-1.6113,1.1846-1.6113,2.1309c0,0.2285,0.0625,0.6592,0.2178,1.1094c1.9707,8.5801,10.2432,14.3447,20.5732,14.3447
c0.125,0.002,0.249,0.002,0.374,0.002c6.5947,0,12.6748-2.3193,16.7275-6.3945c3.1895-3.208,4.8311-7.2363,4.748-11.6357
c0-2.8187-0.6185-5.3109-1.8062-7.481C382.4437,109.2624,381.0062,107.631,379.1182,106.2691z"/>
<path style="fill:#FFFFFF;" d="M348.9043,45.7325c0-6.3157-3.2826-11.8699-8.2238-15.0756
c-2.811-1.8237-6.1537-2.8947-9.7469-2.8947c-9.9092,0-17.9707,8.0615-17.9707,17.9702c0,4.7659,1.8775,9.0925,4.9157,12.3123
c-3.6619,4.3709-6.6334,9.3336-8.7663,14.7186c-1.5873-0.2422-3.2123-0.3683-4.8662-0.3683
c-17.7158,0-32.1289,14.4131-32.1289,32.1289c0,14.6854,9.9077,27.0922,23.3869,30.9101
c-6.7762,17.3461-23.6572,29.6719-43.3742,29.6719c-16.8195,0-31.583-8.9662-39.7656-22.369
c-2.4778,0.5446-5.0429,0.8519-7.6721,0.9023c9.0226,16.99,26.8969,28.5917,47.4377,28.5917
c23.2646,0,43.1121-14.8788,50.5461-35.6179c0.5204,0.0251,1.0435,0.0398,1.5701,0.0398c17.7158,0,32.1289-14.4131,32.1289-32.1289
c0-13.557-8.4446-25.1712-20.3465-29.8811c1.9001-4.5678,4.5115-8.7646,7.6888-12.4641c0.9996,0.4404,2.0479,0.785,3.1324,1.0384
c1.3144,0.3071,2.6773,0.486,4.0839,0.486C340.8428,63.7032,348.9043,55.6416,348.9043,45.7325z M304.2461,129.5279
c-13.7871,0-25.0039-11.2168-25.0039-25.0039s11.2168-25.0039,25.0039-25.0039S329.25,90.7369,329.25,104.524
S318.0332,129.5279,304.2461,129.5279z M330.9336,34.8872c0.645,0,1.2737,0.0671,1.8881,0.1755
c5.0818,0.8974,8.9576,5.3347,8.9576,10.6697c0,5.9805-4.8652,10.8457-10.8457,10.8457s-10.8457-4.8652-10.8457-10.8457
c0-1.3967,0.2746-2.7282,0.7576-3.9555C322.4306,37.7496,326.35,34.8872,330.9336,34.8872z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 115.9988 115.9988" style="enable-background:new 0 0 115.9988 115.9988;" xml:space="preserve">
<g>
<g>
<g>
<path style="fill:#180D38;" d="M29.1281,108.2941c9.5736-4.5548,17.1531-12.6456,21.0335-22.5787
c-12.0865-3.4232-20.9707-14.548-20.9707-27.7159c0-15.8849,12.9236-28.8085,28.8085-28.8085
c1.4832,0,2.9404,0.113,4.3639,0.3303c1.9125-4.8287,4.5771-9.2786,7.8607-13.1979c-2.7243-2.8871-4.4077-6.7665-4.4077-11.0399
c0-1.6191,0.2457-3.1808,0.6921-4.6562C63.7305,0.2186,60.8908,0,57.9995,0C25.9672,0,0,25.9672,0,57.9994
C0,79.5165,11.7263,98.2828,29.1281,108.2941z"/>
<path style="fill:#EF3939;" d="M81.9297,15.0082c3.6788,0,6.886-2.0536,8.5379-5.0742
c-5.3185-3.5997-11.2684-6.3339-17.646-8.0151c-0.3903,1.0504-0.6168,2.1798-0.6168,3.3644
C72.2049,10.6458,76.5673,15.0082,81.9297,15.0082z"/>
<path style="fill:#180D38;" d="M95.5663,13.828c-2.8537,4.5375-7.8918,7.5688-13.6366,7.5688
c-1.2614,0-2.4835-0.1604-3.6622-0.4359c-0.9722-0.2272-1.9121-0.5362-2.8083-0.931c-2.8492,3.3173-5.1907,7.0806-6.8945,11.1766
c10.6715,4.2233,18.2432,14.6371,18.2432,26.7928c0,15.8849-12.9235,28.8085-28.8085,28.8085
c-0.4718,0-0.9406-0.0131-1.4069-0.0357c-3.7532,10.4704-11.0354,19.2744-20.406,24.9696
c6.7355,2.7367,14.0948,4.257,21.8129,4.257c32.0322,0,57.9994-25.9672,57.9994-57.9995
C115.9988,40.3018,108.0628,24.4664,95.5663,13.828z"/>
<circle style="fill:#EF3939;" cx="57.9994" cy="57.9994" r="22.4198"/>
</g>
<path style="fill:#FFFFFF;" d="M78.2676,20.961c1.1786,0.2755,2.4008,0.4359,3.6622,0.4359
c5.7448,0,10.7829-3.0313,13.6366-7.5688c-1.6275-1.3855-3.3236-2.6925-5.0987-3.894c-1.6519,3.0206-4.8591,5.0742-8.5379,5.0742
c-5.3624,0-9.7249-4.3624-9.7249-9.7249c0-1.1846,0.2264-2.3141,0.6168-3.3644c-2.062-0.5436-4.1682-0.9763-6.3133-1.2917
c-0.4464,1.4753-0.6921,3.0371-0.6921,4.6562c0,4.2734,1.6834,8.1528,4.4077,11.0399c-3.2836,3.9193-5.9482,8.3692-7.8607,13.1979
c-1.4235-0.2172-2.8807-0.3303-4.3639-0.3303c-15.8849,0-28.8085,12.9235-28.8085,28.8085
c0,13.168,8.8842,24.2928,20.9707,27.7159c-3.8804,9.9332-11.4599,18.0239-21.0335,22.5787
c2.2621,1.3013,4.6175,2.456,7.0584,3.4478c9.3706-5.6952,16.6528-14.4992,20.406-24.9696
c0.4663,0.0226,0.9351,0.0357,1.4069,0.0357c15.8849,0,28.8085-12.9236,28.8085-28.8085c0-12.1557-7.5717-22.5695-18.2432-26.7928
c1.7038-4.0959,4.0453-7.8593,6.8945-11.1766C76.3555,20.4248,77.2953,20.7338,78.2676,20.961z M80.4193,57.9994
c0,12.3623-10.0576,22.4199-22.4198,22.4199c-12.3623,0-22.4199-10.0576-22.4199-22.4199
c0-12.3622,10.0576-22.4199,22.4199-22.4199C70.3617,35.5795,80.4193,45.6371,80.4193,57.9994z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 115.5026 115.5026" style="enable-background:new 0 0 115.5026 115.5026;" xml:space="preserve">
<path style="fill:#181139;" d="M115.5026,0h-13.957c0.0002,0.0315,0.0031,0.0623,0.0031,0.0938
c0,9.718-7.9059,17.6239-17.6239,17.6239c-1.3796,0-2.7163-0.1754-4.0055-0.4767c-1.0634-0.2485-2.0913-0.5864-3.0715-1.0182
c-3.1162,3.6283-5.6772,7.7443-7.5408,12.2242c11.6719,4.6192,19.9532,16.0091,19.9532,29.3043
c0,17.374-14.1349,31.5089-31.5089,31.5089c-0.5161,0-1.0288-0.0143-1.5388-0.039c-3.8856,10.8397-11.2302,20.0454-20.6959,26.2814
h79.986V0z"/>
<circle style="fill:#EF3939;" cx="57.7513" cy="57.7513" r="24.5214"/>
<path style="fill:#181139;" d="M49.1788,88.0652c-13.2195-3.744-22.9364-15.9116-22.9364-30.3139
c0-17.374,14.1349-31.5089,31.5089-31.5089c1.6223,0,3.2161,0.1237,4.7729,0.3612c2.0918-5.2813,5.0061-10.1484,8.5975-14.4351
c-2.9796-3.1577-4.8209-7.4008-4.8209-12.0747c0-0.0317,0.0046-0.0622,0.0048-0.0938H0v115.5026h18.8623
C32.7495,111.6378,43.9877,101.3537,49.1788,88.0652z"/>
<path style="fill:#EF3939;" d="M83.9248,10.7302c5.8651,0,10.6364-4.7714,10.6364-10.6364c0-0.0316-0.004-0.0623-0.0043-0.0938
H73.293c-0.0003,0.0316-0.0046,0.0621-0.0046,0.0938C73.2884,5.9589,78.0598,10.7302,83.9248,10.7302z"/>
<path style="fill:#FFFFFF;" d="M56.2125,89.2212c0.51,0.0247,1.0228,0.039,1.5388,0.039c17.374,0,31.5089-14.1349,31.5089-31.5089
c0-13.2952-8.2814-24.6851-19.9532-29.3043c1.8635-4.4799,4.4246-8.5959,7.5408-12.2242c0.9802,0.4318,2.0082,0.7698,3.0715,1.0182
c1.2892,0.3013,2.6259,0.4767,4.0055,0.4767c9.718,0,17.6239-7.9059,17.6239-17.6239c0-0.0315-0.0029-0.0623-0.0031-0.0938h-6.9887
c0.0003,0.0316,0.0043,0.0622,0.0043,0.0938c0,5.8651-4.7714,10.6364-10.6364,10.6364S73.2884,5.9589,73.2884,0.0938
c0-0.0317,0.0043-0.0622,0.0046-0.0938h-6.9874c-0.0002,0.0316-0.0048,0.0621-0.0048,0.0938c0,4.674,1.8413,8.9171,4.8209,12.0747
c-3.5914,4.2867-6.5057,9.1537-8.5975,14.4351c-1.5569-0.2375-3.1507-0.3612-4.7729-0.3612
c-17.374,0-31.5089,14.1349-31.5089,31.5089c0,14.4023,9.7169,26.5699,22.9364,30.3139
c-5.1912,13.2885-16.4293,23.5726-30.3165,27.4374h16.6543C44.9824,109.2666,52.327,100.0609,56.2125,89.2212z M33.2299,57.7513
c0-13.5211,11.0004-24.5214,24.5214-24.5214s24.5214,11.0004,24.5214,24.5214S71.2724,82.2727,57.7513,82.2727
S33.2299,71.2723,33.2299,57.7513z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 437.4 209.6" style="enable-background:new 0 0 437.4 209.6;" xmlns="http://www.w3.org/2000/svg">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#EF3939;}
</style>
<path class="st0" d="M95.1,128.1H58.7V65.7c0-1.5-0.8-2.4-2.2-2.4h-3.1c-1.3,0-2.2,1-2.2,2.4v67.4c0,1.5,0.8,2.3,2.2,2.3h41.7 c1.5,0,2.5-0.9,2.5-2.3v-2.8C97.6,128.9,96.7,128.1,95.1,128.1z"/>
<path class="st0" d="M146.5,77.2c-4.8-3-10.3-4.5-16.3-4.5c0,0,0,0,0,0c-5.7,0-11.1,1.5-16.1,4.4c-4.9,2.9-8.9,6.8-11.8,11.7 c-2.9,4.9-4.4,10.3-4.4,16.1c0,5.7,1.5,11.1,4.5,15.9c3,4.9,6.9,8.7,11.9,11.7c4.9,2.9,10.2,4.3,16,4.3c0,0,0,0,0,0 c8.1,0,14.7-2.6,21.5-8.4c0.5-0.5,0.9-1.1,0.9-1.8c0-0.6-0.2-1.1-0.5-1.5l-1.4-1.8l-0.1-0.1c-0.3-0.3-0.7-0.6-1.4-0.6 c-0.6,0-1.3,0.2-1.7,0.6c-1.9,1.6-3.5,2.8-4.8,3.6c-1.8,1.2-5.8,3.2-12.6,3.2c0,0,0,0,0,0c-0.1,0-0.3,0-0.4,0 c-12.3,0-23.5-10.3-24.5-22.5l53,0c0,0,0,0,0,0c2.2,0,3.5-1.2,3.5-3.3c0-5.3-1.4-10.4-4-15.3C155,84.1,151.2,80.1,146.5,77.2z M154.6,100.4l-49.1,0c2-11.7,13-21.2,24.5-21c0.1,0,0.3,0,0.4,0C143.5,79.4,152.5,90,154.6,100.4z"/>
<path class="st0" d="M204,136.6c5.7,0,11.1-1.5,16-4.3c4.9-2.9,8.8-6.7,11.7-11.7c2.9-4.9,4.3-10.3,4.3-16c0-5.7-1.5-11.1-4.3-16 c-2.9-4.9-6.7-8.8-11.7-11.7c-4.9-2.9-10.2-4.3-16-4.3c-9.9,0-19.2,4.8-24.4,12.3v-9.4c0-0.6-0.1-1.1-0.4-1.5 c-0.4-0.5-1-0.8-1.9-0.8h-2.7c-1.5,0-2.3,0.8-2.3,2.3v5.7v46.5v24c0,1.3,0.8,2,2,2h3.4c0.9,0,1.6-0.7,1.6-1.6v-17.1v-11 C185.1,131.9,194.3,136.6,204,136.6z M186.6,122.6c-4.9-4.9-7.7-11.5-7.5-18c-0.1-6.4,2.5-12.8,7.4-17.6c4.9-4.9,11.3-7.6,17.7-7.4 c4.4,0,8.5,1.1,12.2,3.4c3.7,2.3,6.7,5.4,8.9,9.2c2.2,3.9,3.3,8.1,3.3,12.5c0.1,6.5-2.6,13-7.5,17.9c-4.8,4.8-11,7.4-17.3,7.3 C197.7,130,191.4,127.3,186.6,122.6z"/>
<path class="st0" d="M241.9,80.4h7.6v42c0,6.8,4.6,12.1,11.1,12.9c0.7,0.1,1.8,0.2,3.5,0.2c2,0,3-0.4,3-2.3v-2.4c0-1.8-1.3-2-2.8-2 c0,0,0,0,0,0l-1.8,0c-2,0-3.5-0.8-4.4-2.1c-1-1.5-1.5-3.2-1.5-5V80.4h10.1c1.3,0,2.3-1.4,2.3-2.6v-1.9c0-1.4-0.8-2.2-2.2-2.2h-10.1 v-13c0-1.5-0.8-2.5-1.9-2.5c-0.2,0-0.4,0-0.6,0.1l-2.6,0.7c-1.2,0.4-2,0.9-2,2.5v12.2h-7.6c-1.4,0-2.3,0.9-2.3,2.3v1.9 C239.6,79.2,240.6,80.4,241.9,80.4z"/>
<path class="st0" d="M379.1,106.3c-4-2.9-8.9-5-14.3-6.3c-6.5-1.4-13.4-5.2-13.3-10.3c0-7.5,6.6-10.2,12.3-10.2c5.4,0,10.2,3,13,8 c0.7,1.3,1.5,1.9,2.4,1.9c0.3,0,0.7-0.1,1-0.3l2.2-1.2c0.6-0.3,0.9-0.9,0.9-1.7c0-0.5-0.1-1-0.3-1.3c-3.6-8.2-10.1-12.4-19.2-12.4 c-11.7,0-19.6,6.9-19.6,17.2c0,4.3,1.8,8,5.5,10.9c3.4,2.8,7.9,4.9,13.3,6.1c4.6,1.1,8.3,2.6,11.1,4.4c1.4,0.9,2.4,2.1,3.1,3.4 c0.6,1.2,0.9,2.6,0.9,4.1c0.1,2.4-0.8,4.5-2.6,6.3c-3.1,3.2-8.4,4.7-12,4.7c0,0-0.1,0-0.1,0c-3.4,0-6.5-0.8-9.1-2.5 c-2.6-1.6-4.2-3.8-5-6.6c-0.3-1.1-0.8-2.2-2.2-2.2c-0.1,0-0.2,0-0.4,0c-0.1,0-0.3,0-0.5,0.1l-1.9,0.4l-0.1,0c-1,0.4-1.6,1.2-1.6,2.1 c0,0.2,0.1,0.7,0.2,1.1c2,8.6,10.2,14.3,20.6,14.3c0.1,0,0.2,0,0.4,0c6.6,0,12.7-2.3,16.7-6.4c3.2-3.2,4.8-7.2,4.7-11.6 c0-2.8-0.6-5.3-1.8-7.5C382.4,109.3,381,107.6,379.1,106.3z"/>
<path class="st1" d="M348.9,45.7c0-6.3-3.3-11.9-8.2-15.1c-2.8-1.8-6.2-2.9-9.7-2.9c-9.9,0-18,8.1-18,18c0,4.8,1.9,9.1,4.9,12.3 c-3.7,4.4-6.6,9.3-8.8,14.7c-1.6-0.2-3.2-0.4-4.9-0.4c-17.7,0-32.1,14.4-32.1,32.1c0,14.7,9.9,27.1,23.4,30.9 c-6.8,17.3-23.7,29.7-43.4,29.7c-16.8,0-31.6-9-39.8-22.4c-2.5,0.5-5,0.9-7.7,0.9c9,17,26.9,28.6,47.4,28.6 c23.3,0,43.1-14.9,50.5-35.6c0.5,0,1,0,1.6,0c17.7,0,32.1-14.4,32.1-32.1c0-13.6-8.4-25.2-20.3-29.9c1.9-4.6,4.5-8.8,7.7-12.5 c1,0.4,2,0.8,3.1,1c1.3,0.3,2.7,0.5,4.1,0.5C340.8,63.7,348.9,55.6,348.9,45.7z M304.2,129.5c-13.8,0-25-11.2-25-25s11.2-25,25-25 s25,11.2,25,25S318,129.5,304.2,129.5z M330.9,34.9c0.6,0,1.3,0.1,1.9,0.2c5.1,0.9,9,5.3,9,10.7c0,6-4.9,10.8-10.8,10.8 s-10.8-4.9-10.8-10.8c0-1.4,0.3-2.7,0.8-4C322.4,37.7,326.4,34.9,330.9,34.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -2,6 +2,7 @@
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -23,7 +23,7 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "2"
simple_logger = "4.0.0"
gloo-net = { git = "https://github.com/rustwasm/gloo" }
[features]
@@ -54,7 +54,7 @@ site-pkg-dir = "pkg"
# [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 <site-root>/<site-pkg>/app.css
# style-file = "src/styles/tailwind.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
# assets-dir = "static/assets"
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
# The port to use for automatic reload monitoring

View File

@@ -25,7 +25,7 @@ cargo leptos build --release
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"pkg"`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,5 +1,6 @@
use leptos::*;
use leptos_router::*;
use leptos_meta::*;
#[cfg(feature = "ssr")]
use std::sync::atomic::{AtomicI32, Ordering};
@@ -44,6 +45,7 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
}
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
view! {
cx,
<Router>
@@ -59,6 +61,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
<li><A href="multi">"Multi-User"</A></li>
</ul>
</nav>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<main>
<Routes>
<Route path="" view=|cx| view! {
@@ -209,8 +212,8 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
};
#[cfg(feature = "ssr")]
let multiplayer_value =
create_signal_from_stream(cx, futures::stream::once(Box::pin(async { 0.to_string() })));
let (multiplayer_value, _) =
create_signal(cx, None::<i32>);
view! {
cx,

View File

@@ -9,6 +9,7 @@ cfg_if! {
use actix_files::{Files};
use actix_web::*;
use crate::counters::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
#[get("/api/events")]
async fn counter_events() -> impl Responder {
@@ -29,23 +30,25 @@ cfg_if! {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
crate::counters::register_server_functions();
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
let pkg_dir = &leptos_options.site_pkg_dir;
let bundle_path = format!("/{site_root}/{pkg_dir}");
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_address.clone();
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.service(Files::new("/pkg", "./pkg")) // used by wasm-pack and cargo run. Can be removed if using cargo-leptos
.service(Files::new(&bundle_path, format!("./{bundle_path}"))) // used by cargo-leptos. Can be removed if using wasm-pack and cargo run.
.service(counter_events)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream(leptos_options.to_owned(), |cx| view! { cx, <Counters/> }))
//.wrap(middleware::Compress::default())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <Counters/> })
.service(Files::new("/", &site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()

View File

@@ -2,6 +2,7 @@
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<style>
img {

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -19,7 +19,7 @@ leptos_meta = { path = "../../meta", default-features = false }
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "2"
simple_logger = "4.0.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
gloo-net = { version = "0.2", features = ["http"] }
@@ -56,7 +56,7 @@ site-pkg-dir = "pkg"
# [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 <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
# assets-dir = "static/assets"
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
# The port to use for automatic reload monitoring

View File

@@ -25,7 +25,7 @@ cargo leptos build --release
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"pkg"`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes..
1. Install wasm-pack
```bash
cargo install wasm-pack

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,4 +1,4 @@
use leptos::{on_cleanup, Scope, Serializable};
use leptos::{Scope, Serializable};
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
@@ -29,7 +29,7 @@ where
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
on_cleanup(cx, move || {
leptos::on_cleanup(cx, move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}

View File

@@ -15,7 +15,8 @@ pub fn App(cx: Scope) -> impl IntoView {
view! {
cx,
<>
<Stylesheet id="leptos" href="/target/site/pkg/hackernews.css"/>
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
@@ -23,7 +24,7 @@ pub fn App(cx: Scope) -> impl IntoView {
<Routes>
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
<Route path="*stories" view=|cx| view! { cx, <Stories/> }/>
<Route path=":stories?" view=|cx| view! { cx, <Stories/> }/>
</Routes>
</main>
</Router>

View File

@@ -8,28 +8,36 @@ cfg_if! {
use actix_files::{Files};
use actix_web::*;
use hackernews::{App,AppProps};
use leptos_actix::{LeptosRoutes, generate_route_list};
#[get("/style.css")]
async fn css() -> impl Responder {
actix_files::NamedFile::open_async("./style.css").await
}
#[get("/favicon.ico")]
async fn favicon() -> impl Responder {
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_address.clone();
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> });
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
let pkg_dir = &leptos_options.site_pkg_dir;
let bundle_path = format!("/{site_root}/{pkg_dir}");
App::new()
.service(Files::new("/pkg", "./pkg")) // used by wasm-pack and cargo run. Can be removed if using cargo-leptos
.service(Files::new(&bundle_path, format!("./{bundle_path}"))) // used by cargo-leptos. Can be removed if using wasm-pack and cargo run.
.service(css)
.service(favicon)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream(leptos_options.to_owned(), |cx| view! { cx, <App/> }))
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <App/> })
.service(Files::new("/", &site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?

View File

@@ -134,7 +134,7 @@ fn Story(cx: Scope, story: api::Story) -> impl IntoView {
view! { cx,
<span>
{"by "}
{story.user.map(|user| view ! { cx, <A href=format!("/users/{}", user)>{user.clone()}</A>})}
{story.user.map(|user| view ! { cx, <A href=format!("/users/{user}")>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {

View File

@@ -17,7 +17,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
}
},
);
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title.clone())).unwrap_or_else(|| "Loading story...".to_string());
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
view! { cx,
<>
@@ -37,7 +37,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
{story.user.map(|user| view! { cx, <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{}", user)>{user.clone()}</A>
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>

View File

@@ -64,7 +64,7 @@ site-pkg-dir = "pkg"
# [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 <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
# assets-dir = "static/assets"
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
# The port to use for automatic reload monitoring

View File

@@ -25,7 +25,7 @@ cargo leptos build --release
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"pkg"`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes..
1. Install wasm-pack
```bash
cargo install wasm-pack

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,41 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::LeptosOptions;
pub async fn file_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>) -> Result<Response<BoxBody>, (StatusCode, String)> {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await?;
match res.status() {
StatusCode::OK => Ok(res),
_ => Err((res.status(), "File Not Found".to_string()))
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
let root_path = format!("{root}");
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(&root_path).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}
}
}

View File

@@ -3,6 +3,7 @@ use leptos::{component, view, IntoView, Scope};
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod file;
pub mod handlers;
mod routes;
use routes::nav::*;
@@ -16,7 +17,8 @@ pub fn App(cx: Scope) -> impl IntoView {
view! {
cx,
<>
<Stylesheet id="leptos" href="./target/site/pkg/hackernews_axum.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
@@ -24,7 +26,7 @@ pub fn App(cx: Scope) -> impl IntoView {
<Routes>
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
<Route path="*stories" view=|cx| view! { cx, <Stories/> }/>
<Route path=":stories?" view=|cx| view! { cx, <Stories/> }/>
</Routes>
</main>
</Router>

View File

@@ -6,10 +6,12 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
Router,
error_handling::HandleError,
routing::get,
extract::Extension,
};
use http::StatusCode;
use tower_http::services::ServeDir;
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
use hackernews_axum::file::file_handler;
#[tokio::main]
async fn main() {
@@ -17,40 +19,17 @@ if #[cfg(feature = "ssr")] {
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let site_root = &leptos_options.site_root;
let pkg_dir = &leptos_options.site_pkg_dir;
// The URL path of the generated JS/WASM bundle from cargo-leptos
let bundle_path = format!("/{site_root}/{pkg_dir}");
// The filesystem path of the generated JS/WASM bundle from cargo-leptos
let bundle_filepath = format!("./{site_root}/{pkg_dir}");
let addr = leptos_options.site_address.clone();
log::debug!("serving at {addr}");
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// These are Tower Services that will serve files from the static and pkg repos.
// HandleError is needed as Axum requires services to implement Infallible Errors
// because all Errors are converted into Responses
let static_service = HandleError::new( ServeDir::new("./static"), handle_file_error);
let pkg_service =HandleError::new( ServeDir::new("./pkg"), handle_file_error);
let cargo_leptos_service = HandleError::new( ServeDir::new(&bundle_filepath), handle_file_error);
/// Convert the Errors from ServeDir to a type that implements IntoResponse
async fn handle_file_error(err: std::io::Error) -> (StatusCode, String) {
(
StatusCode::NOT_FOUND,
format!("File Not Found: {}", err),
)
}
// build our application with a route
let app = Router::new()
// `GET /` goes to `root`
.nest_service("/pkg", pkg_service) // Only need if using wasm-pack. Can be deleted if using cargo-leptos
.nest_service(&bundle_path, cargo_leptos_service) // Only needed if using cargo-leptos. Can be deleted if using wasm-pack and cargo-run
.nest_service("/static", static_service)
.fallback(leptos_axum::render_app_to_stream(leptos_options, |cx| view! { cx, <App/> }));
.route("/favicon.ico", get(file_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
.fallback(file_handler)
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -134,7 +134,7 @@ fn Story(cx: Scope, story: api::Story) -> impl IntoView {
view! { cx,
<span>
{"by "}
{story.user.map(|user| view ! { cx, <A href=format!("/users/{}", user)>{user.clone()}</A>})}
{story.user.map(|user| view ! { cx, <A href=format!("/users/{user}")>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {

View File

@@ -17,7 +17,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
}
},
);
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title.clone())).unwrap_or_else(|| "Loading story...".to_string());
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
view! { cx,
<>
@@ -37,7 +37,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
{story.user.map(|user| view! { cx, <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{}", user)>{user.clone()}</A>
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>

View File

@@ -1,326 +0,0 @@
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-size: 15px;
background-color: #f2f3f5;
margin: 0;
padding-top: 55px;
color: #34495e;
overflow-y: scroll
}
a {
color: #34495e;
text-decoration: none
}
.header {
background-color: #335d92;
position: fixed;
z-index: 999;
height: 55px;
top: 0;
left: 0;
right: 0
}
.header .inner {
max-width: 800px;
box-sizing: border-box;
margin: 0 auto;
padding: 15px 5px
}
.header a {
color: rgba(255, 255, 255, .8);
line-height: 24px;
transition: color .15s ease;
display: inline-block;
vertical-align: middle;
font-weight: 300;
letter-spacing: .075em;
margin-right: 1.8em
}
.header a:hover {
color: #fff
}
.header a.active {
color: #fff;
font-weight: 400
}
.header a:nth-child(6) {
margin-right: 0
}
.header .github {
color: #fff;
font-size: .9em;
margin: 0;
float: right
}
.logo {
width: 24px;
margin-right: 10px;
display: inline-block;
vertical-align: middle
}
.view {
max-width: 800px;
margin: 0 auto;
position: relative
}
.fade-enter-active,
.fade-exit-active {
transition: all .2s ease
}
.fade-enter,
.fade-exit-active {
opacity: 0
}
@media (max-width:860px) {
.header .inner {
padding: 15px 30px
}
}
@media (max-width:600px) {
.header .inner {
padding: 15px
}
.header a {
margin-right: 1em
}
.header .github {
display: none
}
}
.news-view {
padding-top: 45px
}
.news-list,
.news-list-nav {
background-color: #fff;
border-radius: 2px
}
.news-list-nav {
padding: 15px 30px;
position: fixed;
text-align: center;
top: 55px;
left: 0;
right: 0;
z-index: 998;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.news-list-nav .page-link {
margin: 0 1em
}
.news-list-nav .disabled {
color: #aaa
}
.news-list {
position: absolute;
margin: 30px 0;
width: 100%;
transition: all .5s cubic-bezier(.55, 0, .1, 1)
}
.news-list ul {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.news-list {
margin: 10px 0
}
}
.news-item {
background-color: #fff;
padding: 20px 30px 20px 80px;
border-bottom: 1px solid #eee;
position: relative;
line-height: 20px
}
.news-item .score {
color: #335d92;
font-size: 1.1em;
font-weight: 700;
position: absolute;
top: 50%;
left: 0;
width: 80px;
text-align: center;
margin-top: -10px
}
.news-item .host,
.news-item .meta {
font-size: .85em;
color: #626262
}
.news-item .host a,
.news-item .meta a {
color: #626262;
text-decoration: underline
}
.news-item .host a:hover,
.news-item .meta a:hover {
color: #335d92
}
.item-view-header {
background-color: #fff;
padding: 1.8em 2em 1em;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.item-view-header h1 {
display: inline;
font-size: 1.5em;
margin: 0;
margin-right: .5em
}
.item-view-header .host,
.item-view-header .meta,
.item-view-header .meta a {
color: #626262
}
.item-view-header .meta a {
text-decoration: underline
}
.item-view-comments {
background-color: #fff;
margin-top: 10px;
padding: 0 2em .5em
}
.item-view-comments-header {
margin: 0;
font-size: 1.1em;
padding: 1em 0;
position: relative
}
.item-view-comments-header .spinner {
display: inline-block;
margin: -15px 0
}
.comment-children {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.item-view-header h1 {
font-size: 1.25em
}
}
.comment-children .comment-children {
margin-left: 1.5em
}
.comment {
border-top: 1px solid #eee;
position: relative
}
.comment .by,
.comment .text,
.comment .toggle {
font-size: .9em;
margin: 1em 0
}
.comment .by {
color: #626262
}
.comment .by a {
color: #626262;
text-decoration: underline
}
.comment .text {
overflow-wrap: break-word
}
.comment .text a:hover {
color: #335d92
}
.comment .text pre {
white-space: pre-wrap
}
.comment .toggle {
background-color: #fffbf2;
padding: .3em .5em;
border-radius: 4px
}
.comment .toggle a {
color: #626262;
cursor: pointer
}
.comment .toggle.open {
padding: 0;
background-color: transparent;
margin-bottom: -.5em
}
.user-view {
background-color: #fff;
box-sizing: border-box;
padding: 2em 3em
}
.user-view h1 {
margin: 0;
font-size: 1.5em
}
.user-view .meta {
list-style-type: none;
padding: 0
}
.user-view .label {
display: inline-block;
min-width: 4em
}
.user-view .about {
margin: 1em 0
}
.user-view .links a {
text-decoration: underline
}

View File

@@ -2,6 +2,7 @@
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
<style>
.red {
color: red;

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -7,11 +7,11 @@ edition = "2021"
console_log = "0.2"
log = "0.4"
leptos = { path = "../../leptos" }
leptos_router = { path = "../../router", features=["csr"] }
leptos_router = { path = "../../router", features = ["csr"] }
serde = { version = "1", features = ["derive"] }
futures = "0.3"
console_error_panic_hook = "0.1.7"
leptos_meta = { path = "../../meta", default-features = false }
leptos_meta = { path = "../../meta", features = ["csr"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@@ -2,6 +2,7 @@
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
<style>
a[aria-current] {
font-weight: bold;

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -87,9 +87,9 @@ site-root = "target/site"
# Defaults to pkg
site-pkg-dir = "pkg"
# [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 <site-root>/<site-pkg>/app.css
# style-file = "src/styles/tailwind.css"
style-file = "style/output.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "assets"
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
# The port to use for automatic reload monitoring

View File

@@ -1,6 +1,6 @@
# Leptos Starter Template
This is a template demonstrating how to integrate [TailwindCSS](https://tailwindcss.com/) with the [Leptos](https://github.com/gbj/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
This is a template demonstrating how to integrate [TailwindCSS](https://tailwindcss.com/) with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
If you don't have `cargo-leptos` installed you can install it with
@@ -52,13 +52,11 @@ If you're using VS Code, add the following to your `settings.json`
"css.validate": false,
```
Install [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss).
Install "VS Browser" extension, a browser at the right window.
Allow vscode Ports forward: 3000, 3001.
## Notes about Tooling
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
@@ -72,8 +70,8 @@ By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If
## Alternatives to cargo-leptos
This crate can be run without `cargo-leptos`, using `wasm-pack` and `cargo`. To do so, you'll need to install some other tools.
1. `cargo install wasm-pack`
0. `cargo install wasm-pack`
1. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
### Server Side Rendering With Hydration
@@ -91,15 +89,16 @@ Then run the server with `cargo run` to serve the server side rendered HTML and
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above before running
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above before running
> `cargo run`
### Client Side Rendering
You'll need to install trunk to client side render this bundle.
1. `cargo install trunk`
Then the site can be served with `trunk serve --open`
Then the site can be served with `trunk serve --open`
## Attribution
Many thanks to GreatGreg for putting together this guide. You can find the original, with added details, [here](https://github.com/gbj/leptos/discussions/125).
Many thanks to GreatGreg for putting together this guide. You can find the original, with added details, [here](https://github.com/leptos-rs/leptos/discussions/125).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,28 +1,37 @@
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
let (count, set_count) = create_signal(cx, 0);
view! {
cx,
<main class="my-0 mx-auto max-w-3xl text-center">
<Stylesheet id="leptos" href="/style.css"/>
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
<button
class="bg-sky-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
on:click=move |_| set_count.update(|count| *count += 1)
>
{move || if count() == 0 {
"Click me!".to_string()
} else {
count().to_string()
}}
</button>
</main>
<Stylesheet id="leptos" href="/pkg/tailwind.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Router>
<Routes>
<Route path="" view= move |cx| view! {
cx,
<main class="my-0 mx-auto max-w-3xl text-center">
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
<button
class="bg-sky-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
on:click=move |_| set_count.update(|count| *count += 1)
>
{move || if count() == 0 {
"Click me!".to_string()
} else {
count().to_string()
}}
</button>
</main>
}/>
</Routes>
</Router>
}
}

View File

@@ -7,6 +7,7 @@ cfg_if! {
use actix_web::*;
use leptos::*;
use crate::app::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
#[get("/style.css")]
async fn css() -> impl Responder {
@@ -15,20 +16,23 @@ cfg_if! {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_address.clone();
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
let pkg_dir = &leptos_options.site_pkg_dir;
let bundle_path = format!("/{site_root}/{pkg_dir}");
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> });
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
let routes = &routes;
App::new()
.service(css)
.service(Files::new("/pkg", "./pkg")) // used by wasm-pack and cargo run. Can be removed if using cargo-leptos
.service(Files::new(&bundle_path, format!("./{bundle_path}"))) // used by cargo-leptos. Can be removed if using wasm-pack and cargo run.
.route("/{tail:.*}", leptos_actix::render_app_to_stream(leptos_options.to_owned(), |cx| view! { cx, <App/> }))
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <App/> })
.service(Files::new("/", &site_root))
.wrap(middleware::Compress::default())
})
.bind(&addr)?

View File

@@ -1,5 +1,5 @@
/*
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com
*/
/*
@@ -30,7 +30,6 @@
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
*/
html {
@@ -45,8 +44,6 @@ html {
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
}
/*
@@ -413,13 +410,54 @@ video {
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
*, ::before, ::after {
::-webkit-backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;

View File

@@ -58,7 +58,7 @@ site-pkg-dir = "pkg"
# [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 <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
# assets-dir = "static/assets"
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
# The port to use for automatic reload monitoring

View File

@@ -25,7 +25,7 @@ cargo leptos build --release
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"pkg"`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS file is no longer processed by cargo-leptos. Building to alternative folders is not supported at this time
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -9,6 +9,7 @@ cfg_if! {
use actix_files::{Files};
use actix_web::*;
use crate::todo::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
#[get("/style.css")]
async fn css() -> impl Responder {
@@ -25,23 +26,25 @@ cfg_if! {
crate::todo::register_server_functions();
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let addr = conf.leptos_options.site_address.clone();
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> });
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
let pkg_dir = &leptos_options.site_pkg_dir;
let bundle_path = format!("/{site_root}/{pkg_dir}");
let routes = &routes;
App::new()
.service(Files::new("/pkg", "./pkg")) // used by wasm-pack and cargo run. Can be removed if using cargo-leptos
.service(Files::new(&bundle_path, format!("./{bundle_path}"))) // used by cargo-leptos. Can be removed if using wasm-pack and cargo run.
.service(css)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream(leptos_options.to_owned(), |cx| view! { cx, <TodoApp/> }))
//.wrap(middleware::Compress::default())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <TodoApp/> })
.service(Files::new("/", &site_root))
//.wrap(middleware::Compress::default())
})
.bind(addr)?
.run()

View File

@@ -38,9 +38,11 @@ cfg_if! {
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
let req =
use_context::<actix_web::HttpRequest>(cx).expect("couldn't get HttpRequest from context");
println!("req.path = {:?}", req.path());
use_context::<actix_web::HttpRequest>(cx);
if let Some(req) = req{
println!("req.path = {:#?}", req.path());
}
use futures::TryStreamExt;
let mut conn = db().await?;
@@ -90,9 +92,11 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
view! {
cx,
<Stylesheet id="leptos" href="./target/site/pkg/todo_app_sqlite.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/todo_app_sqlite.css"/>
<Router>
<header>
<h1>"My Tasks"</h1>

View File

@@ -72,7 +72,7 @@ site-pkg-dir = "pkg"
# [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 <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
# assets-dir = "static/assets"
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
# The port to use for automatic reload monitoring

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,41 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::LeptosOptions;
pub async fn file_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>) -> Result<Response<BoxBody>, (StatusCode, String)> {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await?;
match res.status() {
StatusCode::OK => Ok(res),
_ => Err((res.status(), "File Not Found".to_string()))
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
let root_path = format!("{root}");
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(&root_path).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}
}
}

View File

@@ -1,5 +1,6 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod file;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.

View File

@@ -1,24 +1,24 @@
use cfg_if::cfg_if;
use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
routing::{post},
error_handling::HandleError,
routing::post,
extract::Extension,
Router,
};
use crate::todo::*;
use todo_app_sqlite_axum::*;
use http::StatusCode;
use tower_http::services::ServeDir;
use crate::file::file_handler;
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
let mut conn = db().await.expect("couldn't connect to DB");
let conn = db().await.expect("couldn't connect to DB");
/* sqlx::migrate!()
.run(&mut conn)
.await
@@ -26,40 +26,18 @@ if #[cfg(feature = "ssr")] {
crate::todo::register_server_functions();
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let site_root = &leptos_options.site_root;
let pkg_dir = &leptos_options.site_pkg_dir;
// The URL path of the generated JS/WASM bundle from cargo-leptos
let bundle_path = format!("/{site_root}/{pkg_dir}");
// The filesystem path of the generated JS/WASM bundle from cargo-leptos
let bundle_filepath = format!("./{site_root}/{pkg_dir}");
let addr = leptos_options.site_address.clone();
log::debug!("serving at {addr}");
// These are Tower Services that will serve files from the static and pkg repos.
// HandleError is needed as Axum requires services to implement Infallible Errors
// because all Errors are converted into Responses
let static_service = HandleError::new( ServeDir::new("./static"), handle_file_error);
let pkg_service = HandleError::new( ServeDir::new("./pkg"), handle_file_error);
let cargo_leptos_service = HandleError::new( ServeDir::new(&bundle_filepath), handle_file_error);
/// Convert the Errors from ServeDir to a type that implements IntoResponse
async fn handle_file_error(err: std::io::Error) -> (StatusCode, String) {
(
StatusCode::NOT_FOUND,
format!("File Not Found: {}", err),
)
}
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.nest_service("/pkg", pkg_service) // Only need if using wasm-pack. Can be deleted if using cargo-leptos
.nest_service(&bundle_path, cargo_leptos_service) // Only needed if using cargo-leptos. Can be deleted if using wasm-pack and cargo-run
.nest_service("/static", static_service)
.fallback(leptos_axum::render_app_to_stream(leptos_options, |cx| view! { cx, <TodoApp/> }));
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
.fallback(file_handler)
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -39,9 +39,11 @@ cfg_if! {
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
let req_parts = use_context::<leptos_axum::RequestParts>(cx).unwrap();
println!("\ncalling server fn");
let req_parts = use_context::<leptos_axum::RequestParts>(cx);
if let Some(req_parts) = req_parts{
println!("Uri = {:?}", req_parts.uri);
}
use futures::TryStreamExt;
@@ -105,9 +107,11 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
view! {
cx,
<Stylesheet id="leptos" href="./target/site/pkg/todo_app_sqlite_axum.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/todo_app_sqlite_axum.css"/>
<Router>
<header>
<h1>"My Tasks"</h1>

View File

@@ -7,6 +7,7 @@
<link data-trunk rel="css" href="./node_modules/todomvc-app-css/index.css">
<title>Leptos • TodoMVC</title>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,22 +1,17 @@
[package]
name = "leptos_actix"
version = "0.1.0-beta"
version = { workspace = true }
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
repository = "https://github.com/leptos-rs/leptos"
description = "Actix integrations for the Leptos web framework."
[dependencies]
actix-web = "4"
futures = "0.3"
leptos = { path = "../../leptos", default-features = false, version = "0.1.0-alpha", features = [
"ssr",
] }
leptos_meta = { path = "../../meta", default-features = false, version = "0.1.0-alpha", features = [
"ssr",
] }
leptos_router = { path = "../../router", default-features = false, version = "0.1.0-alpha", features = [
"ssr",
] }
tokio = { version = "1.0", features = ["full"] }
leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
regex = "1.7.0"
tokio = "1.24.1"

View File

@@ -1,10 +1,18 @@
use actix_web::{http::header, web::Bytes, *};
use futures::{StreamExt};
#![forbid(unsafe_code)]
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
http::header,
web::Bytes,
*,
};
use futures::{Future, StreamExt};
use http::StatusCode;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use regex::Regex;
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -16,13 +24,13 @@ pub struct ResponseParts {
pub status: Option<StatusCode>,
}
impl ResponseParts{
impl ResponseParts {
/// Insert a header, overwriting any previous value with the same key
pub fn insert_header(&mut self, key: header::HeaderName, value: header::HeaderValue){
pub fn insert_header(&mut self, key: header::HeaderName, value: header::HeaderValue) {
self.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub fn append_header(&mut self, key: header::HeaderName, value: header::HeaderValue){
pub fn append_header(&mut self, key: header::HeaderName, value: header::HeaderValue) {
self.headers.append(key, value);
}
}
@@ -40,21 +48,21 @@ impl ResponseOptions {
*writable = parts
}
/// Set the status of the returned Response
pub async fn set_status(&self, status: StatusCode){
pub async fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write().await;
let res_parts = &mut*writeable;
let res_parts = &mut *writeable;
res_parts.status = Some(status);
}
/// Insert a header, overwriting any previous value with the same key
pub async fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue){
pub async fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write().await;
let res_parts = &mut*writeable;
let res_parts = &mut *writeable;
res_parts.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub async fn append_header(&self, key: header::HeaderName, value: header::HeaderValue){
pub async fn append_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write().await;
let res_parts = &mut*writeable;
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
}
}
@@ -62,10 +70,15 @@ impl ResponseOptions {
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
pub async fn redirect(cx: leptos::Scope, path: &str){
pub async fn redirect(cx: leptos::Scope, path: &str) {
let response_options = use_context::<ResponseOptions>(cx).unwrap();
response_options.set_status(StatusCode::FOUND).await;
response_options.insert_header(header::LOCATION, header::HeaderValue::from_str(path).expect("Failed to create HeaderValue")).await;
response_options
.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
)
.await;
}
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
@@ -132,7 +145,7 @@ pub fn handle_server_fns() -> Route {
let mut res: HttpResponseBuilder;
let mut res_parts = res_options.0.write().await;
if accept_header == Some("application/json")
|| accept_header == Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
@@ -184,10 +197,12 @@ pub fn handle_server_fns() -> Route {
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
}
} else {
HttpResponse::BadRequest()
.body(format!("Could not find a server function at the route {:?}. \
HttpResponse::BadRequest().body(format!(
"Could not find a server function at the route {:?}. \
\n\nIt's likely that you need to call ServerFn::register() on the \
server function type, somewhere in your `main` function.", req.path()))
server function type, somewhere in your `main` function.",
req.path()
))
}
}
},
@@ -237,148 +252,358 @@ pub fn render_app_to_stream<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where IV: IntoView
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let res_options = ResponseOptions::default();
let res_options_default = res_options.clone();
async move {
let path = req.path();
let query = req.query_string();
let path = if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
};
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
provide_context(cx, MetaContext::new());
provide_context(cx, res_options_default.clone());
provide_context(cx, req.clone());
provide_contexts(cx, &req, res_options);
(app_fn)(cx).into_view(cx)
}
};
let site_root = &options.site_root;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let output_name = &options.output_name;
let mut wasm_output_name = output_name.clone();
if std::env::var("OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}
let site_ip = &options.site_address.ip().to_string();
let reload_port = options.reload_port;
let site_root = &options.site_root;
let pkg_path = &options.site_pkg_dir;
// We need to do some logic to check if the site_root is pkg
// if it is, then we need to not add pkg_path. This would mean
// the site was built with cargo run and not cargo-leptos
let bundle_path = match site_root.as_ref() {
"pkg" => "pkg".to_string(),
_ => format!("{}/{}", site_root, pkg_path),
};
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin="">(function () {{
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
ws.onmessage = (ev) => {{
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {{
const link = document.querySelector("link#leptos");
if (link) {{
let href = link.getAttribute('href').split('?')[0];
let newHref = href + '?version=' + new Date().getMilliseconds();
link.setAttribute('href', newHref);
}} else {{
console.warn("Could not find link#leptos");
}}
}};
}};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
}})()
</script>
"#
),
false => "".to_string(),
};
let head = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="modulepreload" href="/{bundle_path}/{output_name}.js">
<link rel="preload" href="/{bundle_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{bundle_path}/{output_name}.js'; init('/{bundle_path}/{wasm_output_name}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>";
let (stream, runtime, _) = render_to_stream_with_prefix_undisposed(
app,
move |cx| {
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>").into()
});
let mut stream = Box::pin(futures::stream::once(async move { head.clone() })
.chain(stream)
.chain(futures::stream::once(async move {
runtime.dispose();
tail.to_string()
}))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>));
// Get the first, second, and third chunks in the stream, which renders the app shell, and thus allows Resources to run
let first_chunk = stream.next().await;
let second_chunk = stream.next().await;
let third_chunk = stream.next().await;
let res_options = res_options.0.read().await;
let (status, mut headers) = (res_options.status.clone(), res_options.headers.clone());
let status = status.unwrap_or_default();
let complete_stream =
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap(), third_chunk.unwrap()])
.chain(stream);
let mut res = HttpResponse::Ok().content_type("text/html").streaming(
complete_stream
);
// Add headers manipulated in the response
for (key, value) in headers.drain(){
if let Some(key) = key{
res.headers_mut().append(key, value);
}
};
// Set status to what is returned in the function
let res_status = res.status_mut();
*res_status = status;
// Return the response
res
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options).await
}
})
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{HttpServer, App};
/// use leptos::*;
/// use std::{env,net::SocketAddr};
/// use leptos_actix::DataResponse;
///
/// #[component]
/// fn MyApp(cx: Scope, data: &'static str) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
/// let addr = conf.leptos_options.site_address.clone();
/// HttpServer::new(move || {
/// let leptos_options = &conf.leptos_options;
///
/// App::new()
/// // {tail:.*} passes the remainder of the URL as the route
/// // the actual routing will be handled by `leptos_router`
/// .route("/{tail:.*}", leptos_actix::render_preloaded_data_app(
/// leptos_options.to_owned(),
/// |req| async move { Ok(DataResponse::Data("async func that can preload data")) },
/// |cx, data| view! { cx, <MyApp data/> })
/// )
/// })
/// .bind(&addr)?
/// .run()
/// .await
/// }
/// # }
/// ```
pub fn render_preloaded_data_app<Data, Fut, IV>(
options: LeptosOptions,
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
) -> Route
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
IV: IntoView + 'static,
{
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let data_fn = data_fn.clone();
let res_options = ResponseOptions::default();
async move {
let data = match data_fn(req.clone()).await {
Err(e) => return HttpResponse::from_error(e),
Ok(DataResponse::Response(r)) => return r.into(),
Ok(DataResponse::Data(d)) => d,
};
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move |cx| {
provide_contexts(cx, &req, res_options);
(app_fn)(cx, data).into_view(cx)
}
};
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options).await
}
})
}
fn provide_contexts(cx: leptos::Scope, req: &HttpRequest, res_options: ResponseOptions) {
let path = leptos_corrected_path(req);
let integration = ServerIntegration { path };
provide_context(cx, RouterIntegrationContext::new(integration));
provide_context(cx, MetaContext::new());
provide_context(cx, res_options);
provide_context(cx, req.clone());
}
fn leptos_corrected_path(req: &HttpRequest) -> String {
let path = req.path();
let query = req.query_string();
if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
}
}
async fn stream_app(
app: impl FnOnce(leptos::Scope) -> View + 'static,
head: String,
tail: String,
res_options: ResponseOptions,
) -> HttpResponse<BoxBody> {
let (stream, runtime, _) = render_to_stream_with_prefix_undisposed(app, move |cx| {
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>").into()
});
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })
.chain(stream)
.chain(futures::stream::once(async move {
runtime.dispose();
tail.to_string()
}))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
);
// Get the first, second, and third chunks in the stream, which renders the app shell, and thus allows Resources to run
let first_chunk = stream.next().await;
let second_chunk = stream.next().await;
let third_chunk = stream.next().await;
let res_options = res_options.0.read().await;
let (status, mut headers) = (res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
let complete_stream = futures::stream::iter([
first_chunk.unwrap(),
second_chunk.unwrap(),
third_chunk.unwrap(),
])
.chain(stream);
let mut res = HttpResponse::Ok()
.content_type("text/html")
.streaming(complete_stream);
// Add headers manipulated in the response
for (key, value) in headers.drain() {
if let Some(key) = key {
res.headers_mut().append(key, value);
}
}
// Set status to what is returned in the function
let res_status = res.status_mut();
*res_status = status;
// Return the response
res
}
fn html_parts(options: &LeptosOptions) -> (String, String) {
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let output_name = &options.output_name;
let mut wasm_output_name = output_name.clone();
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}
let site_ip = &options.site_address.ip().to_string();
let reload_port = options.reload_port;
let pkg_path = &options.site_pkg_dir;
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin="">(function () {{
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
ws.onmessage = (ev) => {{
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {{
const link = document.querySelector("link#leptos");
if (link) {{
let href = link.getAttribute('href').split('?')[0];
let newHref = href + '?version=' + new Date().getMilliseconds();
link.setAttribute('href', newHref);
}} else {{
console.warn("Could not find link#leptos");
}}
}};
}};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
}})()
</script>
"#
),
false => "".to_string(),
};
let head = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>".to_string();
(head, tail)
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list<IV>(app_fn: impl FnOnce(leptos::Scope) -> IV + 'static) -> Vec<String>
where
IV: IntoView + 'static,
{
let mut routes = leptos_router::generate_route_list_inner(app_fn);
// Empty strings screw with Actix pathing, they need to be "/"
routes = routes
.iter()
.map(|s| {
if s.is_empty() {
return "/".to_string();
}
s.to_string()
})
.collect();
// Actix's Router doesn't follow Leptos's
// Match `*` or `*someword` to replace with replace it with "/{tail.*}
let wildcard_re = Regex::new(r"\*.*").unwrap();
// Match `:some_word` but only capture `some_word` in the groups to replace with `{some_word}`
let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap();
let routes: Vec<String> = routes
.iter()
.map(|s| wildcard_re.replace_all(s, "{tail:.*}").to_string())
.map(|s| capture_re.replace_all(&s, "{$1}").to_string())
.collect();
if routes.is_empty() {
vec!["/".to_string()]
} else {
routes
}
}
pub enum DataResponse<T> {
Data(T),
Response(actix_web::dev::Response<BoxBody>),
}
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
fn leptos_preloaded_data_routes<Data, Fut, IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
) -> Self
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
IV: IntoView + 'static;
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
impl<T> LeptosRoutes for actix_web::App<T>
where
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
{
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(path, render_app_to_stream(options.clone(), app_fn.clone()));
}
router
}
fn leptos_preloaded_data_routes<Data, Fut, IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
) -> Self
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(
path,
render_preloaded_data_app(options.clone(), data_fn.clone(), app_fn.clone()),
);
}
router
}
}

View File

@@ -1,27 +1,19 @@
[package]
name = "leptos_axum"
version = "0.1.0-beta"
version = { workspace = true }
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
[dependencies]
axum = {version="0.6", features=["macros"]}
derive_builder = "0.12.0"
axum = { version = "0.6", features = ["macros"] }
futures = "0.3"
http = "0.2.8"
hyper = "0.14.23"
kdl = "4.6.0"
leptos = { path = "../../leptos", default-features = false, version = "0.1.0-beta", features = [
"ssr",
] }
leptos_meta = { path = "../../meta", default-features = false, version = "0.1.0-beta", features = [
"ssr",
] }
leptos_router = { path = "../../router", default-features = false, version = "0.1.0-beta", features = [
"ssr",
] }
leptos_config = { path = "../../leptos_config", default-features = false, version = "0.1.0-beta" }
leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_config = { workspace = true }
tokio = { version = "1.0", features = ["full"] }

View File

@@ -1,8 +1,11 @@
#![forbid(unsafe_code)]
use axum::{
body::{Body, Bytes, Full, StreamBody},
extract::Path,
http::{header::HeaderName, header::HeaderValue, HeaderMap, Request, StatusCode},
response::IntoResponse,
routing::get,
};
use futures::{Future, SinkExt, Stream, StreamExt};
use http::{header, method::Method, uri::Uri, version::Version, Response};
@@ -11,7 +14,7 @@ use leptos::*;
use leptos_meta::MetaContext;
use leptos_router::*;
use std::{io, pin::Pin, sync::Arc};
use tokio::{sync::RwLock, task::spawn_blocking};
use tokio::{sync::RwLock, task::spawn_blocking, task::LocalSet};
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
/// to construct this for Leptos to use in Axum
@@ -97,7 +100,7 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
uri: parts.uri,
headers: parts.headers,
version: parts.version,
body: body.clone(),
body,
}
}
@@ -139,7 +142,7 @@ pub async fn handle_server_fns(
req: Request<Body>,
) -> impl IntoResponse {
// Axum Path extractor doesn't remove the first slash from the path, while Actix does
let fn_name: String = match fn_name.strip_prefix("/") {
let fn_name: String = match fn_name.strip_prefix('/') {
Some(path) => path.to_string(),
None => fn_name,
};
@@ -180,15 +183,12 @@ pub async fn handle_server_fns(
let res_options_outer = res_options.unwrap().0;
let res_options_inner = res_options_outer.read().await;
let (status, mut res_headers) = (
res_options_inner.status.clone(),
res_options_inner.status,
res_options_inner.headers.clone(),
);
match res.headers_mut() {
Some(header_ref) => {
header_ref.extend(res_headers.drain());
}
None => (),
if let Some(header_ref) = res.headers_mut() {
header_ref.extend(res_headers.drain());
};
if accept_header == Some("application/json")
@@ -237,9 +237,9 @@ pub async fn handle_server_fns(
Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Full::from(
format!("Could not find a server function at the route {:?}. \
format!("Could not find a server function at the route {fn_name}. \
\n\nIt's likely that you need to call ServerFn::register() on the \
server function type, somewhere in your `main` function.", fn_name)
server function type, somewhere in your `main` function." )
))
}
.expect("could not build Response");
@@ -322,33 +322,17 @@ where
async move {
// Need to get the path and query string of the Request
let path = req.uri();
let query = path.query();
let full_path;
if let Some(query) = query {
full_path = "http://leptos".to_string() + &path.to_string() + "?" + query
} else {
full_path = "http://leptos".to_string() + &path.to_string()
}
let full_path = format!("http://leptos.dev{path}");
let site_root = &options.site_root;
let pkg_path = &options.site_pkg_dir;
// We need to do some logic to check if the site_root is pkg
// if it is, then we need to not add pkg_path. This would mean
// the site was built with cargo run and not cargo-leptos
let bundle_path = match site_root.as_ref() {
"pkg" => "pkg".to_string(),
_ => format!("{}/{}", site_root, pkg_path),
};
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var OUTPUT_NAME
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();
if std::env::var("OUTPUT_NAME").is_err() {
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}
@@ -388,9 +372,9 @@ where
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="modulepreload" href="/{bundle_path}/{output_name}.js">
<link rel="preload" href="/{bundle_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{bundle_path}/{output_name}.js'; init('/{bundle_path}/{wasm_output_name}.wasm').then(hydrate);</script>
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
@@ -487,10 +471,9 @@ where
Box::pin(complete_stream) as PinnedHtmlStream
));
match res_options.status {
Some(status) => *res.status_mut() = status,
None => (),
};
if let Some(status) = res_options.status {
*res.status_mut() = status
}
let mut res_headers = res_options.headers.clone();
res.headers_mut().extend(res_headers.drain());
@@ -499,3 +482,85 @@ where
})
}
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
pub async fn generate_route_list<IV>(app_fn: impl FnOnce(Scope) -> IV + 'static) -> Vec<String>
where
IV: IntoView + 'static,
{
#[derive(Default, Clone, Debug)]
pub struct Routes(pub Arc<RwLock<Vec<String>>>);
let routes = Routes::default();
let routes_inner = routes.clone();
let local = LocalSet::new();
// Run the local task set.
local
.run_until(async move {
tokio::task::spawn_local(async move {
let routes = leptos_router::generate_route_list_inner(app_fn);
let mut writable = routes_inner.0.write().await;
*writable = routes;
})
.await
.unwrap();
})
.await;
let routes = routes.0.read().await.to_owned();
// Axum's Router defines Root routes as "/" not ""
let routes: Vec<String> = routes
.iter()
.map(|s| {
if s.is_empty() {
return "/".to_string();
}
s.to_string()
})
.collect();
if routes.is_empty() {
vec!["/".to_string()]
} else {
routes
}
}
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
impl LeptosRoutes for axum::Router {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(
path,
get(render_app_to_stream(options.clone(), app_fn.clone())),
);
}
router
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos"
version = "0.1.0-beta"
version = { workspace = true }
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -10,11 +10,11 @@ readme = "../README.md"
[dependencies]
cfg-if = "1"
leptos_config = { path = "../leptos_config", default-features = false, version = "0.1.0-beta" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.1.0-beta" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.1.0-beta" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.1.0-beta" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.1.0-beta" }
leptos_dom = { workspace = true }
leptos_macro = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_config = { workspace = true }
tracing = "0.1"
typed-builder = "0.11"

View File

@@ -1,4 +1,5 @@
#![deny(missing_docs)]
#![forbid(unsafe_code)]
//! # About Leptos
//!
@@ -18,33 +19,33 @@
//! 1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.0", features = ["stable"] }`
//! 2. `nightly` enables the function call syntax for accessing and setting signals. If youre using `stable`,
//! youll just call `.get()`, `.set()`, or `.update()` manually. Check out the
//! [`counters_stable` example](https://github.com/gbj/leptos/blob/main/examples/counters_stable/src/main.rs)
//! [`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
//! for examples of the correct API.
//!
//! # Learning by Example
//!
//! These docs are a work in progress. If you want to see what Leptos is capable of, check out
//! the [examples](https://github.com/gbj/leptos/tree/main/examples):
//! - [`counter`](https://github.com/gbj/leptos/tree/main/examples/counter) is the classic
//! the [examples](https://github.com/leptos-rs/leptos/tree/main/examples):
//! - [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) is the classic
//! counter example, showing the basics of client-side rendering and reactive DOM updates
//! - [`counters`](https://github.com/gbj/leptos/tree/main/examples/counter) introduces parent-child
//! - [`counters`](https://github.com/leptos-rs/leptos/tree/main/examples/counters) introduces parent-child
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
//! - [`parent_child`](https://github.com/gbj/leptos/tree/main/examples/parent_child) shows four different
//! - [`parent_child`](https://github.com/leptos-rs/leptos/tree/main/examples/parent_child) shows four different
//! ways a parent component can communicate with a child, including passing a closure, context, and more
//! - [`todomvc`](https://github.com/gbj/leptos/tree/main/examples/todomvc) implements the classic to-do
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) implements the classic to-do
//! app in Leptos. This is a good example of a complete, simple app. In particular, you might want to
//! see how we use [create_effect] to [serialize JSON to `localStorage`](https://github.com/gbj/leptos/blob/16f084a71268ac325fbc4a5e50c260df185eadb6/examples/todomvc/src/lib.rs#L164)
//! and [reactively call DOM methods](https://github.com/gbj/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L291)
//! on [references to elements](https://github.com/gbj/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L254).
//! - [`fetch`](https://github.com/gbj/leptos/tree/main/examples/fetch) introduces
//! see how we use [create_effect] to [serialize JSON to `localStorage`](https://github.com/leptos-rs/leptos/blob/16f084a71268ac325fbc4a5e50c260df185eadb6/examples/todomvc/src/lib.rs#L164)
//! and [reactively call DOM methods](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L291)
//! on [references to elements](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L254).
//! - [`fetch`](https://github.com/leptos-rs/leptos/tree/main/examples/fetch) introduces
//! [Resource](leptos_reactive::Resource)s, which allow you to integrate arbitrary `async` code like an
//! HTTP request within your reactive code.
//! - [`router`](https://github.com/gbj/leptos/tree/main/examples/router) shows how to use Leptoss nested router
//! - [`router`](https://github.com/leptos-rs/leptos/tree/main/examples/router) shows how to use Leptoss nested router
//! to enable client-side navigation and route-specific, reactive data loading.
//! - [`todomvc`](https://github.com/gbj/leptos/tree/main/examples/todomvc) shows the basics of building an
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) shows the basics of building an
//! isomorphic web app. Both the server and the client import the same app code from the `todomvc` example.
//! The server renders the app directly to an HTML string, and the client hydrates that HTML to make it interactive.
//! - [`hackernews`](https://github.com/gbj/leptos/tree/main/examples/hackernews) pulls everything together.
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews) pulls everything together.
//! It integrates calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional PEMPA that works as intended even before WASM has loaded and begun to run.
//!

View File

@@ -1,10 +1,10 @@
use cfg_if::cfg_if;
use leptos_macro::component;
use std::rc::Rc;
use leptos_dom::{DynChild, Fragment, IntoView, Component};
use leptos_reactive::{provide_context, Scope, SuspenseContext};
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
use leptos_dom::{HydrationCtx, HydrationKey};
use leptos_dom::HydrationCtx;
use leptos_dom::{DynChild, Fragment, IntoView};
use leptos_macro::component;
use leptos_reactive::{provide_context, Scope, SuspenseContext};
use std::rc::Rc;
/// If any [Resources](leptos_reactive::Resource) are read in the `children` of this
/// component, it will show the `fallback` while they are loading. Once all are resolved,
@@ -73,7 +73,7 @@ where
let orig_child = Rc::new(children);
Component::new("Suspense", move |cx| {
leptos_dom::custom(cx, leptos_dom::Custom::new("leptos-suspense")).child({
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
let current_id = HydrationCtx::peek();
@@ -88,8 +88,8 @@ where
} else {
// run the child; we'll probably throw this away, but it will register resource reads
let child = orig_child(cx).into_view(cx);
let initial = {
let initial = {
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
child.clone()
@@ -97,37 +97,33 @@ where
// show the fallback, but also prepare to stream HTML
else {
let orig_child = Rc::clone(&orig_child);
cx.register_suspense(
context,
&id_before_suspense.to_string(),
&id_before_suspense.to_string(),
&current_id.to_string(),
{
let current_id = current_id.clone();
let fragment_id = HydrationKey {
previous: current_id.previous,
offset: current_id.offset + 1
};
move || {
HydrationCtx::continue_from(fragment_id);
orig_child(cx)
HydrationCtx::continue_from(current_id.clone());
DynChild::new(move || orig_child(cx))
.into_view(cx)
.render_to_string(cx)
.to_string()
}
}
);
// return the fallback for now, wrapped in fragment identifer
fallback().into_view(cx)
}
};
HydrationCtx::continue_from(current_id.clone());
initial
}
}
})
})
}
}

View File

@@ -1,6 +1,6 @@
use leptos_dom::{Fragment, IntoView, View};
use leptos_macro::component;
use leptos_reactive::{ Scope, SignalSetter};
use leptos_reactive::{Scope, SignalSetter};
use std::{cell::RefCell, rc::Rc};
/// If any [Resource](leptos_reactive::Resource)s are read in the `children` of this
@@ -66,7 +66,7 @@ pub fn Transition<F, E>(
#[prop(optional)]
set_pending: Option<SignalSetter<bool>>,
/// Will be displayed once all resources have resolved.
children: Box<dyn Fn(Scope) -> Fragment>
children: Box<dyn Fn(Scope) -> Fragment>,
) -> impl IntoView
where
F: Fn() -> E + 'static,
@@ -78,7 +78,6 @@ where
crate::SuspenseProps::builder()
.fallback({
let prev_child = Rc::clone(&prev_children);
let set_pending = set_pending.clone();
move || {
if let Some(set_pending) = &set_pending {
set_pending.set(true);
@@ -98,6 +97,6 @@ where
}
frag
}))
.build()
.build(),
)
}
}

View File

@@ -16,7 +16,7 @@ fn simple_ssr_test() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span id=\"_0-3\">Value: <leptos-dyn-child-start leptos id=\"_0-4o\"></leptos-dyn-child-start>0<leptos-dyn-child-end leptos id=\"_0-4c\"></leptos-dyn-child-end>!</span><button id=\"_0-5\">+1</button></div>"
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span id=\"_0-3\">Value: <!--hk=_0-4o|leptos-dyn-child-start-->0<!--hk=_0-4c|leptos-dyn-child-end-->!</span><button id=\"_0-5\">+1</button></div>"
);
});
}
@@ -50,7 +50,7 @@ fn ssr_test_with_components() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div class=\"counters\" id=\"_0-1\"><leptos-counter-start leptos id=\"_0-1-0o\"></leptos-counter-start><div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span id=\"_0-1-3\">Value: <leptos-dyn-child-start leptos id=\"_0-1-4o\"></leptos-dyn-child-start>1<leptos-dyn-child-end leptos id=\"_0-1-4c\"></leptos-dyn-child-end>!</span><button id=\"_0-1-5\">+1</button></div><leptos-counter-end leptos id=\"_0-1-0c\"></leptos-counter-end><leptos-counter-start leptos id=\"_0-1-5-0o\"></leptos-counter-start><div id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span id=\"_0-1-5-3\">Value: <leptos-dyn-child-start leptos id=\"_0-1-5-4o\"></leptos-dyn-child-start>2<leptos-dyn-child-end leptos id=\"_0-1-5-4c\"></leptos-dyn-child-end>!</span><button id=\"_0-1-5-5\">+1</button></div><leptos-counter-end leptos id=\"_0-1-5-0c\"></leptos-counter-end></div>"
"<div class=\"counters\" id=\"_0-1\"><!--hk=_0-1-0o|leptos-counter-start--><div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span id=\"_0-1-3\">Value: <!--hk=_0-1-4o|leptos-dyn-child-start-->1<!--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5\">+1</button></div><!--hk=_0-1-0c|leptos-counter-end--><!--hk=_0-1-5-0o|leptos-counter-start--><div id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span id=\"_0-1-5-3\">Value: <!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5-5\">+1</button></div><!--hk=_0-1-5-0c|leptos-counter-end--></div>"
);
});
}

View File

@@ -1,10 +1,10 @@
[package]
name = "leptos_config"
version = "0.1.0-beta"
version = { workspace = true }
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
repository = "https://github.com/leptos-rs/leptos"
description = "Configuraiton for the Leptos web framework."
[dependencies]
@@ -14,4 +14,3 @@ regex = "1.7.0"
serde = { version = "1.0.151", features = ["derive"] }
thiserror = "1.0.38"
typed-builder = "0.11.0"

View File

@@ -1,3 +1,5 @@
use std::{net::AddrParseError, num::ParseIntError};
use thiserror::Error;
#[derive(Debug, Error, Clone)]
@@ -10,9 +12,23 @@ pub enum LeptosConfigError {
EnvError,
#[error("Config Error: {0}")]
ConfigError(String),
#[error("Config Error: {0}")]
EnvVarError(String),
}
impl From<config::ConfigError> for LeptosConfigError {
fn from(e: config::ConfigError) -> Self {
Self::ConfigError(e.to_string())
}
}
impl From<ParseIntError> for LeptosConfigError {
fn from(e: ParseIntError) -> Self {
Self::ConfigError(e.to_string())
}
}
impl From<AddrParseError> for LeptosConfigError {
fn from(e: AddrParseError) -> Self {
Self::ConfigError(e.to_string())
}
}

View File

@@ -1,3 +1,5 @@
#![forbid(unsafe_code)]
pub mod errors;
use crate::errors::LeptosConfigError;
@@ -10,21 +12,23 @@ use typed_builder::TypedBuilder;
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
/// occur with LeptosOptions
#[derive(Clone, serde::Deserialize)]
#[derive(Clone, Debug, serde::Deserialize)]
pub struct ConfFile {
pub leptos_options: LeptosOptions,
}
/// This struct serves as a convenient place to store details used for configuring Leptos.
/// It's used in our actix and axum integrations to generate the
/// correct path for WASM, JS, and Websockets, as well as other configuration tasks.
/// It shares keys with cargo-leptos, to allow for easy interoperability
#[derive(TypedBuilder, Clone, serde::Deserialize)]
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
pub struct LeptosOptions {
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
#[builder(setter(into))]
pub output_name: String,
/// The path of the all the files generated by cargo-leptos
#[builder(setter(into), default="pkg".to_string())]
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
/// tools.
#[builder(setter(into), default=".".to_string())]
pub site_root: String,
/// The path of the WASM and JS files generated by wasm-bindgen from the root of your app
/// By default, wasm-bindgen puts them in `pkg`.
@@ -46,6 +50,28 @@ pub struct LeptosOptions {
pub reload_port: u32,
}
impl LeptosOptions {
fn try_from_env() -> Result<Self, LeptosConfigError> {
Ok(LeptosOptions {
output_name: std::env::var("LEPTOS_OUTPUT_NAME")
.map_err(|e| LeptosConfigError::EnvVarError(format!("LEPTOS_OUTPUT_NAME: {e}")))?,
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
env: Env::default(),
site_address: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?.parse()?,
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?.parse()?,
})
}
}
fn env_w_default(key: &str, default: &str) -> Result<String, LeptosConfigError> {
match std::env::var(key) {
Ok(val) => Ok(val),
Err(VarError::NotPresent) => Ok(default.to_string()),
Err(e) => Err(LeptosConfigError::EnvVarError(format!("{key}: {e}"))),
}
}
/// An enum that can be used to define the environment Leptos is running in. Can be passed to [RenderOptions].
/// Setting this to the `PROD` variant will not include the websockets code for `cargo-leptos` watch mode.
/// Defaults to `DEV`.
@@ -119,46 +145,48 @@ impl TryFrom<String> for Env {
"prod" => Ok(Self::PROD),
"production" => Ok(Self::PROD),
other => Err(format!(
"{} is not a supported environment. Use either `dev` or `production`.",
other
"{other} is not a supported environment. Use either `dev` or `production`."
)),
}
}
}
/// Loads [LeptosOptions] from a Cargo.toml with layered overrides. If an env var is specified, like `LEPTOS_ENV`,
/// it will override a setting in the file.
/// it will override a setting in the file. It takes in an optional path to a Cargo.toml file. If None is provided,
/// you'll need to set the options as environment variables or rely on the defaults. This is the preferred
/// approach for cargo-leptos. If Some("./Cargo.toml") is provided, Leptos will read in the settings itself. This
/// option currently does not allow dashes in file or foldernames, as all dashes become underscores
pub async fn get_configuration(path: Option<&str>) -> Result<ConfFile, LeptosConfigError> {
// Allow Cargo.toml path to be specified in case of workspace wonkiness
let text = match path {
Some(p) => fs::read_to_string(p).map_err(|_| LeptosConfigError::ConfigNotFound)?,
None => fs::read_to_string("Cargo.toml").map_err(|_| LeptosConfigError::ConfigNotFound)?,
};
if let Some(path) = path {
let text = fs::read_to_string(path).map_err(|_| LeptosConfigError::ConfigNotFound)?;
let re: Regex =
Regex::new(r#"(?m)^\[package.metadata.leptos\]|(?m)^\[\[workspace.metadata.leptos\]\]"#)
.unwrap();
let start = match re.find(&text) {
Some(found) => found.start(),
None => return Err(LeptosConfigError::ConfigSectionNotFound),
};
let re: Regex = Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
let start = match re.find(&text) {
Some(found) => found.start(),
None => return Err(LeptosConfigError::ConfigSectionNotFound),
};
// so that serde error messages have right line number
let newlines = text[..start].matches('\n').count();
let input = "\n".repeat(newlines) + &text[start..];
let toml = input
.replace("[package.metadata.leptos]", "[leptos_options]")
.replace("[[workspace.metadata.leptos]]", "[leptos_options]")
.replace('-', "_");
let settings = Config::builder()
// Read the "default" configuration file
.add_source(File::from_str(&toml, FileFormat::Toml))
// Layer on the environment-specific values.
// Add in settings from environment variables (with a prefix of LEPTOS and '_' as separator)
// E.g. `LEPTOS_RELOAD_PORT=5001 would set `LeptosOptions.reload_port`
.add_source(config::Environment::with_prefix("LEPTOS").separator("_"))
.build()?;
// so that serde error messages have right line number
let newlines = text[..start].matches('\n').count();
let input = "\n".repeat(newlines) + &text[start..];
let toml = input
.replace("[package.metadata.leptos]", "[leptos_options]")
.replace('-', "_");
let settings = Config::builder()
// Read the "default" configuration file
.add_source(File::from_str(&toml, FileFormat::Toml))
// Layer on the environment-specific values.
// Add in settings from environment variables (with a prefix of LEPTOS and '_' as separator)
// E.g. `LEPTOS_RELOAD_PORT=5001 would set `LeptosOptions.reload_port`
.add_source(config::Environment::with_prefix("LEPTOS").separator("_"))
.build()?;
settings
.try_deserialize()
.map_err(|e| LeptosConfigError::ConfigError(e.to_string()))
settings
.try_deserialize()
.map_err(|e| LeptosConfigError::ConfigError(e.to_string()))
} else {
Ok(ConfFile {
leptos_options: LeptosOptions::try_from_env()?,
})
}
}

View File

@@ -1,10 +1,10 @@
[package]
name = "leptos_dom"
version = "0.1.0-beta"
version = { workspace = true }
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
repository = "https://github.com/leptos-rs/leptos"
description = "DOM operations for the Leptos web framework."
[dependencies]
@@ -17,7 +17,7 @@ html-escape = "0.2"
indexmap = "1.9"
itertools = "0.10"
js-sys = "0.3"
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.1.0-beta" }
leptos_reactive = { workspace = true }
once_cell = "1"
pad-adapter = "0.1"
paste = "1"
@@ -39,6 +39,7 @@ features = [
"Range",
"Text",
"HtmlCollection",
"TreeWalker",
# Events we cast to in leptos_macro -- added here so we don't force users to import them
"AnimationEvent",

View File

@@ -43,6 +43,7 @@ fn view_fn(cx: Scope) -> impl IntoView {
let (is_a, set_is_a) = create_signal(cx, true);
let handle_toggle = move |_| {
trace!("toggling");
if is_a() {
set_b(a());
@@ -54,11 +55,14 @@ fn view_fn(cx: Scope) -> impl IntoView {
}
};
let a_tag = view! { cx, <svg::a/> };
view! { cx,
<>
<div>
<button on:click=handle_toggle>"Toggle"</button>
</div>
<svg>{a_tag}</svg>
<Example/>
<A child=Signal::from(a) />
<A child=Signal::from(b) />
@@ -73,23 +77,26 @@ fn A(cx: Scope, child: Signal<View>) -> impl IntoView {
#[component]
fn Example(cx: Scope) -> impl IntoView {
trace!("rendering <Example/>");
let (value, set_value) = create_signal(cx, 10);
trace!("rendering <Example/>");
let memo = create_memo(cx, move |_| value() * 2);
let derived = Signal::derive(cx, move || {
value() * 3
});
let (value, set_value) = create_signal(cx, 10);
create_effect(cx, move |_| {
trace!("logging value of derived..., {}", derived.get());
});
let memo = create_memo(cx, move |_| value() * 2);
let derived = Signal::derive(cx, move || value() * 3);
create_effect(cx, move |_| {
trace!("logging value of derived..., {}", derived.get());
});
set_timeout(move || { set_value.update(|v| *v += 1)}, std::time::Duration::from_millis(50));
set_timeout(
move || set_value.update(|v| *v += 1),
std::time::Duration::from_millis(50),
);
view! { cx,
view! { cx,
<h1>"Example"</h1>
<button on:click=move |_| set_value.update(|value| *value += 1)>
"Click me"
</button>
}
}

View File

@@ -243,7 +243,7 @@ where
children_fn,
} = self;
let mut repr = ComponentRepr::new_with_id(name.clone(), id);
let mut repr = ComponentRepr::new_with_id(name, id);
// disposed automatically when the parent scope is disposed
let (child, _) =

View File

@@ -154,10 +154,7 @@ where
let component = DynChildRepr::new();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let (frag, closing) = (
component.document_fragment.clone(),
component.closing.node.clone(),
);
let closing = component.closing.node.clone();
let child = component.child.clone();
@@ -189,7 +186,9 @@ where
// or to reuse it in the case of a text node
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none() && child.get_closing_node().next_sibling().as_ref() != Some(&closing);
let was_child_moved = prev_t.is_none()
&& child.get_closing_node().next_sibling().as_ref()
!= Some(&closing);
// If the previous child was a text node, we would like to
// make use of it again if our current child is also a text

View File

@@ -16,10 +16,20 @@ thread_local! {
pub fn add_event_listener<E>(
target: &web_sys::Element,
event_name: Cow<'static, str>,
cb: impl FnMut(E) + 'static,
mut cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
{
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
}
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
let key = event_delegation_key(&event_name);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
@@ -31,10 +41,20 @@ pub fn add_event_listener<E>(
pub fn add_event_listener_undelegated<E>(
target: &web_sys::Element,
event_name: &str,
cb: impl FnMut(E) + 'static,
mut cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
{
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
}
}
let event_name = intern(event_name);
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
@@ -97,7 +117,20 @@ pub(crate) fn add_delegated_event_listener(event_name: Cow<'static, str>) {
}
};
crate::window_event_listener(&event_name, handler);
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let handler = move |e| {
let _guard = span.enter();
handler(e);
};
}
}
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
let handler = Closure::wrap(handler).into_js_value();
_ = crate::window()
.add_event_listener_with_callback(&event_name, handler.unchecked_ref());
// register that we've created handler
events.insert(event_name);

View File

@@ -15,6 +15,15 @@ pub fn set_property(
};
}
/// Gets the value of a property set on a DOM element.
pub fn get_property(
el: &web_sys::Element,
prop_name: &str,
) -> Result<JsValue, JsValue> {
let key = JsValue::from_str(prop_name);
js_sys::Reflect::get(el, &key)
}
/// Returns the current [`window.location`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
pub fn location() -> web_sys::Location {
window().location()
@@ -71,21 +80,57 @@ pub fn event_target_checked(ev: &web_sys::Event) -> bool {
/// Runs the given function between the next repaint
/// using [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
}
let cb = Closure::once_into_js(cb);
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
}
/// Queues the given function during an idle period
/// using [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_idle_callback(cb: impl Fn() + 'static) {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
}
/// Executes the given function after the given duration of time has passed.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
}
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
@@ -107,10 +152,24 @@ impl IntervalHandle {
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_interval(
cb: impl Fn() + 'static,
duration: Duration,
) -> Result<IntervalHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
let handle = window()
.set_interval_with_callback_and_timeout_and_arguments_0(
@@ -121,10 +180,24 @@ pub fn set_interval(
}
/// Adds an event listener to the `Window`.
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(event_name = %event_name))
)]
pub fn window_event_listener(
event_name: &str,
cb: impl Fn(web_sys::Event) + 'static,
) {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
}
}
if !is_server() {
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;

View File

@@ -90,7 +90,12 @@ where
element: el,
};
HtmlElement { cx, element }
HtmlElement {
cx,
element,
#[cfg(debug_assertions)]
span: ::tracing::Span::current(),
}
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
@@ -160,6 +165,66 @@ pub struct Custom {
id: HydrationKey,
}
impl Custom {
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
let name = name.into();
let id = HydrationCtx::id();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let element = if HydrationCtx::is_hydrating() {
if let Some(el) = crate::document().get_element_by_id(&format!("_{id}")) {
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
name.to_ascii_uppercase(),
"SSR and CSR elements have the same `TopoId` but different node \
kinds. This is either a discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it can also be a \
leptos hydration issue."
);
el.remove_attribute("id").unwrap();
el.unchecked_into()
} else if let Ok(Some(el)) =
crate::document().query_selector(&format!("[leptos-hk=_{id}]"))
{
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
name.to_ascii_uppercase(),
"SSR and CSR elements have the same `TopoId` but different node \
kinds. This is either a discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it can also be a \
leptos hydration issue."
);
el.remove_attribute("leptos-hk").unwrap();
el.unchecked_into()
} else {
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
crate::document().create_element(&name).unwrap()
}
} else {
crate::document().create_element(&name).unwrap()
};
Self {
name,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
element: element.unchecked_into(),
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
impl std::ops::Deref for Custom {
type Target = web_sys::HtmlElement;
@@ -192,6 +257,8 @@ cfg_if! {
/// Represents an HTML element.
#[derive(Clone)]
pub struct HtmlElement<El: ElementDescriptor> {
#[cfg(debug_assertions)]
pub(crate) span: ::tracing::Span,
pub(crate) cx: Scope,
pub(crate) element: El,
}
@@ -236,6 +303,8 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
Self {
cx,
element,
#[cfg(debug_assertions)]
span: ::tracing::Span::current()
}
} else {
Self {
@@ -272,6 +341,8 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
let Self {
cx,
element,
#[cfg(debug_assertions)]
span
} = self;
HtmlElement {
@@ -281,6 +352,8 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
element: element.as_ref().clone(),
is_void: element.is_void(),
},
#[cfg(debug_assertions)]
span
}
} else {
let Self {
@@ -571,10 +644,22 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
pub fn on<E: EventDescriptor + 'static>(
self,
event: E,
event_handler: impl FnMut(E::EventType) + 'static,
#[allow(unused_mut)] // used for tracing in debug
mut event_handler: impl FnMut(E::EventType) + 'static,
) -> Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
cfg_if! {
if #[cfg(debug_assertions)] {
let onspan = ::tracing::span!(
parent: &self.span,
::tracing::Level::TRACE,
"on",
event = %event.name()
);
let _onguard = onspan.enter();
}
}
let event_name = event.name();
if event.bubbles() {
@@ -607,6 +692,10 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
if !HydrationCtx::is_hydrating() {
// add a debug-only, run-time warning for the SVG <a> element
#[cfg(debug_assertions)]
warn_on_ambiguous_a(self.element.as_ref(), &child);
mount_child(MountKind::Append(self.element.as_ref()), &child);
}
@@ -683,9 +772,9 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
let children = children;
if attrs.iter_mut().any(|(name, _)| name == "id") {
attrs.push(("leptos-hk".into(), format!("_{}", id).into()));
attrs.push(("leptos-hk".into(), format!("_{id}").into()));
} else {
attrs.push(("id".into(), format!("_{}", id).into()));
attrs.push(("id".into(), format!("_{id}").into()));
}
element.attrs = attrs;
@@ -865,6 +954,17 @@ macro_rules! generate_html_tags {
}
#[$meta]
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "HtmlElement",
skip_all,
fields(
tag = %format!("<{}/>", stringify!($tag))
)
)
)]
pub fn $tag(cx: Scope) -> HtmlElement<[<$tag:camel $($trailing_)?>]> {
HtmlElement::new(cx, [<$tag:camel $($trailing_)?>]::default())
}
@@ -879,6 +979,23 @@ macro_rules! generate_html_tags {
}
}
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
fn warn_on_ambiguous_a(parent: &web_sys::Element, child: &View) {
if let View::Element(el) = &child {
if el.name == "a" {
if parent.namespace_uri() != el.element.namespace_uri() {
crate::warn!(
"Warning: you are appending an SVG <a/> to an HTML element, or an \
HTML <a/> to an SVG. Typically, this occurs when you create an \
<a/> with the `view` macro and append it to an SVG, but the \
framework assumed it was HTML when you created it. To specify that \
it is an SVG <a/>, use <svg::a/> in the view macro."
)
}
}
}
}
generate_html_tags![
// ==========================
// Main root

View File

@@ -1,21 +1,51 @@
use cfg_if::cfg_if;
use std::{cell::RefCell, fmt::Display};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use once_cell::unsync::Lazy as LazyCell;
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use once_cell::unsync::Lazy as LazyCell;
use std::collections::HashMap;
use wasm_bindgen::JsCast;
/// We can tell if we start in hydration mode by checking to see if the
/// id "_0" is present in the DOM. If it is, we know we are hydrating from
/// the server, if not, we are starting off in CSR
#[cfg(all(target_arch = "wasm32", feature = "web"))]
thread_local! {
static IS_HYDRATING: RefCell<LazyCell<bool>> = RefCell::new(LazyCell::new(|| {
#[cfg(debug_assertions)]
return crate::document().get_element_by_id("_0-0-0").is_some()
|| crate::document().get_element_by_id("_0-0-0o").is_some();
// We can tell if we start in hydration mode by checking to see if the
// id "_0-0-0" is present in the DOM. If it is, we know we are hydrating from
// the server, if not, we are starting off in CSR
thread_local! {
static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
let document = crate::document();
let body = document.body().unwrap();
let walker = document
.create_tree_walker_with_what_to_show(&body, 128)
.unwrap();
let mut map = HashMap::new();
while let Ok(Some(node)) = walker.next_node() {
if let Some(content) = node.text_content() {
if let Some(hk) = content.strip_prefix("hk=") {
if let Some(hk) = hk.split("|").next() {
map.insert(hk.into(), node.unchecked_into());
}
}
}
}
map
});
#[cfg(not(debug_assertions))]
return crate::document().get_element_by_id("_0-0-0").is_some();
}));
static IS_HYDRATING: RefCell<LazyCell<bool>> = RefCell::new(LazyCell::new(|| {
#[cfg(debug_assertions)]
return crate::document().get_element_by_id("_0-0-0").is_some()
|| crate::document().get_element_by_id("_0-0-0o").is_some()
|| HYDRATION_COMMENTS.with(|comments| comments.get("_0-0-0o").is_some());
#[cfg(not(debug_assertions))]
return crate::document().get_element_by_id("_0-0-0").is_some()
|| HYDRATION_COMMENTS.with(|comments| comments.get("_0-0-0").is_some());
}));
}
pub(crate) fn get_marker(id: &str) -> Option<web_sys::Comment> {
HYDRATION_COMMENTS.with(|comments| comments.get(id).cloned())
}
}
}
/// A stable identifer within the server-rendering or hydration process.

View File

@@ -1,4 +1,5 @@
#![deny(missing_docs)]
#![forbid(unsafe_code)]
#![cfg_attr(not(feature = "stable"), feature(fn_traits))]
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
@@ -206,7 +207,12 @@ impl Element {
is_void: false,
};
HtmlElement { cx, element }
HtmlElement {
cx,
element,
#[cfg(debug_assertions)]
span: ::tracing::Span::current(),
}
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
@@ -297,7 +303,7 @@ impl Comment {
if HydrationCtx::is_hydrating() {
let id = HydrationCtx::to_string(id, closing);
if let Some(marker) = document().get_element_by_id(&id) {
if let Some(marker) = hydration::get_marker(&id) {
marker.before_with_node_1(&node).unwrap();
marker.remove();
@@ -542,8 +548,19 @@ impl View {
pub fn on<E: ev::EventDescriptor + 'static>(
self,
event: E,
event_handler: impl FnMut(E::EventType) + 'static,
#[allow(unused_mut)] mut event_handler: impl FnMut(E::EventType) + 'static,
) -> Self {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
trace!("calling on() {}", event.name());
let span = ::tracing::Span::current();
let event_handler = move |e| {
let _guard = span.enter();
event_handler(e);
};
}
}
self.on_impl(event, Box::new(event_handler))
}

View File

@@ -45,7 +45,7 @@ macro_rules! debug_warn {
/// or via `println!()` (if not in the browser).
pub fn console_log(s: &str) {
if is_server() {
println!("{}", s);
println!("{s}");
} else {
web_sys::console::log_1(&JsValue::from_str(s));
}
@@ -55,7 +55,7 @@ pub fn console_log(s: &str) {
/// or via `println!()` (if not in the browser).
pub fn console_warn(s: &str) {
if is_server() {
eprintln!("{}", s);
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
@@ -65,7 +65,7 @@ pub fn console_warn(s: &str) {
/// or via `println!()` (if not in the browser).
pub fn console_error(s: &str) {
if is_server() {
eprintln!("{}", s);
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
@@ -77,7 +77,7 @@ pub fn console_debug_warn(s: &str) {
cfg_if! {
if #[cfg(debug_assertions)] {
if is_server() {
eprintln!("{}", s);
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}

View File

@@ -16,7 +16,7 @@ use std::borrow::Cow;
/// <p>"Hello, world!"</p>
/// });
/// // static HTML includes some hydration info
/// assert_eq!(html, "<style>[leptos]{display:none;}</style><p id=\"_0-1\">Hello, world!</p>");
/// assert_eq!(html, "<p id=\"_0-1\">Hello, world!</p>");
/// # }}
/// ```
pub fn render_to_string<F, N>(f: F) -> String
@@ -33,13 +33,7 @@ where
runtime.dispose();
#[cfg(debug_assertions)]
{
format!("<style>[leptos]{{display:none;}}</style>{html}")
}
#[cfg(not(debug_assertions))]
format!("<style>l-m{{display:none;}}</style>{html}")
html.into()
}
/// Renders a function to a stream of HTML strings.
@@ -122,16 +116,6 @@ pub fn render_to_stream_with_prefix_undisposed(
let pending_resources = serde_json::to_string(&resources).unwrap();
let prefix = prefix(cx);
let shell = {
#[cfg(debug_assertions)]
{
format!("<style>[leptos]{{display:none;}}</style>{shell}")
}
#[cfg(not(debug_assertions))]
format!("<style>l-m{{display:none;}}</style>{shell}")
};
(
shell,
prefix,
@@ -149,45 +133,19 @@ pub fn render_to_stream_with_prefix_undisposed(
// resources and fragments
// stream HTML for each <Suspense/> as it resolves
let fragments = fragments.map(|(fragment_id, id_before_suspense, html)| {
cfg_if! {
if #[cfg(debug_assertions)] {
_ = id_before_suspense;
// Debug-mode <Suspense/>-replacement code
format!(
r#"
<template id="{fragment_id}f">{html}</template>
<script>
var start = document.getElementById("_{fragment_id}o");
var end = document.getElementById("_{fragment_id}c");
var range = new Range();
range.setStartBefore(start.nextSibling.nextSibling);
range.setEndAfter(end.previousSibling.previousSibling);
range.deleteContents();
var tpl = document.getElementById("{fragment_id}f");
end.parentNode.insertBefore(tpl.content.cloneNode(true), end.previousSibling);
</script>
"#
)
} else {
// Release-mode <Suspense/>-replacement code
format!(
r#"
<template id="{fragment_id}f">{html}</template>
<script>
var start = document.getElementById("_{id_before_suspense}");
var end = document.getElementById("_{fragment_id}");
var range = new Range();
range.setStartAfter(start);
range.setEndBefore(end);
range.deleteContents();
var tpl = document.getElementById("{fragment_id}f");
end.parentNode.insertBefore(tpl.content.cloneNode(true), end.previousSibling);
</script>
"#
)
}
}
// TODO can remove id_before_suspense entirely now
let fragments = fragments.map(|(fragment_id, _, html)| {
format!(
r#"
<template id="{fragment_id}f">{html}</template>
<script>
var placeholder = document.getElementById("_{fragment_id}");
var tpl = document.getElementById("{fragment_id}f");
placeholder.textContent = "";
placeholder.append(tpl.content.cloneNode(true));
</script>
"#
)
});
// stream data for each Resource as it resolves
let resources = serializers.map(|(id, json)| {
@@ -244,7 +202,7 @@ impl View {
};
cfg_if! {
if #[cfg(debug_assertions)] {
format!(r#"<leptos-{name}-start leptos id="{}"></leptos-{name}-start>{}<leptos-{name}-end leptos id="{}"></leptos-{name}-end>"#,
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
HydrationCtx::to_string(&node.id, false),
content(),
HydrationCtx::to_string(&node.id, true),
@@ -252,7 +210,7 @@ impl View {
).into()
} else {
format!(
r#"{}<l-m id="{}"></l-m>"#,
r#"{}<!--hk={}-->"#,
content(),
HydrationCtx::to_string(&node.id, true)
).into()
@@ -269,14 +227,14 @@ impl View {
#[cfg(debug_assertions)]
{
format!(
"<leptos-unit leptos id={}></leptos-unit>",
"<!--hk={}|leptos-unit-->",
HydrationCtx::to_string(&u.id, true)
)
.into()
}
#[cfg(not(debug_assertions))]
format!("<l-m id={}></l-m>", HydrationCtx::to_string(&u.id, true))
format!("<!--hk={}-->", HydrationCtx::to_string(&u.id, true))
.into()
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
),
@@ -312,7 +270,6 @@ impl View {
}
CoreComponent::Each(node) => {
let children = node.children.take();
(
node.id,
"each",
@@ -329,10 +286,8 @@ impl View {
#[cfg(debug_assertions)]
{
format!(
"<leptos-each-item-start leptos \
id=\"{}\"></\
leptos-each-item-start>{}<leptos-each-item-end \
leptos id=\"{}\"></leptos-each-item-end>",
"<!--hk={}|leptos-each-item-start-->{}<!\
--hk={}|leptos-each-item-end-->",
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(&id, true),
@@ -341,7 +296,7 @@ impl View {
#[cfg(not(debug_assertions))]
format!(
"{}<l-m id=\"{}\"></l-m>",
"{}<!--hk={}-->",
content(),
HydrationCtx::to_string(&id, true)
)
@@ -357,7 +312,7 @@ impl View {
cfg_if! {
if #[cfg(debug_assertions)] {
format!(
r#"<leptos-{name}-start leptos id="{}"></leptos-{name}-start>{}<leptos-{name}-end leptos id="{}"></leptos-{name}-end>"#,
r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(&id, true),
@@ -366,7 +321,7 @@ impl View {
let _ = name;
format!(
r#"{}<l-m id="{}"></l-m>"#,
r#"{}<!--hk={}-->"#,
content(),
HydrationCtx::to_string(&id, true)
).into()
@@ -382,24 +337,33 @@ impl View {
} else {
let tag_name = el.name;
let mut inner_html = None;
let attrs = el
.attrs
.into_iter()
.map(|(name, value)| -> Cow<'static, str> {
.filter_map(|(name, value)| -> Option<Cow<'static, str>> {
if value.is_empty() {
format!(" {name}").into()
Some(format!(" {name}").into())
} else if name == "inner_html" {
inner_html = Some(value);
None
} else {
format!(
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
Some(
format!(
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
.into(),
)
.into()
}
})
.join("");
if el.is_void {
format!("<{tag_name}{attrs}/>").into()
} else if let Some(inner_html) = inner_html {
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>").into()
} else {
let children = el
.children
@@ -416,6 +380,7 @@ impl View {
}
}
#[cfg(debug_assertions)]
fn to_kebab_case(name: &str) -> String {
if name.is_empty() {
return String::new();
@@ -449,4 +414,4 @@ fn to_kebab_case(name: &str) -> String {
}
new_name
}
}

View File

@@ -1,10 +1,10 @@
[package]
name = "leptos_macro"
version = "0.1.0-beta"
version = { workspace = true }
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
repository = "https://github.com/leptos-rs/leptos"
description = "view macro for the Leptos web framework."
[lib]
@@ -22,9 +22,9 @@ quote = "1"
syn = { version = "1", features = ["full"] }
syn-rsx = "0.9"
uuid = { version = "1", features = ["v4"] }
leptos_dom = { path = "../leptos_dom", version = "0.1.0-beta" }
leptos_reactive = { path = "../leptos_reactive", version = "0.1.0-beta" }
leptos_server = { path = "../leptos_server", version = "0.1.0-beta" }
leptos_dom = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
lazy_static = "1.4"
[dev-dependencies]

View File

@@ -116,6 +116,7 @@ impl ToTokens for Model {
let body_name = body.sig.ident.clone();
let (_, generics, where_clause) = body.sig.generics.split_for_impl();
let lifetimes = body.sig.generics.lifetimes();
let props_name = format_ident!("{name}Props");
let trace_name = format!("<{name} />");
@@ -187,7 +188,7 @@ impl ToTokens for Model {
#[allow(unused_variables)]
#scope_name: Scope,
props: #props_name #generics
) #ret
) #ret #(+ #lifetimes)*
#where_clause
{
#body
@@ -377,10 +378,11 @@ impl Docs {
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
enum PropOpt {
Optional,
OptionalNoStrip,
OptionalWithDefault(syn::Lit),
StripOption,
Into,
}
@@ -389,7 +391,8 @@ impl PropOpt {
fn from_attribute(attr: &Attribute) -> Option<HashSet<Self>> {
const ABORT_OPT_MESSAGE: &str = "only `optional`, \
`optional_no_strip`, \
`strip_option`, and `into` are \
`strip_option`, \
`default` and `into` are \
allowed as arguments to `#[prop()]`";
if attr.path != parse_quote!(prop) {
@@ -400,8 +403,8 @@ impl PropOpt {
Some(
nested
.iter()
.map(|opt| {
if let NestedMeta::Meta(Meta::Path(opt)) = opt {
.map(|opt| match opt {
NestedMeta::Meta(Meta::Path(opt)) => {
if *opt == parse_quote!(optional) {
PropOpt::Optional
} else if *opt == parse_quote!(optional_no_strip) {
@@ -417,9 +420,23 @@ impl PropOpt {
help = ABORT_OPT_MESSAGE
);
}
} else {
abort!(opt, ABORT_OPT_MESSAGE,);
}
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
path,
eq_token: _,
lit,
})) => {
if *path == parse_quote!(default) {
PropOpt::OptionalWithDefault(lit.to_owned())
} else {
abort!(
opt,
"invalid prop option";
help = ABORT_OPT_MESSAGE
);
}
}
_ => abort!(opt, ABORT_OPT_MESSAGE,),
})
.collect(),
)
@@ -436,6 +453,7 @@ impl PropOpt {
struct TypedBuilderOpts {
default: bool,
default_with_value: Option<syn::Lit>,
strip_option: bool,
into: bool,
}
@@ -444,6 +462,10 @@ impl TypedBuilderOpts {
fn from_opts(opts: &HashSet<PropOpt>, is_ty_option: bool) -> Self {
Self {
default: opts.contains(&PropOpt::Optional) || opts.contains(&PropOpt::OptionalNoStrip),
default_with_value: opts.iter().find_map(|p| match p {
PropOpt::OptionalWithDefault(v) => Some(v.to_owned()),
_ => None,
}),
strip_option: opts.contains(&PropOpt::StripOption)
|| (opts.contains(&PropOpt::Optional) && is_ty_option),
into: opts.contains(&PropOpt::Into),
@@ -453,7 +475,9 @@ impl TypedBuilderOpts {
impl ToTokens for TypedBuilderOpts {
fn to_tokens(&self, tokens: &mut TokenStream) {
let default = if self.default {
let default = if let Some(v) = &self.default_with_value {
quote! { default=#v, }
} else if self.default {
quote! { default, }
} else {
quote! {}

View File

@@ -1,4 +1,5 @@
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
#![forbid(unsafe_code)]
#[macro_use]
extern crate proc_macro_error;
@@ -176,6 +177,25 @@ mod server;
/// # });
/// ```
///
/// However, you can pass arbitrary class names using the syntax `class=("name", value)`.
/// ```rust
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 2);
/// // this allows you to use CSS frameworks that include complex class names
/// view! { cx,
/// <div
/// class=("is-[this_-_really]-necessary-42", move || count() < 3)
/// >
/// "Now you see me, now you dont."
/// </div>
/// }
/// # ;
/// # }
/// # });
/// ```
///
/// 8. You can use the `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](leptos_reactive::NodeRef) to use later.
/// ```rust
@@ -358,12 +378,12 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// ```
///
/// 5. You can access the children passed into the component with the `children` property, which takes
/// an argument of the form `Box<dyn Fn(Scope) -> Fragment>`.
/// an argument of the form `Box<dyn FnOnce(Scope) -> Fragment>`.
///
/// ```
/// # use leptos::*;
/// #[component]
/// fn ComponentWithChildren(cx: Scope, children: Box<dyn Fn(Scope) -> Fragment>) -> impl IntoView {
/// fn ComponentWithChildren(cx: Scope, children: Box<dyn FnOnce(Scope) -> Fragment>) -> impl IntoView {
/// view! {
/// cx,
/// <ul>

View File

@@ -105,11 +105,11 @@ mod struct_info {
builder_attr,
builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()),
conversion_helper_trait_name: syn::Ident::new(
&format!("{}_Optional", builder_name),
&format!("{builder_name}_Optional"),
proc_macro2::Span::call_site(),
),
core: syn::Ident::new(
&format!("{}_core", builder_name),
&format!("{builder_name}_core"),
proc_macro2::Span::call_site(),
),
})
@@ -188,8 +188,7 @@ mod struct_info {
Some(ref doc) => quote!(#[doc = #doc]),
None => {
let doc = format!(
"Builder for [`{name}`] instances.\n\nSee [`{name}::builder()`] for more info.",
name = name
"Builder for [`{name}`] instances.\n\nSee [`{name}::builder()`] for more info."
);
quote!(#[doc = #doc])
}
@@ -282,7 +281,7 @@ mod struct_info {
});
let reconstructing = self.included_fields().map(|f| f.name);
let &FieldInfo {
let FieldInfo {
name: ref field_name,
ty: ref field_type,
..
@@ -391,7 +390,7 @@ mod struct_info {
),
proc_macro2::Span::call_site(),
);
let repeated_fields_error_message = format!("Repeated field {}", field_name);
let repeated_fields_error_message = format!("Repeated field {field_name}");
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
@@ -513,7 +512,7 @@ mod struct_info {
),
proc_macro2::Span::call_site(),
);
let early_build_error_message = format!("Missing required field {}", field_name);
let early_build_error_message = format!("Missing required field {field_name}");
Ok(quote! {
#[doc(hidden)]
@@ -622,7 +621,7 @@ mod struct_info {
// I'd prefer “a” or “an” to “its”, but determining which is grammatically
// correct is roughly impossible.
let doc =
format!("Finalise the builder and create its [`{}`] instance", name);
format!("Finalise the builder and create its [`{name}`] instance");
quote!(#[doc = #doc])
}
}
@@ -718,7 +717,7 @@ mod struct_info {
}
_ => Err(Error::new_spanned(
&assign,
format!("Unknown parameter {:?}", name),
format!("Unknown parameter {name:?}"),
)),
}
}
@@ -732,7 +731,7 @@ mod struct_info {
}
_ => Err(Error::new_spanned(
&path,
format!("Unknown parameter {:?}", name),
format!("Unknown parameter {name:?}"),
)),
}
}
@@ -747,7 +746,7 @@ mod struct_info {
let call_func = quote!(#call_func);
Error::new_spanned(
&call.func,
format!("Illegal builder setting group {}", call_func),
format!("Illegal builder setting group {call_func}"),
)
})?;
match subsetting_name.as_str() {
@@ -759,7 +758,7 @@ mod struct_info {
}
_ => Err(Error::new_spanned(
&call.func,
format!("Illegal builder setting group name {}", subsetting_name),
format!("Illegal builder setting group name {subsetting_name}"),
)),
}
}
@@ -924,7 +923,7 @@ mod field_info {
let tokenized_code = TokenStream::from_str(&code.value())?;
self.default = Some(
syn::parse(tokenized_code.into())
.map_err(|e| Error::new_spanned(code, format!("{}", e)))?,
.map_err(|e| Error::new_spanned(code, format!("{e}")))?,
);
} else {
return Err(Error::new_spanned(assign.right, "Expected string"));
@@ -933,7 +932,7 @@ mod field_info {
}
_ => Err(Error::new_spanned(
&assign,
format!("Unknown parameter {:?}", name),
format!("Unknown parameter {name:?}"),
)),
}
}
@@ -950,7 +949,7 @@ mod field_info {
}
_ => Err(Error::new_spanned(
&path,
format!("Unknown parameter {:?}", name),
format!("Unknown parameter {name:?}"),
)),
}
}
@@ -965,7 +964,7 @@ mod field_info {
let call_func = quote!(#call_func);
Error::new_spanned(
&call.func,
format!("Illegal builder setting group {}", call_func),
format!("Illegal builder setting group {call_func}"),
)
})?;
match subsetting_name.as_ref() {
@@ -977,7 +976,7 @@ mod field_info {
}
_ => Err(Error::new_spanned(
&call.func,
format!("Illegal builder setting group name {}", subsetting_name),
format!("Illegal builder setting group name {subsetting_name}"),
)),
}
}
@@ -1047,7 +1046,7 @@ mod field_info {
}
_ => Err(Error::new_spanned(
&assign,
format!("Unknown parameter {:?}", name),
format!("Unknown parameter {name:?}"),
)),
}
}

View File

@@ -47,9 +47,9 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
use proc_macro::Span;
let span = Span::call_site();
#[cfg(not(target_os = "windows"))]
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("/", "-");
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace('/', "-");
#[cfg(target_os = "windows")]
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("\\", "-");
let url = format!("{}\\{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("\\", "-");
} else {
let url = fn_name_as_str;
}

View File

@@ -1,6 +1,6 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use syn::{spanned::Spanned, ExprPath};
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName};
use crate::{is_component_node, Mode};
@@ -409,7 +409,9 @@ fn set_class_attribute_ssr(
.filter_map(|a| {
if let Node::Attribute(a) = a {
if a.key.to_string() == "class" {
if a.value.as_ref().and_then(value_to_string).is_some() {
if a.value.as_ref().and_then(value_to_string).is_some()
|| fancy_class_name(&a.key.to_string(), cx, a).is_some()
{
None
} else {
Some((a.key.span(), &a.value))
@@ -429,6 +431,14 @@ fn set_class_attribute_ssr(
.filter_map(|node| {
if let Node::Attribute(node) = node {
let name = node.key.to_string();
if name == "class" {
return if let Some((_, name, value)) = fancy_class_name(&name, cx, node) {
let span = node.key.span();
Some((span, name, value))
} else {
None
};
}
if name.starts_with("class:") || name.starts_with("class-") {
let name = if name.starts_with("class:") {
name.replacen("class:", "", 1)
@@ -544,7 +554,7 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
let tag = node.name.to_string();
let name = if is_custom_element(&tag) {
let name = node.name.to_string();
quote! { leptos::leptos_dom::custom(#cx, #name) }
quote! { leptos::leptos_dom::custom(#cx, leptos::leptos_dom::Custom::new(#name)) }
} else if is_svg_element(&tag) {
let name = &node.name;
parent_type = TagType::Svg;
@@ -557,8 +567,9 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
let name = &node.name;
match parent_type {
TagType::Unknown => {
proc_macro_error::emit_warning!(name.span(), "The view macro is assuming this is an HTML element, \
but it is ambiguous; if it is an SVG or MathML element, prefix with svg:: or math::");
// We decided this warning was too aggressive, but I'll leave it here in case we want it later
/* proc_macro_error::emit_warning!(name.span(), "The view macro is assuming this is an HTML element, \
but it is ambiguous; if it is an SVG or MathML element, prefix with svg:: or math::"); */
quote! {
leptos::leptos_dom::#name(#cx)
}
@@ -752,6 +763,12 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
}
} else {
let name = name.replacen("attr:", "", 1);
if let Some((fancy, _, _)) = fancy_class_name(&name, cx, node) {
return fancy;
}
// all other attributes
let value = match node.value.as_ref() {
Some(value) => {
let value = value.as_ref();
@@ -866,7 +883,11 @@ fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
.last()
.map(|segment| segment.ident.clone())
.expect("element needs to have a name"),
NodeName::Block(_) => panic!("blocks not allowed in tag-name position"),
NodeName::Block(_) => {
let span = tag_name.span();
proc_macro_error::emit_error!(span, "blocks not allowed in tag-name position");
Ident::new("", span)
}
_ => Ident::new(
&tag_name.to_string().replace(['-', ':'], "_"),
tag_name.span(),
@@ -1044,3 +1065,51 @@ fn parse_event(event_name: &str) -> (&str, bool) {
(event_name, false)
}
}
fn fancy_class_name<'a>(
name: &str,
cx: &Ident,
node: &'a NodeAttribute,
) -> Option<(TokenStream, String, &'a Expr)> {
// special case for complex class names:
// e.g., Tailwind `class=("mt-[calc(100vh_-_3rem)]", true)`
if name == "class" {
if let Some(expr) = node.value.as_ref() {
if let syn::Expr::Tuple(tuple) = expr.as_ref() {
if tuple.elems.len() == 2 {
let span = node.key.span();
let class = quote_spanned! {
span => .class
};
let class_name = &tuple.elems[0];
let class_name = if let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = class_name
{
s.value()
} else {
proc_macro_error::emit_error!(
class_name.span(),
"class name must be a string literal"
);
Default::default()
};
let value = &tuple.elems[1];
return Some((
quote! {
#class(#class_name, (#cx, #value))
},
class_name,
value,
));
} else {
proc_macro_error::emit_error!(
tuple.span(),
"class tuples must have two elements."
)
}
}
}
}
None
}

View File

@@ -1,10 +1,10 @@
[package]
name = "leptos_reactive"
version = "0.1.0-beta"
version = { workspace = true }
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
repository = "https://github.com/leptos-rs/leptos"
description = "Reactive system for the Leptos web framework."
[dependencies]

View File

@@ -1,3 +1,4 @@
#![forbid(unsafe_code)]
use std::{
any::{Any, TypeId},
collections::HashMap,

Some files were not shown because too many files have changed in this diff Show More