mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
122 Commits
v0.1.0
...
Fix-missin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8eafe1d2c | ||
|
|
1563d237d0 | ||
|
|
b861f84e40 | ||
|
|
62812af5b2 | ||
|
|
f300e7fd41 | ||
|
|
44974fcf69 | ||
|
|
815c2e6dc2 | ||
|
|
2fc20d8312 | ||
|
|
679692e202 | ||
|
|
be1343fa88 | ||
|
|
fc7199f188 | ||
|
|
154e42f3f4 | ||
|
|
4c24795ffd | ||
|
|
f2e7b00d5a | ||
|
|
0b36b68846 | ||
|
|
f24bad4bf2 | ||
|
|
a2ea1d8483 | ||
|
|
9b0fb63632 | ||
|
|
2febaf6b99 | ||
|
|
6c8e8e9ce7 | ||
|
|
7aa0181192 | ||
|
|
8496bd59ce | ||
|
|
fd6e63796e | ||
|
|
39cddfc82d | ||
|
|
d1333a3402 | ||
|
|
7f9919e2d5 | ||
|
|
fc2d6ef19d | ||
|
|
a5531b1a7c | ||
|
|
81ab77e8ea | ||
|
|
23bd399239 | ||
|
|
3e04318082 | ||
|
|
2d88524354 | ||
|
|
ecb784e422 | ||
|
|
69e02bfce2 | ||
|
|
a75abb9e04 | ||
|
|
bf1ef1b7c2 | ||
|
|
7fb7bb90f8 | ||
|
|
a22a693de7 | ||
|
|
cbb1e4c9d2 | ||
|
|
dbccf525ac | ||
|
|
ed6d6ae4b0 | ||
|
|
89ee88d75e | ||
|
|
9ea604f516 | ||
|
|
b5ab7b107a | ||
|
|
18eecd9606 | ||
|
|
a49dfd3f8e | ||
|
|
7075f58451 | ||
|
|
bcabdddce5 | ||
|
|
726393c446 | ||
|
|
c336eb8769 | ||
|
|
0f5f0de410 | ||
|
|
3d769c9f21 | ||
|
|
9ac0f0a579 | ||
|
|
a385f502b6 | ||
|
|
603eead12d | ||
|
|
8a73f3b879 | ||
|
|
5fc8907b85 | ||
|
|
678990194f | ||
|
|
a964e89d1a | ||
|
|
285092a467 | ||
|
|
c1c74ead0f | ||
|
|
e4c9109278 | ||
|
|
64add54de6 | ||
|
|
ac343427e7 | ||
|
|
452e397048 | ||
|
|
ad3ac5ad3c | ||
|
|
db63eda2f5 | ||
|
|
6cbdc57f7a | ||
|
|
3215e44c9a | ||
|
|
b54a60213b | ||
|
|
ebeb1d69d1 | ||
|
|
cadb04b076 | ||
|
|
490b7a1596 | ||
|
|
f4d781e739 | ||
|
|
ebe5bf4600 | ||
|
|
d62046dc6f | ||
|
|
c7abb57168 | ||
|
|
bc0cd5d0ba | ||
|
|
7df67444f9 | ||
|
|
40155e91ea | ||
|
|
5c062fa6f1 | ||
|
|
3517820afd | ||
|
|
300cc4f54c | ||
|
|
586e9be99a | ||
|
|
6ed86d0ee9 | ||
|
|
1fe93fd588 | ||
|
|
2723871a80 | ||
|
|
70d92c7f42 | ||
|
|
e96d4b0687 | ||
|
|
ce0910caca | ||
|
|
81a937277d | ||
|
|
355e711964 | ||
|
|
27ec506fd5 | ||
|
|
79c76ae4cb | ||
|
|
e416815591 | ||
|
|
81bdd6788f | ||
|
|
f7d5567a35 | ||
|
|
ae0a243cc0 | ||
|
|
7893ff8b55 | ||
|
|
6130e708ce | ||
|
|
d049d2f36b | ||
|
|
61ca6465df | ||
|
|
10a833d763 | ||
|
|
17982e8ac5 | ||
|
|
6cfb6227f5 | ||
|
|
159852b8d7 | ||
|
|
6f95713b59 | ||
|
|
e17afd4559 | ||
|
|
e0aa1e245b | ||
|
|
7951a6e9cf | ||
|
|
1f39299303 | ||
|
|
2be4610233 | ||
|
|
af254e9b61 | ||
|
|
4aae8a5088 | ||
|
|
7ff044cef6 | ||
|
|
11122b575e | ||
|
|
a62ee4031b | ||
|
|
19b43607a1 | ||
|
|
884297706a | ||
|
|
fb4c208609 | ||
|
|
5701e74efb | ||
|
|
2afe8e202a |
11
.github/workflows/test.yml
vendored
11
.github/workflows/test.yml
vendored
@@ -39,16 +39,7 @@ jobs:
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- 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') }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make ci
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ blob.rs
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.idea
|
||||
|
||||
51
CODE_OF_CONDUCT.md
Normal file
51
CODE_OF_CONDUCT.md
Normal 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. There’s 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 don’t 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 you’re a regular contributor or a newcomer, we care about making this community a safe place for you and we’ve 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 community’s 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.
|
||||
18
Cargo.toml
18
Cargo.toml
@@ -24,17 +24,17 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
|
||||
[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" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.1.1" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.1" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.1" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.1" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.1" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.1" }
|
||||
leptos_router = { path = "./router", version = "0.1.1" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.1" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
35
README.md
35
README.md
@@ -1,5 +1,3 @@
|
||||
**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.**
|
||||
|
||||
<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">
|
||||
@@ -31,7 +29,7 @@ pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
<div>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=increment>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
@@ -61,9 +59,9 @@ 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
|
||||
@@ -83,12 +81,12 @@ If you’re 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 you’re using `stable`,
|
||||
you’ll 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.
|
||||
|
||||
## `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
|
||||
@@ -97,8 +95,28 @@ cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
## FAQs
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
People usually mean one of three things by this question.
|
||||
|
||||
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
|
||||
|
||||
With 0.1 the APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to, for example, a 0.2.0 release.
|
||||
|
||||
2. **Are there bugs?**
|
||||
|
||||
Yes, I’m sure there are. You can see from the state of our issue tracker over time that there aren’t that _many_ bugs and they’re usually resolved pretty quickly. But for sure, there may be moments where you encounter something that requires a fix at the framework level, which may not be immediately resolved.
|
||||
|
||||
3. **Am I a consumer or a contributor?**
|
||||
|
||||
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) don’t have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
|
||||
|
||||
There are several people in this community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
|
||||
### Can I use this for native GUI?
|
||||
|
||||
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive native any GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:
|
||||
@@ -108,7 +126,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?
|
||||
|
||||
@@ -126,7 +144,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:
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
b.iter(|| {
|
||||
use leptos::*;
|
||||
|
||||
HydrationCtx::reset_id();
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
|
||||
@@ -32,18 +32,17 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><div><button>-1</button><span>Value: <!>1<template id=\"_3\"></template>!</span><button>+1</button></div><template id=\"_1\"></template><div><button>-1</button><span>Value: <!>2<template id=\"_2\"></template>!</span><button>+1</button></div><template id=\"_0\"></template><div><button>-1</button><span>Value: <!>3<template id=\"_2\"></template>!</span><button>+1</button></div><template id=\"_0\"></template></main>"
|
||||
);
|
||||
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>" );
|
||||
});
|
||||
});
|
||||
}
|
||||
/*
|
||||
|
||||
#[bench]
|
||||
fn tera_ssr_bench(b: &mut Bencher) {
|
||||
use tera::*;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
static TEMPLATE: &str = r#"<main>
|
||||
static TEMPLATE: &str = r#"<main>
|
||||
<h1>Welcome to our benchmark page.</h1>
|
||||
<p>Here's some introductory text.</p>
|
||||
{% for counter in counters %}
|
||||
@@ -55,37 +54,40 @@ fn tera_ssr_bench(b: &mut Bencher) {
|
||||
{% endfor %}
|
||||
</main>"#;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
};
|
||||
}
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Counter {
|
||||
value: i32
|
||||
}
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Counter {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
b.iter(|| {
|
||||
let mut ctx = Context::new();
|
||||
ctx.insert("counters", &vec![
|
||||
Counter { value: 0 },
|
||||
Counter { value: 1},
|
||||
Counter { value: 2 }
|
||||
]);
|
||||
b.iter(|| {
|
||||
let mut ctx = Context::new();
|
||||
ctx.insert(
|
||||
"counters",
|
||||
&vec![
|
||||
Counter { value: 0 },
|
||||
Counter { value: 1 },
|
||||
Counter { value: 2 },
|
||||
],
|
||||
);
|
||||
|
||||
let _ = TERA.render("template.html", &ctx).unwrap();
|
||||
});
|
||||
let _ = TERA.render("template.html", &ctx).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_ssr_bench(b: &mut Bencher) {
|
||||
use sycamore::*;
|
||||
use sycamore::prelude::*;
|
||||
use sycamore::prelude::*;
|
||||
use sycamore::*;
|
||||
|
||||
b.iter(|| {
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
#[derive(Prop)]
|
||||
struct CounterProps {
|
||||
@@ -139,10 +141,10 @@ fn sycamore_ssr_bench(b: &mut Bencher) {
|
||||
|
||||
#[bench]
|
||||
fn yew_ssr_bench(b: &mut Bencher) {
|
||||
use yew::prelude::*;
|
||||
use yew::ServerRenderer;
|
||||
use yew::prelude::*;
|
||||
use yew::ServerRenderer;
|
||||
|
||||
b.iter(|| {
|
||||
b.iter(|| {
|
||||
#[derive(Properties, PartialEq, Eq, Debug)]
|
||||
struct CounterProps {
|
||||
initial: i32
|
||||
@@ -194,4 +196,3 @@ fn yew_ssr_bench(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
*/
|
||||
@@ -8,171 +8,165 @@ pub struct Todos(pub Vec<Todo>);
|
||||
const STORAGE_KEY: &str = "todos-leptos";
|
||||
|
||||
impl Todos {
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
pub fn new_with_1000(cx: Scope) -> Self {
|
||||
let todos = (0..1000)
|
||||
.map(|id| Todo::new(cx, id, format!("Todo #{id}")))
|
||||
.collect();
|
||||
Self(todos)
|
||||
}
|
||||
pub fn new_with_1000(cx: Scope) -> Self {
|
||||
let todos = (0..1000)
|
||||
.map(|id| Todo::new(cx, id, format!("Todo #{id}")))
|
||||
.collect();
|
||||
Self(todos)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, todo: Todo) {
|
||||
self.0.push(todo);
|
||||
}
|
||||
pub fn add(&mut self, todo: Todo) {
|
||||
self.0.push(todo);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: usize) {
|
||||
self.0.retain(|todo| todo.id != id);
|
||||
}
|
||||
pub fn remove(&mut self, id: usize) {
|
||||
self.0.retain(|todo| todo.id != id);
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.0.iter().filter(|todo| !(todo.completed)()).count()
|
||||
}
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.0.iter().filter(|todo| !(todo.completed)()).count()
|
||||
}
|
||||
|
||||
pub fn completed(&self) -> usize {
|
||||
self.0.iter().filter(|todo| (todo.completed)()).count()
|
||||
}
|
||||
pub fn completed(&self) -> usize {
|
||||
self.0.iter().filter(|todo| (todo.completed)()).count()
|
||||
}
|
||||
|
||||
pub fn toggle_all(&self) {
|
||||
// if all are complete, mark them all active instead
|
||||
if self.remaining() == 0 {
|
||||
for todo in &self.0 {
|
||||
if todo.completed.get() {
|
||||
(todo.set_completed)(false);
|
||||
pub fn toggle_all(&self) {
|
||||
// if all are complete, mark them all active instead
|
||||
if self.remaining() == 0 {
|
||||
for todo in &self.0 {
|
||||
if todo.completed.get() {
|
||||
(todo.set_completed)(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// otherwise, mark them all complete
|
||||
else {
|
||||
for todo in &self.0 {
|
||||
(todo.set_completed)(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// otherwise, mark them all complete
|
||||
else {
|
||||
for todo in &self.0 {
|
||||
(todo.set_completed)(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_completed(&mut self) {
|
||||
self.0.retain(|todo| !todo.completed.get());
|
||||
}
|
||||
fn clear_completed(&mut self) {
|
||||
self.0.retain(|todo| !todo.completed.get());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Todo {
|
||||
pub id: usize,
|
||||
pub title: ReadSignal<String>,
|
||||
pub set_title: WriteSignal<String>,
|
||||
pub completed: ReadSignal<bool>,
|
||||
pub set_completed: WriteSignal<bool>,
|
||||
pub id: usize,
|
||||
pub title: ReadSignal<String>,
|
||||
pub set_title: WriteSignal<String>,
|
||||
pub completed: ReadSignal<bool>,
|
||||
pub set_completed: WriteSignal<bool>,
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(cx: Scope, id: usize, title: String) -> Self {
|
||||
Self::new_with_completed(cx, id, title, false)
|
||||
}
|
||||
|
||||
pub fn new_with_completed(
|
||||
cx: Scope,
|
||||
id: usize,
|
||||
title: String,
|
||||
completed: bool,
|
||||
) -> Self {
|
||||
let (title, set_title) = create_signal(cx, title);
|
||||
let (completed, set_completed) = create_signal(cx, completed);
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
set_title,
|
||||
completed,
|
||||
set_completed,
|
||||
pub fn new(cx: Scope, id: usize, title: String) -> Self {
|
||||
Self::new_with_completed(cx, id, title, false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
self
|
||||
.set_completed
|
||||
.update(|completed| *completed = !*completed);
|
||||
}
|
||||
pub fn new_with_completed(cx: Scope, id: usize, title: String, completed: bool) -> Self {
|
||||
let (title, set_title) = create_signal(cx, title);
|
||||
let (completed, set_completed) = create_signal(cx, completed);
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
set_title,
|
||||
completed,
|
||||
set_completed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
self.set_completed
|
||||
.update(|completed| *completed = !*completed);
|
||||
}
|
||||
}
|
||||
|
||||
const ESCAPE_KEY: u32 = 27;
|
||||
const ENTER_KEY: u32 = 13;
|
||||
|
||||
#[component]
|
||||
pub fn TodoMVC(cx: Scope,todos: Todos) -> impl IntoView {
|
||||
let mut next_id = todos
|
||||
.0
|
||||
.iter()
|
||||
.map(|todo| todo.id)
|
||||
.max()
|
||||
.map(|last| last + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
let (todos, set_todos) = create_signal(cx, todos);
|
||||
provide_context(cx, set_todos);
|
||||
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener("hashchange", move |_| {
|
||||
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(cx, next_id, title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
next_id += 1;
|
||||
target.set_value("");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
|
||||
todos.with(|todos| match mode.get() {
|
||||
Mode::All => todos.0.to_vec(),
|
||||
Mode::Active => todos
|
||||
pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
let mut next_id = todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| !todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
Mode::Completed => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
})
|
||||
});
|
||||
.map(|todo| todo.id)
|
||||
.max()
|
||||
.map(|last| last + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
// effect to serialize to JSON
|
||||
// this does reactive reads, so it will automatically serialize on any relevant change
|
||||
create_effect(cx, move |_| {
|
||||
if let Ok(Some(storage)) = window().local_storage() {
|
||||
let objs = todos
|
||||
.get()
|
||||
.0
|
||||
.iter()
|
||||
.map(TodoSerialized::from)
|
||||
.collect::<Vec<_>>();
|
||||
let json = json::to_string(&objs);
|
||||
if storage.set_item(STORAGE_KEY, &json).is_err() {
|
||||
log::error!("error while trying to set item in localStorage");
|
||||
}
|
||||
}
|
||||
});
|
||||
let (todos, set_todos) = create_signal(cx, todos);
|
||||
provide_context(cx, set_todos);
|
||||
|
||||
view! { cx,
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener("hashchange", move |_| {
|
||||
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(cx, next_id, title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
next_id += 1;
|
||||
target.set_value("");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
|
||||
todos.with(|todos| match mode.get() {
|
||||
Mode::All => todos.0.to_vec(),
|
||||
Mode::Active => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| !todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
Mode::Completed => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
})
|
||||
});
|
||||
|
||||
// effect to serialize to JSON
|
||||
// this does reactive reads, so it will automatically serialize on any relevant change
|
||||
create_effect(cx, move |_| {
|
||||
if let Ok(Some(storage)) = window().local_storage() {
|
||||
let objs = todos
|
||||
.get()
|
||||
.0
|
||||
.iter()
|
||||
.map(TodoSerialized::from)
|
||||
.collect::<Vec<_>>();
|
||||
let json = json::to_string(&objs);
|
||||
if storage.set_item(STORAGE_KEY, &json).is_err() {
|
||||
log::error!("error while trying to set item in localStorage");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
@@ -228,100 +222,100 @@ pub fn TodoMVC(cx: Scope,todos: Todos) -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
//let input = NodeRef::new(cx);
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
//let input = NodeRef::new(cx);
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
set_todos.update(|t| t.remove(todo.id));
|
||||
} else {
|
||||
(todo.set_title)(value.to_string());
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
set_todos.update(|t| t.remove(todo.id));
|
||||
} else {
|
||||
(todo.set_title)(value.to_string());
|
||||
}
|
||||
set_editing(false);
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={move || (todo.completed)()}
|
||||
//_ref=input
|
||||
>
|
||||
<div class="view">
|
||||
<input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={move || (todo.completed)()}
|
||||
|
||||
/>
|
||||
<label on:dblclick=move |_| set_editing(true)>
|
||||
{move || todo.title.get()}
|
||||
</label>
|
||||
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
|
||||
</div>
|
||||
{move || editing().then(|| view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden={move || !(editing)()}
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</li>
|
||||
}
|
||||
set_editing(false);
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={move || (todo.completed)()}
|
||||
//_ref=input
|
||||
>
|
||||
<div class="view">
|
||||
<input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={move || (todo.completed)()}
|
||||
|
||||
/>
|
||||
<label on:dblclick=move |_| set_editing(true)>
|
||||
{move || todo.title.get()}
|
||||
</label>
|
||||
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
|
||||
</div>
|
||||
{move || editing().then(|| view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden={move || !(editing)()}
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Active,
|
||||
Completed,
|
||||
All,
|
||||
Active,
|
||||
Completed,
|
||||
All,
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Mode::All
|
||||
}
|
||||
fn default() -> Self {
|
||||
Mode::All
|
||||
}
|
||||
}
|
||||
|
||||
pub fn route(hash: &str) -> Mode {
|
||||
match hash {
|
||||
"/active" => Mode::Active,
|
||||
"/completed" => Mode::Completed,
|
||||
_ => Mode::All,
|
||||
}
|
||||
match hash {
|
||||
"/active" => Mode::Active,
|
||||
"/completed" => Mode::Completed,
|
||||
_ => Mode::All,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TodoSerialized {
|
||||
pub id: usize,
|
||||
pub title: String,
|
||||
pub completed: bool,
|
||||
pub id: usize,
|
||||
pub title: String,
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl TodoSerialized {
|
||||
pub fn into_todo(self, cx: Scope) -> Todo {
|
||||
Todo::new_with_completed(cx, self.id, self.title, self.completed)
|
||||
}
|
||||
pub fn into_todo(self, cx: Scope) -> Todo {
|
||||
Todo::new_with_completed(cx, self.id, self.title, self.completed)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Todo> for TodoSerialized {
|
||||
fn from(todo: &Todo) -> Self {
|
||||
Self {
|
||||
id: todo.id,
|
||||
title: todo.title.get(),
|
||||
completed: (todo.completed)(),
|
||||
fn from(todo: &Todo) -> Self {
|
||||
Self {
|
||||
id: todo.id,
|
||||
title: todo.title.get(),
|
||||
completed: (todo.completed)(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new(cx)/>
|
||||
}.into_view(cx).render_to_string(cx);
|
||||
}
|
||||
.into_view(cx)
|
||||
.render_to_string(cx);
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
});
|
||||
@@ -55,7 +57,7 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
@@ -107,3 +109,4 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Introduction
|
||||
|
||||
This book is intended as an introduction to the [Leptos](https://github.com/gbj/leptos) Web framework. Together, we’ll 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, we’ll build a simple todo app—first as a client-side app, then as a full-stack app.
|
||||
|
||||
The guide doesn’t 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.
|
||||
|
||||
|
||||
@@ -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 don’t already have it installed, you can install Trunk by running
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -3,3 +3,5 @@
|
||||
This example creates a simple counter in a client side rendered app with Rust and WASM!
|
||||
|
||||
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
|
||||
@@ -17,7 +17,7 @@ pub fn SimpleCounter(
|
||||
<div>
|
||||
<button on:click=move |_| set_value(0)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ skip_feature_sets = [["ssr", "hydrate"]]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "counter_isomorphic"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
# When NOT using cargo-leptos this must be updated to "." or the counters will not work. The above warning still applies if you do switch to cargo-leptos later.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
@@ -56,7 +57,7 @@ site-pkg-dir = "pkg"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
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"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result.
|
||||
|
||||
## Client Side Rendering
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
|
||||
For this example the server must store the counter state since it can be modified by many users.
|
||||
This means it is not possible to produce a working CSR-only version as a non-static server is required.
|
||||
|
||||
## Server Side Rendering with cargo-leptos
|
||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||
@@ -17,6 +17,9 @@ cargo install --locked cargo-leptos
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
@@ -25,7 +28,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 `"."`. For examples with CSS you 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
|
||||
|
||||
@@ -116,7 +116,7 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
{move || error_msg().map(|msg| view! { cx, <p>"Error: " {msg.to_string()}</p>})}
|
||||
|
||||
@@ -33,7 +33,10 @@ cfg_if! {
|
||||
|
||||
crate::counters::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.
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_address.clone();
|
||||
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
|
||||
|
||||
@@ -54,14 +57,11 @@ cfg_if! {
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
// client-only main for Trunk
|
||||
else {
|
||||
use counter_isomorphic::counters::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <Counter/> });
|
||||
// isomorphic counters cannot work in a Client-Side-Rendered only
|
||||
// app as a server is required to maintain state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
examples/counter_without_macros/Cargo.toml
Normal file
13
examples/counter_without_macros/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "counter_without_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
5
examples/counter_without_macros/README.md
Normal file
5
examples/counter_without_macros/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Leptos Counter Example
|
||||
|
||||
This example is the same like the `counter` but it's written without using macros and can be build with stable Rust.
|
||||
|
||||
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
|
||||
@@ -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>
|
||||
BIN
examples/counter_without_macros/public/favicon.ico
Normal file
BIN
examples/counter_without_macros/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
44
examples/counter_without_macros/src/lib.rs
Normal file
44
examples/counter_without_macros/src/lib.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use leptos::{ev, *};
|
||||
|
||||
pub struct Props {
|
||||
/// The starting value for the counter
|
||||
pub initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
pub step: i32,
|
||||
}
|
||||
|
||||
/// A simple counter view.
|
||||
pub fn view(cx: Scope, props: Props) -> impl IntoView {
|
||||
let Props {
|
||||
initial_value,
|
||||
step,
|
||||
} = props;
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
div(cx)
|
||||
.child((
|
||||
cx,
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value = 0))
|
||||
.child((cx, "Clear")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value -= step))
|
||||
.child((cx, "-1")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
span(cx)
|
||||
.child((cx, "Value: "))
|
||||
.child((cx, move || value.get()))
|
||||
.child((cx, "!")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value += step))
|
||||
.child((cx, "+1")),
|
||||
))
|
||||
}
|
||||
16
examples/counter_without_macros/src/main.rs
Normal file
16
examples/counter_without_macros/src/main.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use counter_without_macros as counter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
58
examples/counter_without_macros/tests/mod.rs
Normal file
58
examples/counter_without_macros/tests/mod.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counter_without_macros as counter;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let clear = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let dec = clear
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let text = dec
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let inc = text
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
inc.click();
|
||||
inc.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 2!".to_string()));
|
||||
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: -2!".to_string()));
|
||||
|
||||
clear.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
|
||||
}
|
||||
9
examples/counters/README.md
Normal file
9
examples/counters/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Leptos Counters Example
|
||||
|
||||
This example showcases a basic leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -97,10 +97,10 @@ fn Counter(
|
||||
<li>
|
||||
<button on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input type="text"
|
||||
prop:value={move || value().to_string()}
|
||||
prop:value={value}
|
||||
on:input=input
|
||||
/>
|
||||
<span>{move || value().to_string()}</span>
|
||||
<span>{value}</span>
|
||||
<button on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
</li>
|
||||
|
||||
9
examples/counters_stable/README.md
Normal file
9
examples/counters_stable/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Leptos Counters Example on Rust Stable
|
||||
|
||||
This example showcases a basic Leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events. Unlike the other counters example, it will compile on Rust stable, because it has the `stable` feature enabled.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
9
examples/fetch/README.md
Normal file
9
examples/fetch/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Client Side Fetch
|
||||
|
||||
This example shows how to fetch data from the client in WebAssembly.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -58,7 +58,7 @@ style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
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"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
|
||||
@@ -17,6 +17,9 @@ cargo install --locked cargo-leptos
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
@@ -25,7 +28,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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-cargo-features="csr"/>
|
||||
<link data-trunk rel="css" href="./style.css"/>
|
||||
</head>
|
||||
<body></body>
|
||||
|
||||
@@ -21,7 +21,9 @@ 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();
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> });
|
||||
@@ -44,7 +46,11 @@ cfg_if! {
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
// no client-side main function
|
||||
use hackernews::{App, AppProps};
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <App/> })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
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"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
|
||||
@@ -17,6 +17,9 @@ cargo install --locked cargo-leptos
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
@@ -25,7 +28,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
|
||||
|
||||
28
examples/hackernews_axum/src/error_template.rs
Normal file
28
examples/hackernews_axum/src/error_template.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use leptos::Errors;
|
||||
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
|
||||
|
||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||
// here than just displaying them
|
||||
pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
|
||||
let Some(errors) = errors else {
|
||||
panic!("No Errors found and we expected errors!");
|
||||
};
|
||||
view! {cx,
|
||||
<h1>"Errors"</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.get().0.into_iter()}
|
||||
// a unique key for each item as a reference
|
||||
key=|error| error.0.clone()
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
view! {
|
||||
cx,
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
.into_view(cx)
|
||||
}
|
||||
@@ -5,22 +5,26 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
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)> {
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await?;
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
match res.status() {
|
||||
StatusCode::OK => Ok(res),
|
||||
_ => Err((res.status(), "File Not Found".to_string()))
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,5 +41,7 @@ if #[cfg(feature = "ssr")] {
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/pkg").await?;
|
||||
println!("FIRST URI{:?}", uri);
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
// try with `.html`
|
||||
@@ -27,7 +26,6 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/static").await?;
|
||||
println!("FIRST URI{:?}", uri);
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
|
||||
@@ -41,7 +39,6 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// When run normally, the root should be the crate root
|
||||
println!("Base: {:#?}", base);
|
||||
if base == "/static" {
|
||||
match ServeDir::new("./static").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
|
||||
@@ -3,7 +3,8 @@ use leptos::{component, view, IntoView, Scope};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
pub mod file;
|
||||
pub mod error_template;
|
||||
pub mod fallback;
|
||||
pub mod handlers;
|
||||
mod routes;
|
||||
use routes::nav::*;
|
||||
|
||||
@@ -11,7 +11,7 @@ if #[cfg(feature = "ssr")] {
|
||||
};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
use hackernews_axum::file::file_handler;
|
||||
use hackernews_axum::fallback::file_and_error_handler;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -26,9 +26,9 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/favicon.ico", get(file_handler))
|
||||
.route("/favicon.ico", get(file_and_error_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
|
||||
.fallback(file_handler)
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
|
||||
// run our app with hyper
|
||||
|
||||
17
examples/parent_child/README.md
Normal file
17
examples/parent_child/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Parent Child Example
|
||||
|
||||
This example highlights four different ways that child components can communicate with their parent:
|
||||
|
||||
1. <ButtonA/>: passing a WriteSignal as one of the child component props,
|
||||
for the child component to write into and the parent to read
|
||||
2. <ButtonB/>: passing a closure as one of the child component props, for
|
||||
the child component to call
|
||||
3. <ButtonC/>: adding a simple event listener on the child component itself
|
||||
4. <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -1,8 +1,11 @@
|
||||
# Leptos Router Example
|
||||
|
||||
This example demonstrates how Leptos' router works
|
||||
This example demonstrates how Leptos’s router works for client side routing.
|
||||
|
||||
## Build and Run it
|
||||
|
||||
## Run it
|
||||
```bash
|
||||
trunk serve --open
|
||||
```
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
|
||||
@@ -12,6 +12,11 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<Router>
|
||||
<nav>
|
||||
// ordinary <a> elements can be used for client-side navigation
|
||||
// using <A> has two effects:
|
||||
// 1) ensuring that relative routing works properly for nested routes
|
||||
// 2) setting the `aria-current` attribute on the current link,
|
||||
// for a11y and styling purposes
|
||||
<A exact=true href="/">"Contacts"</A>
|
||||
<A href="about">"About"</A>
|
||||
<A href="settings">"Settings"</A>
|
||||
@@ -105,12 +110,15 @@ pub fn Contact(cx: Scope) -> impl IntoView {
|
||||
// Some(None) => has loaded and found no contact
|
||||
Some(None) => Some(view! { cx, <p>"No contact with this ID was found."</p> }.into_any()),
|
||||
// Some(Some) => has loaded and found a contact
|
||||
Some(Some(contact)) => Some(view! { cx,
|
||||
<section class="card">
|
||||
<h1>{contact.first_name} " " {contact.last_name}</h1>
|
||||
<p>{contact.address_1}<br/>{contact.address_2}</p>
|
||||
</section>
|
||||
}.into_any()),
|
||||
Some(Some(contact)) => Some(
|
||||
view! { cx,
|
||||
<section class="card">
|
||||
<h1>{contact.first_name} " " {contact.last_name}</h1>
|
||||
<p>{contact.address_1}<br/>{contact.address_2}</p>
|
||||
</section>
|
||||
}
|
||||
.into_any(),
|
||||
),
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
@@ -125,9 +133,18 @@ pub fn Contact(cx: Scope) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn About(cx: Scope) -> impl IntoView {
|
||||
log::debug!("rendering <About/>");
|
||||
// use_navigate allows you to navigate programmatically by calling a function
|
||||
let navigate = use_navigate(cx);
|
||||
|
||||
view! { cx,
|
||||
<>
|
||||
// note: this is just an illustration of how to use `use_navigate`
|
||||
// <button on:click> to navigate is an *anti-pattern*
|
||||
// you should ordinarily use a link instead,
|
||||
// both semantically and so your link will work before WASM loads
|
||||
<button on:click=move |_| { _ = navigate("/", Default::default()); }>
|
||||
"Home"
|
||||
</button>
|
||||
<h1>"About"</h1>
|
||||
<p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."</p>
|
||||
</>
|
||||
|
||||
@@ -91,7 +91,7 @@ style-file = "style/output.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
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"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,6 +16,8 @@ and
|
||||
|
||||
in this directory.
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
You can begin editing your app at `src/app.rs`.
|
||||
|
||||
## Installing Tailwind
|
||||
@@ -52,13 +54,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 +72,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 +91,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).
|
||||
|
||||
@@ -17,7 +17,9 @@ 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();
|
||||
|
||||
// Generate the list of routes in your Leptos App
|
||||
|
||||
@@ -60,7 +60,7 @@ style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
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"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
|
||||
@@ -17,6 +17,9 @@ cargo install --locked cargo-leptos
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
@@ -25,7 +28,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
|
||||
|
||||
@@ -26,7 +26,9 @@ cfg_if! {
|
||||
|
||||
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 addr = conf.leptos_options.site_address.clone();
|
||||
|
||||
// Generate the list of routes in your Leptos App
|
||||
|
||||
@@ -18,6 +18,7 @@ leptos = { path = "../../../leptos/leptos", default-features = false, features =
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
leptos_reactive = { path = "../../../leptos/leptos_reactive", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
@@ -38,17 +39,7 @@ sqlx = { version = "0.6.2", features = [
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"leptos_axum",
|
||||
]
|
||||
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:sqlx", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "leptos_axum"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
@@ -74,7 +65,7 @@ style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
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"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
@@ -103,4 +94,4 @@ lib-features = ["hydrate"]
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
lib-default-features = false
|
||||
|
||||
@@ -17,6 +17,9 @@ cargo install --locked cargo-leptos
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
@@ -25,7 +28,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
|
||||
|
||||
28
examples/todo_app_sqlite_axum/src/error_template.rs
Normal file
28
examples/todo_app_sqlite_axum/src/error_template.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use leptos::Errors;
|
||||
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
|
||||
|
||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||
// here than just displaying them
|
||||
pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
|
||||
let Some(errors) = errors else {
|
||||
panic!("No Errors found and we expected errors!");
|
||||
};
|
||||
view! {cx,
|
||||
<h1>"Errors"</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.get().0.into_iter()}
|
||||
// a unique key for each item as a reference
|
||||
key=|error| error.0.clone()
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
view! {
|
||||
cx,
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
.into_view(cx)
|
||||
}
|
||||
@@ -5,22 +5,26 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
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)> {
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await?;
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
match res.status() {
|
||||
StatusCode::OK => Ok(res),
|
||||
_ => Err((res.status(), "File Not Found".to_string()))
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,5 +41,7 @@ if #[cfg(feature = "ssr")] {
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod file;
|
||||
pub mod error_template;
|
||||
pub mod fallback;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
|
||||
@@ -4,15 +4,31 @@ use leptos::*;
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
routing::post,
|
||||
extract::Extension,
|
||||
routing::{post, get},
|
||||
extract::{Extension, Path},
|
||||
http::Request,
|
||||
body::StreamBody,
|
||||
response::{IntoResponse, Response},
|
||||
Router,
|
||||
};
|
||||
use axum::body::Body as AxumBody;
|
||||
use crate::todo::*;
|
||||
use todo_app_sqlite_axum::*;
|
||||
use crate::file::file_handler;
|
||||
use crate::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
use leptos_reactive::run_scope;
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, id.clone());
|
||||
},
|
||||
|cx| view! { cx, <TodoApp/> }
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -26,7 +42,8 @@ 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 addr = leptos_options.site_address.clone();
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
|
||||
@@ -34,8 +51,9 @@ if #[cfg(feature = "ssr")] {
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
|
||||
.fallback(file_handler)
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
|
||||
// run our app with hyper
|
||||
|
||||
@@ -107,6 +107,7 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let id = use_context::<String>(cx);
|
||||
provide_meta_context(cx);
|
||||
view! {
|
||||
cx,
|
||||
@@ -120,7 +121,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<Todos/>
|
||||
<Todos/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
10
examples/todomvc/README.md
Normal file
10
examples/todomvc/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos TodoMVC
|
||||
|
||||
This is a Leptos implementation of the TodoMVC example common to many frameworks. This is a relatively-simple application but shows off features like interaction between components and state management.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -262,7 +262,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
|
||||
// this will be filled by _ref=input below
|
||||
let todo_input = NodeRef::new(cx);
|
||||
let todo_input = NodeRef::<HtmlElement<Input>>::new(cx);
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
@@ -293,7 +293,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
/>
|
||||
<label on:dblclick=move |_| {
|
||||
set_editing(true);
|
||||
|
||||
|
||||
if let Some(input) = todo_input.get() {
|
||||
_ = input.focus();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "leptos_actix"
|
||||
version = {workspace = true}
|
||||
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]
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Provides functions to easily integrate Leptos with Actix.
|
||||
//!
|
||||
//! For more details on how to use the integrations, see the
|
||||
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
//! directory in the Leptos repository.
|
||||
|
||||
use actix_web::{
|
||||
body::BoxBody,
|
||||
dev::{ServiceFactory, ServiceRequest},
|
||||
@@ -68,8 +74,8 @@ 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
|
||||
/// it sets a [StatusCode] of 302 and a [LOCATION](header::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) {
|
||||
let response_options = use_context::<ResponseOptions>(cx).unwrap();
|
||||
response_options.set_status(StatusCode::FOUND).await;
|
||||
@@ -114,10 +120,39 @@ pub async fn redirect(cx: leptos::Scope, path: &str) {
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
pub fn handle_server_fns() -> Route {
|
||||
handle_server_fns_with_context(|_cx| {})
|
||||
}
|
||||
|
||||
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
|
||||
/// Leptos server function arguments in the body, runs the server function if found,
|
||||
/// and returns the resulting [HttpResponse].
|
||||
///
|
||||
/// This provides the [HttpRequest] to the server [Scope](leptos::Scope).
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// This version allows you to pass in a closure that adds additional route data to the
|
||||
/// context, allowing you to pass in info about the route or user from Actix, or other info.
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
pub fn handle_server_fns_with_context(
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
) -> Route {
|
||||
web::post().to(
|
||||
|req: HttpRequest, params: web::Path<String>, body: web::Bytes| async move {
|
||||
{
|
||||
move |req: HttpRequest, params: web::Path<String>, body: web::Bytes| {
|
||||
let additional_context = additional_context.clone();
|
||||
async move {
|
||||
let additional_context = additional_context.clone();
|
||||
|
||||
let path = params.into_inner();
|
||||
let accept_header = req
|
||||
.headers()
|
||||
@@ -129,6 +164,9 @@ pub fn handle_server_fns() -> Route {
|
||||
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) = raw_scope_and_disposer(runtime);
|
||||
|
||||
// Add additional info to the context of the server function
|
||||
additional_context(cx);
|
||||
let res_options = ResponseOptions::default();
|
||||
|
||||
// provide HttpRequest as context in server scope
|
||||
@@ -248,16 +286,47 @@ pub fn handle_server_fns() -> Route {
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
pub fn render_app_to_stream<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_to_stream_with_context(options, |_cx| {}, app_fn)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// This function allows you to provide additional information to Leptos for your route.
|
||||
/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
web::get().to(move |req: HttpRequest| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
let res_options = ResponseOptions::default();
|
||||
|
||||
async move {
|
||||
@@ -272,7 +341,7 @@ where
|
||||
|
||||
let (head, tail) = html_parts(&options);
|
||||
|
||||
stream_app(app, head, tail, res_options).await
|
||||
stream_app(app, head, tail, res_options, additional_context).await
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -321,6 +390,13 @@ where
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
pub fn render_preloaded_data_app<Data, Fut, IV>(
|
||||
options: LeptosOptions,
|
||||
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
|
||||
@@ -355,7 +431,7 @@ where
|
||||
|
||||
let (head, tail) = html_parts(&options);
|
||||
|
||||
stream_app(app, head, tail, res_options).await
|
||||
stream_app(app, head, tail, res_options, |_cx| {}).await
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -385,13 +461,18 @@ async fn stream_app(
|
||||
head: String,
|
||||
tail: String,
|
||||
res_options: ResponseOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
) -> 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 (stream, runtime, _) = render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| {
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body>").into()
|
||||
},
|
||||
additional_context,
|
||||
);
|
||||
|
||||
let mut stream = Box::pin(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "leptos_axum"
|
||||
version = {workspace = true}
|
||||
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]
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Provides functions to easily integrate Leptos with Axum.
|
||||
//!
|
||||
//! For more details on how to use the integrations, see the
|
||||
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
//! directory in the Leptos repository.
|
||||
|
||||
use axum::{
|
||||
body::{Body, Bytes, Full, StreamBody},
|
||||
extract::Path,
|
||||
@@ -91,6 +97,8 @@ pub async fn redirect(cx: leptos::Scope, path: &str) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Decomposes an HTTP request into its parts, allowing you to read its headers
|
||||
/// and other data without consuming the body.
|
||||
pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
// provide request headers as context in server scope
|
||||
let (parts, body) = req.into_parts();
|
||||
@@ -107,8 +115,6 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
/// run the server function if found, and return the resulting [Response].
|
||||
///
|
||||
/// This provides an `Arc<[Request<Body>](axum::http::Request)>` [Scope](leptos::Scope).
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// ```
|
||||
@@ -136,16 +142,53 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
/// ```
|
||||
/// Leptos provides a generic implementation of `handle_server_fns`. If access to more specific parts of the Request is desired,
|
||||
/// you can specify your own server fn handler based on this one and give it it's own route in the server macro.
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
pub async fn handle_server_fns(
|
||||
Path(fn_name): Path<String>,
|
||||
headers: HeaderMap,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
handle_server_fns_inner(fn_name, headers, |_| {}, req).await
|
||||
}
|
||||
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
/// run the server function if found, and return the resulting [Response].
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// This version allows you to pass in a closure to capture additional data from the layers above leptos
|
||||
/// and store it in context. To use it, you'll need to define your own route, and a handler function
|
||||
/// that takes in the data you'd like. See the [render_app_to_stream_with_context] docs for an example
|
||||
/// of one that should work much like this one.
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
pub async fn handle_server_fns_with_context(
|
||||
Path(fn_name): Path<String>,
|
||||
headers: HeaderMap,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
handle_server_fns_inner(fn_name, headers, additional_context, req).await
|
||||
}
|
||||
|
||||
async fn handle_server_fns_inner(
|
||||
fn_name: String,
|
||||
headers: HeaderMap,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
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('/') {
|
||||
Some(path) => path.to_string(),
|
||||
None => fn_name,
|
||||
};
|
||||
let fn_name = fn_name
|
||||
.strip_prefix('/')
|
||||
.map(|fn_name| fn_name.to_string())
|
||||
.unwrap_or(fn_name);
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
spawn_blocking({
|
||||
@@ -158,6 +201,8 @@ pub async fn handle_server_fns(
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) = raw_scope_and_disposer(runtime);
|
||||
|
||||
additional_context(cx);
|
||||
|
||||
let req_parts = generate_request_parts(req).await;
|
||||
// Add this so we can get details about the Request
|
||||
provide_context(cx, req_parts.clone());
|
||||
@@ -166,7 +211,7 @@ pub async fn handle_server_fns(
|
||||
|
||||
match server_fn(cx, &req_parts.body).await {
|
||||
Ok(serialized) => {
|
||||
// If ResponseParts are set, add the headers and extension to the request
|
||||
// If ResponseOptions are set, add the headers and status to the request
|
||||
let res_options = use_context::<ResponseOptions>(cx);
|
||||
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
@@ -299,6 +344,12 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
pub fn render_app_to_stream<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
@@ -310,18 +361,62 @@ pub fn render_app_to_stream<IV>(
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_to_stream_with_context(options, |_| {}, app_fn)
|
||||
}
|
||||
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```ignore
|
||||
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
|
||||
/// let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
|
||||
/// move |cx| {
|
||||
/// provide_context(cx, id.clone());
|
||||
/// },
|
||||
/// |cx| view! { cx, <TodoApp/> }
|
||||
/// );
|
||||
/// handler(req).await.into_response()
|
||||
/// }
|
||||
/// ```
|
||||
/// Otherwise, this function is identical to [render_app_to_stream].
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
move |req: Request<Body>| {
|
||||
Box::pin({
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = additional_context.clone();
|
||||
let default_res_options = ResponseOptions::default();
|
||||
let res_options2 = default_res_options.clone();
|
||||
let res_options3 = default_res_options.clone();
|
||||
|
||||
async move {
|
||||
// Need to get the path and query string of the Request
|
||||
let path = req.uri();
|
||||
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
|
||||
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
|
||||
let path = req.uri().path_and_query().unwrap().as_str();
|
||||
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
|
||||
@@ -384,11 +479,13 @@ where
|
||||
|
||||
spawn_blocking({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
async move {
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
@@ -411,7 +508,7 @@ where
|
||||
};
|
||||
|
||||
let (bundle, runtime, scope) =
|
||||
render_to_stream_with_prefix_undisposed(
|
||||
render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| {
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
@@ -419,6 +516,7 @@ where
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body>").into()
|
||||
},
|
||||
add_context,
|
||||
);
|
||||
let mut shell = Box::pin(bundle);
|
||||
while let Some(fragment) = shell.next().await {
|
||||
@@ -514,13 +612,8 @@ where
|
||||
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()
|
||||
})
|
||||
.into_iter()
|
||||
.map(|s| if s.is_empty() { "/".to_string() } else { s })
|
||||
.collect();
|
||||
|
||||
if routes.is_empty() {
|
||||
|
||||
@@ -17,36 +17,17 @@ leptos_server = { workspace = true }
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.11"
|
||||
once_cell = "1.17.0"
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = ".", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["csr", "serde"]
|
||||
csr = [
|
||||
"leptos_dom/web",
|
||||
"leptos_macro/csr",
|
||||
"leptos_reactive/csr",
|
||||
"leptos_server/csr",
|
||||
]
|
||||
hydrate = [
|
||||
"leptos_dom/web",
|
||||
"leptos_macro/hydrate",
|
||||
"leptos_reactive/hydrate",
|
||||
"leptos_server/hydrate",
|
||||
]
|
||||
ssr = [
|
||||
"leptos_dom/ssr",
|
||||
"leptos_macro/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
]
|
||||
stable = [
|
||||
"leptos_dom/stable",
|
||||
"leptos_macro/stable",
|
||||
"leptos_reactive/stable",
|
||||
"leptos_server/stable",
|
||||
]
|
||||
csr = ["leptos_dom/web", "leptos_macro/csr", "leptos_reactive/csr", "leptos_server/csr"]
|
||||
hydrate = ["leptos_dom/web", "leptos_macro/hydrate", "leptos_reactive/hydrate", "leptos_server/hydrate"]
|
||||
ssr = ["leptos_dom/ssr", "leptos_macro/ssr", "leptos_reactive/ssr", "leptos_server/ssr"]
|
||||
stable = ["leptos_dom/stable", "leptos_macro/stable", "leptos_reactive/stable", "leptos_server/stable"]
|
||||
serde = ["leptos_reactive/serde"]
|
||||
serde-lite = ["leptos_reactive/serde-lite"]
|
||||
miniserde = ["leptos_reactive/miniserde"]
|
||||
|
||||
28
leptos/src/error_boundary.rs
Normal file
28
leptos/src/error_boundary.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use leptos_dom::{Errors, Fragment, IntoView};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
|
||||
|
||||
#[component(transparent)]
|
||||
pub fn ErrorBoundary<F, IV>(
|
||||
cx: Scope,
|
||||
/// The components inside the tag which will get rendered
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
/// A fallback that will be shown if an error occurs.
|
||||
fallback: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope, Option<RwSignal<Errors>>) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
|
||||
|
||||
provide_context(cx, errors);
|
||||
|
||||
// Run children so that they render and execute resources
|
||||
let children = children(cx);
|
||||
|
||||
move || match errors.get().0.is_empty() {
|
||||
true => children.clone().into_view(cx),
|
||||
false => fallback(cx, Some(errors)).into_view(cx),
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,8 @@
|
||||
//! or mutating data via async requests to the server
|
||||
//! - multi-page apps (MPAs) rendered on the server, managing navigation, data, and mutations via
|
||||
//! web-standard `<a>` and `<form>` tags
|
||||
//! - progressively-enhanced multi-page apps ([PEMPAs](https://www.epicweb.dev/the-webs-next-transition)?)
|
||||
//! that are rendered on the server and then hydrated on the client, enhancing your `<a>` and `<form>`
|
||||
//! navigations and mutations seamlessly when WASM is available.
|
||||
//! - progressively-enhanced single-page apps that are rendered on the server and then hydrated on the client,
|
||||
//! enhancing your `<a>` and `<form>` navigations and mutations seamlessly when WASM is available.
|
||||
//!
|
||||
//! And you can do all three of these **using the same Leptos code.**
|
||||
//!
|
||||
@@ -19,38 +18,51 @@
|
||||
//! 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 you’re using `stable`,
|
||||
//! you’ll 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
|
||||
//! If you want to see what Leptos is capable of, check out
|
||||
//! 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
|
||||
//! - [`counter_without_macros`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_without_macros)
|
||||
//! adapts the counter example to use the builder pattern for the UI and avoids other macros, instead showing
|
||||
//! the code that Leptos generates.
|
||||
//! - [`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
|
||||
//! - [`counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable) adapts the `counters` example
|
||||
//! to show how to use Leptos with `stable` Rust.
|
||||
//! - [`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 Leptos’s nested router
|
||||
//! - [`router`](https://github.com/leptos-rs/leptos/tree/main/examples/router) shows how to use Leptos’s 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
|
||||
//! - [`counter_isomorphic`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic) shows
|
||||
//! different methods of interaction with a stateful server, including server functions, server actions, forms,
|
||||
//! and server-sent events (SSE).
|
||||
//! - [`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.
|
||||
//! 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.
|
||||
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
|
||||
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
|
||||
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
|
||||
//! a fully-functional that works as intended even before WASM has loaded and begun to run.
|
||||
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite) and
|
||||
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum)
|
||||
//! show how to build a full-stack app using server functions and database connections.
|
||||
//! - [`tailwind`](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind) shows how to integrate
|
||||
//! TailwindCSS with `cargo-leptos`.
|
||||
//!
|
||||
//! (The SPA examples can be run using `trunk serve`. For information about Trunk,
|
||||
//! [see here]((https://trunkrs.dev/)).)
|
||||
//! Details on how to run each example can be found in its README.
|
||||
//!
|
||||
//! # Quick Links
|
||||
//!
|
||||
@@ -138,9 +150,12 @@ pub use leptos_server::*;
|
||||
|
||||
pub use tracing;
|
||||
pub use typed_builder;
|
||||
|
||||
mod error_boundary;
|
||||
pub use error_boundary::*;
|
||||
mod for_loop;
|
||||
mod show;
|
||||
pub use for_loop::*;
|
||||
pub use show::*;
|
||||
mod suspense;
|
||||
pub use suspense::*;
|
||||
mod transition;
|
||||
|
||||
32
leptos/src/show.rs
Normal file
32
leptos/src/show.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use leptos::component;
|
||||
use leptos_dom::{Fragment, IntoView};
|
||||
use leptos_reactive::Scope;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
/// A component that will show it's children when the passed in closure is True, and show the fallback
|
||||
/// when the closure is false
|
||||
#[component]
|
||||
pub fn Show<F, W, IV>(
|
||||
/// The scope the component is running in
|
||||
cx: Scope,
|
||||
/// The components Show wraps
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
/// A closure that returns a bool that determines whether this thing runs
|
||||
when: W,
|
||||
/// A closure that returns what gets rendered if the when statement is false
|
||||
fallback: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
W: Fn() -> bool + 'static,
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
// now you don't render until `when` is actually true
|
||||
let children = Lazy::new(move || children(cx).into_view(cx));
|
||||
let fallback = Lazy::new(move || fallback(cx).into_view(cx));
|
||||
|
||||
move || match when() {
|
||||
true => children.clone(),
|
||||
false => fallback.clone(),
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_dom::{Component, DynChild, Fragment, IntoView};
|
||||
#[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;
|
||||
@@ -73,11 +72,12 @@ where
|
||||
|
||||
let orig_child = Rc::new(children);
|
||||
|
||||
Component::new("Suspense", move |cx| {
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
let current_id = HydrationCtx::peek();
|
||||
let current_id = HydrationCtx::peek();
|
||||
|
||||
DynChild::new(move || {
|
||||
let child = DynChild::new({
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
let current_id = current_id.clone();
|
||||
move || {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
if context.ready() {
|
||||
@@ -104,13 +104,9 @@ where
|
||||
¤t_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()
|
||||
@@ -128,6 +124,13 @@ where
|
||||
initial
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.into_view(cx);
|
||||
let core_component = match child {
|
||||
leptos_dom::View::CoreComponent(repr) => repr,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
leptos_dom::View::Suspense(current_id, core_component)
|
||||
}
|
||||
|
||||
@@ -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,41 @@ 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 id=\"_0-1\" class=\"counters\"><!--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>"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[test]
|
||||
fn ssr_test_with_snake_case_components() {
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
fn snake_case_counter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value.get().to_string()} "!"</span>
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<div class="counters">
|
||||
<SnakeCaseCounter initial_value=1/>
|
||||
<SnakeCaseCounter initial_value=2/>
|
||||
</div>
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<div id=\"_0-1\" class=\"counters\"><!--hk=_0-1-0o|leptos-snake-case-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-snake-case-counter-end--><!--hk=_0-1-5-0o|leptos-snake-case-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-snake-case-counter-end--></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -69,7 +103,29 @@ fn test_classes() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<div class=\"my big red car\" id=\"_0-1\"></div>"
|
||||
"<div id=\"_0-1\" class=\"my big red car\"></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[test]
|
||||
fn ssr_with_styles() {
|
||||
use leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let styles = "myclass";
|
||||
let rendered = view! {
|
||||
cx, class = styles,
|
||||
<div>
|
||||
<button class="btn" on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
</div>
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" class=\"btn myclass\">-1</button></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Configuraiton for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
config = "0.13.3"
|
||||
@@ -13,4 +14,4 @@ fs = "0.0.5"
|
||||
regex = "1.7.0"
|
||||
serde = { version = "1.0.151", features = ["derive"] }
|
||||
thiserror = "1.0.38"
|
||||
typed-builder = "0.11.0"
|
||||
typed-builder = "0.11"
|
||||
|
||||
@@ -72,8 +72,8 @@ fn env_w_default(key: &str, default: &str) -> Result<String, LeptosConfigError>
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// An enum that can be used to define the environment Leptos is running in.
|
||||
/// Setting this to the `PROD` variant will not include the WebSocket code for `cargo-leptos` watch mode.
|
||||
/// Defaults to `DEV`.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub enum Env {
|
||||
@@ -152,7 +152,10 @@ impl TryFrom<String> for Env {
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
if let Some(path) = path {
|
||||
let text = fs::read_to_string(path).map_err(|_| LeptosConfigError::ConfigNotFound)?;
|
||||
|
||||
@@ -4,7 +4,7 @@ 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]
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod dyn_child;
|
||||
mod each;
|
||||
mod errors;
|
||||
mod fragment;
|
||||
mod unit;
|
||||
|
||||
@@ -11,6 +12,7 @@ use crate::{
|
||||
use crate::{mount_child, prepare_to_move, MountKind, Mountable};
|
||||
pub use dyn_child::*;
|
||||
pub use each::*;
|
||||
pub use errors::*;
|
||||
pub use fragment::*;
|
||||
use leptos_reactive::Scope;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -26,12 +28,12 @@ use wasm_bindgen::JsCast;
|
||||
#[derive(educe::Educe)]
|
||||
#[educe(Default, Clone, PartialEq, Eq)]
|
||||
pub enum CoreComponent {
|
||||
/// The [`Unit`] component.
|
||||
/// The [Unit] component.
|
||||
#[educe(Default)]
|
||||
Unit(UnitRepr),
|
||||
/// The [`DynChild`] component.
|
||||
/// The [DynChild] component.
|
||||
DynChild(DynChildRepr),
|
||||
/// The [`EachKey`] component.
|
||||
/// The [Each] component.
|
||||
Each(EachRepr),
|
||||
}
|
||||
|
||||
|
||||
68
leptos_dom/src/components/errors.rs
Normal file
68
leptos_dom/src/components/errors.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use crate::{HydrationCtx, HydrationKey, IntoView};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::{use_context, RwSignal};
|
||||
use std::{collections::HashMap, error::Error, sync::Arc};
|
||||
|
||||
/// A struct to hold all the possible errors that could be provided by child Views
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Errors(pub HashMap<HydrationKey, Arc<dyn Error>>);
|
||||
|
||||
impl<T, E> IntoView for Result<T, E>
|
||||
where
|
||||
T: IntoView + 'static,
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||
match self {
|
||||
Ok(stuff) => stuff.into_view(cx),
|
||||
Err(error) => {
|
||||
match use_context::<RwSignal<Errors>>(cx) {
|
||||
Some(errors) => {
|
||||
let id = HydrationCtx::id();
|
||||
errors.update(move |errors: &mut Errors| errors.insert(id, error));
|
||||
|
||||
// remove the error from the list if this drops,
|
||||
// i.e., if it's in a DynChild that switches from Err to Ok
|
||||
// Only can run on the client, will panic on the server
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "hydrate", feature="csr"))] {
|
||||
use leptos_reactive::{on_cleanup, queue_microtask};
|
||||
on_cleanup(cx, move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
errors.remove::<E>(&id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
#[cfg(debug_assertions)]
|
||||
warn!(
|
||||
"No ErrorBoundary components found! Returning errors will not \
|
||||
be handled and will silently disappear"
|
||||
);
|
||||
}
|
||||
}
|
||||
().into_view(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Errors {
|
||||
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn insert<E>(&mut self, key: HydrationKey, error: E)
|
||||
where
|
||||
E: Error + 'static,
|
||||
{
|
||||
self.0.insert(key, Arc::new(error));
|
||||
}
|
||||
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn remove<E>(&mut self, key: &HydrationKey)
|
||||
where
|
||||
E: Error + 'static,
|
||||
{
|
||||
self.0.remove(key);
|
||||
}
|
||||
}
|
||||
@@ -165,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;
|
||||
@@ -922,14 +982,19 @@ 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 el.name == "a"
|
||||
|| el.name == "script"
|
||||
|| el.name == "style"
|
||||
|| el.name == "title"
|
||||
{
|
||||
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."
|
||||
"Warning: you are appending an SVG element to an HTML element, or \
|
||||
an HTML element to an SVG. Typically, this occurs when you create \
|
||||
an <a/> or <script/> 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 element, use <svg::{{tag name}}/> in the \
|
||||
view macro."
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -978,6 +1043,8 @@ generate_html_tags![
|
||||
footer HtmlElement,
|
||||
/// The `<header>` HTML element represents introductory content, typically a group of introductory or navigational aids. It may contain some heading elements but also a logo, a search form, an author name, and other elements.
|
||||
header HtmlElement,
|
||||
/// The `<hgroup>` HTML element represents a heading and related content. It groups a single `<h1>–<h6>` element with one or more `<p>`.
|
||||
hgroup HtmlElement,
|
||||
/// The `<h1>` to `<h6>` HTML elements represent six levels of section headings. `<h1>` is the highest section level and `<h6>` is the lowest.
|
||||
h1 HtmlHeadingElement,
|
||||
/// The `<h1>` to `<h6>` HTML elements represent six levels of section headings. `<h1>` is the highest section level and `<h6>` is the lowest.
|
||||
|
||||
@@ -1,25 +1,55 @@
|
||||
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-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
|
||||
#[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.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct HydrationKey {
|
||||
/// The key of the previous component.
|
||||
pub previous: String,
|
||||
@@ -74,8 +104,9 @@ impl HydrationCtx {
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) fn reset_id() {
|
||||
pub fn reset_id() {
|
||||
ID.with(|id| *id.borrow_mut() = Default::default());
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ pub use hydration::{HydrationCtx, HydrationKey};
|
||||
pub use js_sys;
|
||||
use leptos_reactive::Scope;
|
||||
pub use logging::*;
|
||||
pub use macro_helpers::{IntoAttribute, IntoClass, IntoProperty};
|
||||
pub use macro_helpers::*;
|
||||
pub use node_ref::*;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
@@ -303,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();
|
||||
@@ -377,6 +377,8 @@ pub enum View {
|
||||
/// Wraps arbitrary data that's not part of the view but is
|
||||
/// passed via the view tree.
|
||||
Transparent(Transparent),
|
||||
/// Marks the contents of Suspense component, which can be replaced in streaming SSR.
|
||||
Suspense(HydrationKey, CoreComponent),
|
||||
}
|
||||
|
||||
impl fmt::Debug for View {
|
||||
@@ -389,6 +391,9 @@ impl fmt::Debug for View {
|
||||
Self::Transparent(arg0) => {
|
||||
f.debug_tuple("Transparent").field(arg0).finish()
|
||||
}
|
||||
Self::Suspense(id, c) => {
|
||||
f.debug_tuple("Suspense").field(id).field(c).finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,7 +436,7 @@ impl Mountable for View {
|
||||
element.element.unchecked_ref::<web_sys::Node>().clone()
|
||||
}
|
||||
Self::Text(t) => t.node.clone(),
|
||||
Self::CoreComponent(c) => match c {
|
||||
Self::CoreComponent(c) | Self::Suspense(_, c) => match c {
|
||||
CoreComponent::Unit(u) => u.get_mountable_node(),
|
||||
CoreComponent::DynChild(dc) => dc.get_mountable_node(),
|
||||
CoreComponent::Each(e) => e.get_mountable_node(),
|
||||
@@ -445,7 +450,7 @@ impl Mountable for View {
|
||||
match self {
|
||||
Self::Text(t) => t.node.clone(),
|
||||
Self::Element(el) => el.element.clone().unchecked_into(),
|
||||
Self::CoreComponent(c) => match c {
|
||||
Self::CoreComponent(c) | Self::Suspense(_, c) => match c {
|
||||
CoreComponent::DynChild(dc) => dc.get_opening_node(),
|
||||
CoreComponent::Each(e) => e.get_opening_node(),
|
||||
CoreComponent::Unit(u) => u.get_opening_node(),
|
||||
@@ -461,7 +466,7 @@ impl Mountable for View {
|
||||
match self {
|
||||
Self::Text(t) => t.node.clone(),
|
||||
Self::Element(el) => el.element.clone().unchecked_into(),
|
||||
Self::CoreComponent(c) => match c {
|
||||
Self::CoreComponent(c) | Self::Suspense(_, c) => match c {
|
||||
CoreComponent::DynChild(dc) => dc.get_closing_node(),
|
||||
CoreComponent::Each(e) => e.get_closing_node(),
|
||||
CoreComponent::Unit(u) => u.get_closing_node(),
|
||||
@@ -487,6 +492,7 @@ impl View {
|
||||
CoreComponent::Unit(..) => "Unit",
|
||||
},
|
||||
Self::Transparent(..) => "Transparent",
|
||||
Self::Suspense(..) => "Suspense",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +554,7 @@ impl View {
|
||||
pub fn on<E: ev::EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
mut 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)] {
|
||||
|
||||
@@ -169,7 +169,7 @@ attr_type!(f64);
|
||||
attr_type!(char);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn attribute_expression(
|
||||
pub(crate) fn attribute_expression(
|
||||
el: &web_sys::Element,
|
||||
attr_name: &str,
|
||||
value: Attribute,
|
||||
|
||||
@@ -67,7 +67,7 @@ impl<T: IntoClass> IntoClass for (Scope, T) {
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn class_expression(
|
||||
pub(crate) fn class_expression(
|
||||
class_list: &web_sys::DomTokenList,
|
||||
class_name: &str,
|
||||
value: bool,
|
||||
|
||||
@@ -77,7 +77,7 @@ prop_type!(f64);
|
||||
prop_type!(bool);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn property_expression(
|
||||
pub(crate) fn property_expression(
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
value: JsValue,
|
||||
|
||||
@@ -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.
|
||||
@@ -84,7 +78,7 @@ pub fn render_to_stream_with_prefix(
|
||||
stream
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [Scope] and [Runtime] that were created, so
|
||||
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
|
||||
/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with
|
||||
/// the same scope. This can be used to generate additional HTML that has access to the same `Scope`.
|
||||
///
|
||||
@@ -102,6 +96,29 @@ pub fn render_to_stream_with_prefix(
|
||||
pub fn render_to_stream_with_prefix_undisposed(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
|
||||
render_to_stream_with_prefix_undisposed_with_context(view, prefix, |_cx| {})
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
|
||||
/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with
|
||||
/// the same scope. This can be used to generate additional HTML that has access to the same `Scope`.
|
||||
///
|
||||
/// This renders:
|
||||
/// 1) the prefix
|
||||
/// 2) the application shell
|
||||
/// a) HTML for everything that is not under a `<Suspense/>`,
|
||||
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
|
||||
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
|
||||
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
|
||||
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
|
||||
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
|
||||
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
additional_context: impl FnOnce(Scope) + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
|
||||
HydrationCtx::reset_id();
|
||||
|
||||
@@ -114,6 +131,8 @@ pub fn render_to_stream_with_prefix_undisposed(
|
||||
_,
|
||||
) = run_scope_undisposed(runtime, {
|
||||
move |cx| {
|
||||
// Add additional context items
|
||||
additional_context(cx);
|
||||
// the actual app body/template code
|
||||
// this does NOT contain any of the data being loaded asynchronously in resources
|
||||
let shell = view(cx).render_to_string(cx);
|
||||
@@ -122,16 +141,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 +158,32 @@ 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 id = "{fragment_id}";
|
||||
var open;
|
||||
var close;
|
||||
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
||||
while(walker.nextNode()) {{
|
||||
if(walker.currentNode.textContent == `suspense-open-${{id}}`) {{
|
||||
open = walker.currentNode;
|
||||
}} else if(walker.currentNode.textContent == `suspense-close-${{id}}`) {{
|
||||
close = walker.currentNode;
|
||||
}}
|
||||
}}
|
||||
var range = new Range();
|
||||
range.setStartAfter(open);
|
||||
range.setEndBefore(close);
|
||||
range.deleteContents();
|
||||
var tpl = document.getElementById("{fragment_id}f");
|
||||
close.parentNode.insertBefore(tpl.content.cloneNode(true), close);
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
});
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = serializers.map(|(id, json)| {
|
||||
@@ -244,7 +240,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,13 +248,18 @@ impl View {
|
||||
).into()
|
||||
} else {
|
||||
format!(
|
||||
r#"{}<l-m id="{}"></l-m>"#,
|
||||
r#"{}<!--hk={}-->"#,
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true)
|
||||
).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Suspense(id, node) => format!(
|
||||
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
|
||||
View::CoreComponent(node).render_to_string_helper()
|
||||
)
|
||||
.into(),
|
||||
View::CoreComponent(node) => {
|
||||
let (id, name, wrap, content) = match node {
|
||||
CoreComponent::Unit(u) => (
|
||||
@@ -269,14 +270,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 +313,6 @@ impl View {
|
||||
}
|
||||
CoreComponent::Each(node) => {
|
||||
let children = node.children.take();
|
||||
|
||||
(
|
||||
node.id,
|
||||
"each",
|
||||
@@ -329,10 +329,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 +339,7 @@ impl View {
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
format!(
|
||||
"{}<l-m id=\"{}\"></l-m>",
|
||||
"{}<!--hk={}-->",
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
)
|
||||
@@ -357,7 +355,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 +364,7 @@ impl View {
|
||||
let _ = name;
|
||||
|
||||
format!(
|
||||
r#"{}<l-m id="{}"></l-m>"#,
|
||||
r#"{}<!--hk={}-->"#,
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
).into()
|
||||
@@ -425,6 +423,7 @@ impl View {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn to_kebab_case(name: &str) -> String {
|
||||
if name.is_empty() {
|
||||
return String::new();
|
||||
@@ -459,3 +458,11 @@ fn to_kebab_case(name: &str) -> String {
|
||||
|
||||
new_name
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn escape_attr<T>(value: &T) -> Cow<'_, str>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
html_escape::encode_double_quoted_attribute(value)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ 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."
|
||||
readme = "../README.md"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -26,10 +27,11 @@ leptos_dom = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
lazy_static = "1.4"
|
||||
convert_case = "0.6.0"
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4"
|
||||
typed-builder = "0.10"
|
||||
typed-builder = "0.11"
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use convert_case::{
|
||||
Case::{Pascal, Snake},
|
||||
Casing,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{format_ident, ToTokens, TokenStreamExt};
|
||||
@@ -75,7 +79,7 @@ impl Parse for Model {
|
||||
is_transparent: false,
|
||||
docs,
|
||||
vis: item.vis.clone(),
|
||||
name: item.sig.ident.clone(),
|
||||
name: convert_from_snake_case(&item.sig.ident),
|
||||
scope_name,
|
||||
props,
|
||||
ret: item.sig.output.clone(),
|
||||
@@ -97,6 +101,15 @@ fn drain_filter<T>(vec: &mut Vec<T>, mut some_predicate: impl FnMut(&mut T) -> b
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_from_snake_case(name: &Ident) -> Ident {
|
||||
let name_str = name.to_string();
|
||||
if !name_str.is_case(Snake) {
|
||||
name.clone()
|
||||
} else {
|
||||
Ident::new(&name_str.to_case(Pascal), name.span())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Model {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self {
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
#[macro_use]
|
||||
extern crate proc_macro_error;
|
||||
|
||||
use proc_macro::{TokenStream, TokenTree};
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenTree;
|
||||
use quote::ToTokens;
|
||||
use server::server_macro_impl;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
@@ -196,8 +197,8 @@ mod server;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 8. You can use the `_ref` attribute to store a reference to its DOM element in a
|
||||
/// [NodeRef](leptos_reactive::NodeRef) to use later.
|
||||
/// 8. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
|
||||
/// [NodeRef](leptos_dom::NodeRef) to use later.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
@@ -211,6 +212,24 @@ mod server;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 9. You can add the same class to every element in the view by passing in a special
|
||||
/// `class = {/* ... */}` argument after `cx, `. This is useful for injecting a class
|
||||
/// providing by a scoped styling library.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let class = "mycustomclass";
|
||||
/// view! { cx, class = class,
|
||||
/// <div> // will have class="mycustomclass"
|
||||
/// <p>"Some text"</p> // will also have class "mycustomclass"
|
||||
/// </div>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// Here’s a simple example that shows off several of these features, put together
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
@@ -243,16 +262,45 @@ mod server;
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
let tokens: proc_macro2::TokenStream = tokens.into();
|
||||
let mut tokens = tokens.into_iter();
|
||||
let (cx, comma) = (tokens.next(), tokens.next());
|
||||
match (cx, comma) {
|
||||
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct))) if punct.as_char() == ',' => {
|
||||
match parse(tokens.collect()) {
|
||||
let first = tokens.next();
|
||||
let second = tokens.next();
|
||||
let third = tokens.next();
|
||||
let fourth = tokens.next();
|
||||
let global_class = match (&first, &second, &third, &fourth) {
|
||||
(
|
||||
Some(TokenTree::Ident(first)),
|
||||
Some(TokenTree::Punct(eq)),
|
||||
Some(val),
|
||||
Some(TokenTree::Punct(comma)),
|
||||
) if *first == "class"
|
||||
&& eq.to_string() == '='.to_string()
|
||||
&& comma.to_string() == ','.to_string() =>
|
||||
{
|
||||
Some(val.clone())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let tokens = if global_class.is_some() {
|
||||
tokens.collect::<proc_macro2::TokenStream>()
|
||||
} else {
|
||||
[first, second, third, fourth]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(tokens)
|
||||
.collect()
|
||||
};
|
||||
|
||||
match parse(tokens.into()) {
|
||||
Ok(nodes) => render_view(
|
||||
&proc_macro2::Ident::new(&cx.to_string(), cx.span().into()),
|
||||
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
|
||||
&nodes,
|
||||
// swap to Mode::default() to use faster SSR templating
|
||||
Mode::Client, //Mode::default(),
|
||||
Mode::default(),
|
||||
global_class.as_ref(),
|
||||
),
|
||||
Err(error) => error.to_compile_error(),
|
||||
}
|
||||
@@ -267,10 +315,10 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// Annotates a function so that it can be used with your template as a Leptos `<Component/>`.
|
||||
///
|
||||
/// The `#[component]` macro allows you to annotate plain Rust functions as components
|
||||
/// and use them within your Leptos [view](mod@view) as if they were custom HTML elements. The
|
||||
/// and use them within your Leptos [view](crate::view!) as if they were custom HTML elements. The
|
||||
/// component function takes a [Scope](leptos_reactive::Scope) and any number of other arguments.
|
||||
/// When you use the component somewhere else, the names of its arguments are the names
|
||||
/// of the properties you use in the [view](mod@view) macro.
|
||||
/// of the properties you use in the [view](crate::view!) macro.
|
||||
///
|
||||
/// Every component function should have the return type `-> impl IntoView`.
|
||||
///
|
||||
@@ -325,18 +373,21 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// to do relatively expensive work within the component function, as it will only happen once,
|
||||
/// not on every state change.
|
||||
///
|
||||
/// 2. The component name should be `CamelCase` instead of `snake_case`. This is how the renderer
|
||||
/// recognizes that a particular tag is a component, not an HTML element.
|
||||
/// 2. Component names are usually in `PascalCase`. If you use a `snake_case` name,
|
||||
/// then the generated component's name will still be in `PascalCase`. This is how the framework
|
||||
/// recognizes that a particular tag is a component, not an HTML element. It's important to be aware
|
||||
/// of this when using or importing the component.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// // ❌ not snake_case
|
||||
/// #[component]
|
||||
/// fn my_component(cx: Scope) -> impl IntoView { todo!() }
|
||||
///
|
||||
/// // ✅ CamelCase
|
||||
/// // PascalCase: Generated component will be called MyComponent
|
||||
/// #[component]
|
||||
/// fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
|
||||
///
|
||||
/// // snake_case: Generated component will be called MySnakeCaseComponent
|
||||
/// #[component]
|
||||
/// fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
|
||||
/// ```
|
||||
///
|
||||
/// 3. The macro generates a type `ComponentProps` for every `Component` (so, `HomePage` generates `HomePageProps`,
|
||||
@@ -355,6 +406,18 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// pub fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
|
||||
/// }
|
||||
/// ```
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
///
|
||||
/// use snake_case_component::{MySnakeCaseComponent, MySnakeCaseComponentProps};
|
||||
///
|
||||
/// mod snake_case_component {
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 4. You can pass generic arguments, but they should be defined in a `where` clause and not inline.
|
||||
///
|
||||
@@ -491,11 +554,11 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// (e.g., `"/api"`). Defaults to `"/"`.
|
||||
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
|
||||
/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string).
|
||||
/// Defaults to `"Url"`. If you want to use this server function to power an
|
||||
/// [ActionForm](leptos_router::ActionForm) the encoding must be `"Url"`.
|
||||
/// Defaults to `"Url"`. If you want to use this server function to power a `<form>` that will
|
||||
/// work without WebAssembly, the encoding must be `"Url"`.
|
||||
///
|
||||
/// The server function itself can take any number of arguments, each of which should be serializable
|
||||
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos::Scope),
|
||||
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos_reactive::Scope),
|
||||
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
|
||||
/// server-side context into the server function.
|
||||
///
|
||||
|
||||
@@ -43,13 +43,13 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
let block = body.block;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(not(feature = "stable"))] {
|
||||
if #[cfg(all(not(feature = "stable"), debug_assertions))] {
|
||||
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('/', "-");
|
||||
#[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;
|
||||
}
|
||||
@@ -71,6 +71,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
let cx_assign_statement = if let Some(FnArg::Typed(arg)) = cx_arg {
|
||||
if let Pat::Ident(id) = &*arg.pat {
|
||||
quote! {
|
||||
#[allow(unused)]
|
||||
let #id = cx;
|
||||
}
|
||||
} else {
|
||||
@@ -90,7 +91,15 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
quote! { #typed_arg }
|
||||
let is_cx = fn_arg_is_cx(f);
|
||||
if is_cx {
|
||||
quote! {
|
||||
#[allow(unused)]
|
||||
#typed_arg
|
||||
}
|
||||
} else {
|
||||
quote! { #typed_arg }
|
||||
}
|
||||
});
|
||||
let fn_args_2 = fn_args.clone();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
|
||||
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName};
|
||||
@@ -142,7 +142,12 @@ const TYPED_EVENTS: [&str; 126] = [
|
||||
"visibilitychange",
|
||||
];
|
||||
|
||||
pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream {
|
||||
pub(crate) fn render_view(
|
||||
cx: &Ident,
|
||||
nodes: &[Node],
|
||||
mode: Mode,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
if mode == Mode::Ssr {
|
||||
if nodes.is_empty() {
|
||||
let span = Span::call_site();
|
||||
@@ -150,9 +155,9 @@ pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream
|
||||
span => leptos::Unit
|
||||
}
|
||||
} else if nodes.len() == 1 {
|
||||
root_node_to_tokens_ssr(cx, &nodes[0])
|
||||
root_node_to_tokens_ssr(cx, &nodes[0], global_class)
|
||||
} else {
|
||||
fragment_to_tokens_ssr(cx, Span::call_site(), nodes)
|
||||
fragment_to_tokens_ssr(cx, Span::call_site(), nodes, global_class)
|
||||
}
|
||||
} else if nodes.is_empty() {
|
||||
let span = Span::call_site();
|
||||
@@ -160,16 +165,27 @@ pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream
|
||||
span => leptos::Unit
|
||||
}
|
||||
} else if nodes.len() == 1 {
|
||||
node_to_tokens(cx, &nodes[0], TagType::Unknown)
|
||||
node_to_tokens(cx, &nodes[0], TagType::Unknown, global_class)
|
||||
} else {
|
||||
fragment_to_tokens(cx, Span::call_site(), nodes, false, TagType::Unknown)
|
||||
fragment_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
nodes,
|
||||
false,
|
||||
TagType::Unknown,
|
||||
global_class,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn root_node_to_tokens_ssr(cx: &Ident, node: &Node) -> TokenStream {
|
||||
fn root_node_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &Node,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
match node {
|
||||
Node::Fragment(fragment) => {
|
||||
fragment_to_tokens_ssr(cx, Span::call_site(), &fragment.children)
|
||||
fragment_to_tokens_ssr(cx, Span::call_site(), &fragment.children, global_class)
|
||||
}
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
|
||||
Node::Text(node) => {
|
||||
@@ -181,16 +197,22 @@ fn root_node_to_tokens_ssr(cx: &Ident, node: &Node) -> TokenStream {
|
||||
Node::Block(node) => {
|
||||
let value = node.value.as_ref();
|
||||
quote! {
|
||||
#[allow(unused_braces)]
|
||||
#value
|
||||
}
|
||||
}
|
||||
Node::Element(node) => root_element_to_tokens_ssr(cx, node),
|
||||
Node::Element(node) => root_element_to_tokens_ssr(cx, node, global_class),
|
||||
}
|
||||
}
|
||||
|
||||
fn fragment_to_tokens_ssr(cx: &Ident, _span: Span, nodes: &[Node]) -> TokenStream {
|
||||
fn fragment_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
_span: Span,
|
||||
nodes: &[Node],
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let nodes = nodes.iter().map(|node| {
|
||||
let node = root_node_to_tokens_ssr(cx, node);
|
||||
let node = root_node_to_tokens_ssr(cx, node, global_class);
|
||||
quote! {
|
||||
#node.into_view(#cx)
|
||||
}
|
||||
@@ -204,33 +226,49 @@ fn fragment_to_tokens_ssr(cx: &Ident, _span: Span, nodes: &[Node]) -> TokenStrea
|
||||
}
|
||||
}
|
||||
|
||||
fn root_element_to_tokens_ssr(cx: &Ident, node: &NodeElement) -> TokenStream {
|
||||
let mut template = String::new();
|
||||
let mut holes = Vec::<TokenStream>::new();
|
||||
let mut exprs_for_compiler = Vec::<TokenStream>::new();
|
||||
|
||||
element_to_tokens_ssr(cx, node, &mut template, &mut holes, &mut exprs_for_compiler);
|
||||
|
||||
let template = if holes.is_empty() {
|
||||
quote! {
|
||||
#template
|
||||
}
|
||||
fn root_element_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
if is_component_node(node) {
|
||||
component_to_tokens(cx, node, global_class)
|
||||
} else {
|
||||
quote! {
|
||||
format!(
|
||||
#template,
|
||||
#(#holes)*
|
||||
)
|
||||
}
|
||||
};
|
||||
let mut template = String::new();
|
||||
let mut holes = Vec::<TokenStream>::new();
|
||||
let mut exprs_for_compiler = Vec::<TokenStream>::new();
|
||||
|
||||
let tag_name = node.name.to_string();
|
||||
let typed_element_name = Ident::new(&camel_case_tag_name(&tag_name), node.name.span());
|
||||
quote! {
|
||||
{
|
||||
#(#exprs_for_compiler)*
|
||||
::leptos::HtmlElement::from_html(cx, leptos::#typed_element_name::default(), #template)
|
||||
}
|
||||
element_to_tokens_ssr(
|
||||
cx,
|
||||
node,
|
||||
&mut template,
|
||||
&mut holes,
|
||||
&mut exprs_for_compiler,
|
||||
true,
|
||||
global_class,
|
||||
);
|
||||
|
||||
let template = if holes.is_empty() {
|
||||
quote! {
|
||||
#template
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
format!(
|
||||
#template,
|
||||
#(#holes)*
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let tag_name = node.name.to_string();
|
||||
let typed_element_name = Ident::new(&camel_case_tag_name(&tag_name), node.name.span());
|
||||
quote! {
|
||||
{
|
||||
#(#exprs_for_compiler)*
|
||||
::leptos::HtmlElement::from_html(cx, leptos::#typed_element_name::default(), #template)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,10 +278,12 @@ fn element_to_tokens_ssr(
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
exprs_for_compiler: &mut Vec<TokenStream>,
|
||||
is_root: bool,
|
||||
global_class: Option<&TokenTree>,
|
||||
) {
|
||||
if is_component_node(node) {
|
||||
template.push_str("{}");
|
||||
let component = component_to_tokens(cx, node);
|
||||
let component = component_to_tokens(cx, node, global_class);
|
||||
holes.push(quote! {
|
||||
{#component}.into_view(cx).render_to_string(cx),
|
||||
})
|
||||
@@ -257,7 +297,27 @@ fn element_to_tokens_ssr(
|
||||
}
|
||||
}
|
||||
|
||||
set_class_attribute_ssr(cx, node, template, holes);
|
||||
// insert hydration ID
|
||||
let hydration_id = if is_root {
|
||||
quote! { leptos::HydrationCtx::peek(), }
|
||||
} else {
|
||||
quote! { leptos::HydrationCtx::id(), }
|
||||
};
|
||||
match node
|
||||
.attributes
|
||||
.iter()
|
||||
.find(|node| matches!(node, Node::Attribute(attr) if attr.key.to_string() == "id"))
|
||||
{
|
||||
Some(_) => {
|
||||
template.push_str(" leptos-hk=\"_{}\"");
|
||||
}
|
||||
None => {
|
||||
template.push_str(" id=\"_{}\"");
|
||||
}
|
||||
}
|
||||
holes.push(hydration_id);
|
||||
|
||||
set_class_attribute_ssr(cx, node, template, holes, global_class);
|
||||
|
||||
if is_self_closing(node) {
|
||||
template.push_str("/>");
|
||||
@@ -265,9 +325,15 @@ fn element_to_tokens_ssr(
|
||||
template.push('>');
|
||||
for child in &node.children {
|
||||
match child {
|
||||
Node::Element(child) => {
|
||||
element_to_tokens_ssr(cx, child, template, holes, exprs_for_compiler)
|
||||
}
|
||||
Node::Element(child) => element_to_tokens_ssr(
|
||||
cx,
|
||||
child,
|
||||
template,
|
||||
holes,
|
||||
exprs_for_compiler,
|
||||
false,
|
||||
global_class,
|
||||
),
|
||||
Node::Text(text) => {
|
||||
if let Some(value) = value_to_string(&text.value) {
|
||||
template.push_str(&value);
|
||||
@@ -345,13 +411,12 @@ fn attribute_to_tokens_ssr(
|
||||
.parse::<TokenStream>()
|
||||
.expect("couldn't parse event name");
|
||||
|
||||
let event_type = if is_force_undelegated {
|
||||
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::ev::#event_type }
|
||||
};
|
||||
exprs_for_compiler.push(quote! {
|
||||
let event_type = if is_force_undelegated {
|
||||
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::ev::#event_type }
|
||||
};
|
||||
|
||||
leptos::ssr_event_listener(#event_type, #handler);
|
||||
})
|
||||
} else if name.strip_prefix("prop:").is_some() || name.strip_prefix("class:").is_some() {
|
||||
@@ -366,15 +431,16 @@ fn attribute_to_tokens_ssr(
|
||||
|
||||
if let Some(value) = node.value.as_ref() {
|
||||
if let Some(value) = value_to_string(value) {
|
||||
template.push_str("=\"");
|
||||
template.push_str(&value);
|
||||
template.push('"');
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
template.push_str("=\"{}\"");
|
||||
let value = value.as_ref();
|
||||
holes.push(quote! {
|
||||
leptos::escape_attr(&{#value}.into_attribute(#cx).as_value_string(#name)),
|
||||
leptos::escape_attr(&{#value}.into_attribute(#cx).as_nameless_value_string()),
|
||||
})
|
||||
}
|
||||
template.push('"');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,7 +451,17 @@ fn set_class_attribute_ssr(
|
||||
node: &NodeElement,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) {
|
||||
let static_global_class = match global_class {
|
||||
Some(TokenTree::Literal(lit)) => lit.to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
let dyn_global_class = match global_class {
|
||||
None => None,
|
||||
Some(TokenTree::Literal(_)) => None,
|
||||
Some(val) => Some(val),
|
||||
};
|
||||
let static_class_attr = node
|
||||
.attributes
|
||||
.iter()
|
||||
@@ -400,6 +476,8 @@ fn set_class_attribute_ssr(
|
||||
None
|
||||
}
|
||||
})
|
||||
.chain(std::iter::once(static_global_class))
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
@@ -463,7 +541,11 @@ fn set_class_attribute_ssr(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !static_class_attr.is_empty() || !dyn_class_attr.is_empty() || !class_attrs.is_empty() {
|
||||
if !static_class_attr.is_empty()
|
||||
|| !dyn_class_attr.is_empty()
|
||||
|| !class_attrs.is_empty()
|
||||
|| dyn_global_class.is_some()
|
||||
{
|
||||
template.push_str(" class=\"");
|
||||
|
||||
template.push_str(&static_class_attr);
|
||||
@@ -485,6 +567,11 @@ fn set_class_attribute_ssr(
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(dyn_global_class) = dyn_global_class {
|
||||
template.push_str(" {}");
|
||||
holes.push(quote! { #dyn_global_class, });
|
||||
}
|
||||
|
||||
template.push('"');
|
||||
}
|
||||
}
|
||||
@@ -495,9 +582,10 @@ fn fragment_to_tokens(
|
||||
nodes: &[Node],
|
||||
lazy: bool,
|
||||
parent_type: TagType,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let nodes = nodes.iter().map(|node| {
|
||||
let node = node_to_tokens(cx, node, parent_type);
|
||||
let node = node_to_tokens(cx, node, parent_type, global_class);
|
||||
|
||||
quote! {
|
||||
#node.into_view(#cx)
|
||||
@@ -522,7 +610,12 @@ fn fragment_to_tokens(
|
||||
}
|
||||
}
|
||||
|
||||
fn node_to_tokens(cx: &Ident, node: &Node, parent_type: TagType) -> TokenStream {
|
||||
fn node_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &Node,
|
||||
parent_type: TagType,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
match node {
|
||||
Node::Fragment(fragment) => fragment_to_tokens(
|
||||
cx,
|
||||
@@ -530,6 +623,7 @@ fn node_to_tokens(cx: &Ident, node: &Node, parent_type: TagType) -> TokenStream
|
||||
&fragment.children,
|
||||
false,
|
||||
parent_type,
|
||||
global_class,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => quote! {},
|
||||
Node::Text(node) => {
|
||||
@@ -543,18 +637,23 @@ fn node_to_tokens(cx: &Ident, node: &Node, parent_type: TagType) -> TokenStream
|
||||
quote! { #value }
|
||||
}
|
||||
Node::Attribute(node) => attribute_to_tokens(cx, node),
|
||||
Node::Element(node) => element_to_tokens(cx, node, parent_type),
|
||||
Node::Element(node) => element_to_tokens(cx, node, parent_type, global_class),
|
||||
}
|
||||
}
|
||||
|
||||
fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -> TokenStream {
|
||||
fn element_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
mut parent_type: TagType,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
if is_component_node(node) {
|
||||
component_to_tokens(cx, node)
|
||||
component_to_tokens(cx, node, global_class)
|
||||
} else {
|
||||
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;
|
||||
@@ -590,6 +689,14 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
|
||||
None
|
||||
}
|
||||
});
|
||||
let global_class_expr = match global_class {
|
||||
None => quote! {},
|
||||
Some(class) => {
|
||||
quote! {
|
||||
.class(#class, true)
|
||||
}
|
||||
}
|
||||
};
|
||||
let children = node.children.iter().map(|node| {
|
||||
let child = match node {
|
||||
Node::Fragment(fragment) => fragment_to_tokens(
|
||||
@@ -598,6 +705,7 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
|
||||
&fragment.children,
|
||||
false,
|
||||
parent_type,
|
||||
global_class,
|
||||
),
|
||||
Node::Text(node) => {
|
||||
let value = node.value.as_ref();
|
||||
@@ -611,7 +719,7 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
|
||||
#[allow(unused_braces)] #value
|
||||
}
|
||||
}
|
||||
Node::Element(node) => element_to_tokens(cx, node, parent_type),
|
||||
Node::Element(node) => element_to_tokens(cx, node, parent_type, global_class),
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
|
||||
};
|
||||
quote! {
|
||||
@@ -621,6 +729,7 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
|
||||
quote! {
|
||||
#name
|
||||
#(#attrs)*
|
||||
#global_class_expr
|
||||
#(#children)*
|
||||
}
|
||||
}
|
||||
@@ -797,7 +906,11 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn component_to_tokens(cx: &Ident, node: &NodeElement) -> TokenStream {
|
||||
fn component_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let name = &node.name;
|
||||
let component_name = ident_from_tag_name(&node.name);
|
||||
let span = node.name.span();
|
||||
@@ -848,7 +961,14 @@ fn component_to_tokens(cx: &Ident, node: &NodeElement) -> TokenStream {
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
let children = fragment_to_tokens(cx, span, &node.children, true, TagType::Unknown);
|
||||
let children = fragment_to_tokens(
|
||||
cx,
|
||||
span,
|
||||
&node.children,
|
||||
true,
|
||||
TagType::Unknown,
|
||||
global_class,
|
||||
);
|
||||
|
||||
let clonables = items_to_clone
|
||||
.iter()
|
||||
@@ -1000,16 +1120,13 @@ fn is_svg_element(tag: &str) -> bool {
|
||||
| "polyline"
|
||||
| "radialGradient"
|
||||
| "rect"
|
||||
| "script"
|
||||
| "set"
|
||||
| "stop"
|
||||
| "style"
|
||||
| "svg"
|
||||
| "switch"
|
||||
| "symbol"
|
||||
| "text"
|
||||
| "textPath"
|
||||
| "title"
|
||||
| "tspan"
|
||||
| "use"
|
||||
| "use_"
|
||||
@@ -1055,7 +1172,7 @@ fn is_math_ml_element(tag: &str) -> bool {
|
||||
}
|
||||
|
||||
fn is_ambiguous_element(tag: &str) -> bool {
|
||||
matches!(tag, "a")
|
||||
tag == "a" || tag == "script"
|
||||
}
|
||||
|
||||
fn parse_event(event_name: &str) -> (&str, bool) {
|
||||
|
||||
@@ -17,7 +17,7 @@ js-sys = "0.3"
|
||||
miniserde = { version = "0.1", optional = true }
|
||||
serde-wasm-bindgen = "0.4"
|
||||
serde_json = "1"
|
||||
base64 = "0.13"
|
||||
base64 = "0.21"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["rt"], optional = true }
|
||||
tracing = "0.1"
|
||||
|
||||
@@ -56,7 +56,7 @@ where
|
||||
{
|
||||
let id = value.type_id();
|
||||
|
||||
with_runtime(cx.runtime, |runtime| {
|
||||
_ = with_runtime(cx.runtime, |runtime| {
|
||||
let mut contexts = runtime.scope_contexts.borrow_mut();
|
||||
let context = contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
|
||||
context.insert(id, Box::new(value) as Box<dyn Any>);
|
||||
@@ -136,4 +136,6 @@ where
|
||||
}),
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ where
|
||||
)
|
||||
)]
|
||||
fn run(&self, id: EffectId, runtime: RuntimeId) {
|
||||
with_runtime(runtime, |runtime| {
|
||||
_ = with_runtime(runtime, |runtime| {
|
||||
// clear previous dependencies
|
||||
id.cleanup(runtime);
|
||||
|
||||
@@ -201,7 +201,7 @@ where
|
||||
|
||||
impl EffectId {
|
||||
pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
|
||||
with_runtime(runtime_id, |runtime| {
|
||||
_ = with_runtime(runtime_id, |runtime| {
|
||||
let effect = {
|
||||
let effects = runtime.effects.borrow();
|
||||
effects.get(*self).cloned()
|
||||
|
||||
@@ -131,7 +131,8 @@ where
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
runtime.create_serializable_resource(Rc::clone(&r))
|
||||
});
|
||||
})
|
||||
.expect("tried to create a Resource in a Runtime that has been disposed.");
|
||||
|
||||
create_isomorphic_effect(cx, {
|
||||
let r = Rc::clone(&r);
|
||||
@@ -250,7 +251,8 @@ where
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
runtime.create_unserializable_resource(Rc::clone(&r))
|
||||
});
|
||||
})
|
||||
.expect("tried to create a Resource in a runtime that has been disposed.");
|
||||
|
||||
create_effect(cx, {
|
||||
let r = Rc::clone(&r);
|
||||
@@ -288,7 +290,7 @@ where
|
||||
{
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
with_runtime(cx.runtime, |runtime| {
|
||||
_ = with_runtime(cx.runtime, |runtime| {
|
||||
let mut context = runtime.shared_context.borrow_mut();
|
||||
if let Some(data) = context.resolved_resources.remove(&id) {
|
||||
// The server already sent us the serialized resource value, so
|
||||
@@ -363,6 +365,8 @@ where
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.read())
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Applies a function to the current value of the resource, and subscribes
|
||||
@@ -376,6 +380,8 @@ where
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.with(f))
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Returns a signal that indicates whether the resource is currently loading.
|
||||
@@ -383,11 +389,12 @@ where
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.loading)
|
||||
})
|
||||
.expect("tried to call Resource::loading() in a runtime that has already been disposed.")
|
||||
}
|
||||
|
||||
/// Re-runs the async function with the current source data.
|
||||
pub fn refetch(&self) {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.refetch())
|
||||
});
|
||||
}
|
||||
@@ -404,6 +411,7 @@ where
|
||||
resource.to_serialization_resolver(self.id)
|
||||
})
|
||||
})
|
||||
.expect("tried to serialize a Resource in a runtime that has already been disposed")
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,19 +34,19 @@ cfg_if! {
|
||||
|
||||
/// Get the selected runtime from the thread-local set of runtimes. On the server,
|
||||
/// this will return the correct runtime. In the browser, there should only be one runtime.
|
||||
pub(crate) fn with_runtime<T>(id: RuntimeId, f: impl FnOnce(&Runtime) -> T) -> T {
|
||||
pub(crate) fn with_runtime<T>(id: RuntimeId, f: impl FnOnce(&Runtime) -> T) -> Result<T, ()> {
|
||||
// in the browser, everything should exist under one runtime
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
_ = id;
|
||||
RUNTIME.with(|runtime| f(runtime))
|
||||
Ok(RUNTIME.with(|runtime| f(runtime)))
|
||||
} else {
|
||||
RUNTIMES.with(|runtimes| {
|
||||
let runtimes = runtimes.borrow();
|
||||
let runtime = runtimes
|
||||
.get(id)
|
||||
.expect("Tried to access a Runtime that no longer exists.");
|
||||
f(runtime)
|
||||
match runtimes.get(id) {
|
||||
None => Err(()),
|
||||
Some(runtime) => Ok(f(runtime))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ impl RuntimeId {
|
||||
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
|
||||
(scope, disposer)
|
||||
})
|
||||
.expect("tried to create raw scope in a runtime that has already been disposed")
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope_undisposed<T>(
|
||||
@@ -105,6 +106,7 @@ impl RuntimeId {
|
||||
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
|
||||
(val, id, disposer)
|
||||
})
|
||||
.expect("tried to run scope in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope<T>(self, f: impl FnOnce(Scope) -> T, parent: Option<Scope>) -> T {
|
||||
@@ -123,7 +125,8 @@ impl RuntimeId {
|
||||
.signals
|
||||
.borrow_mut()
|
||||
.insert(Rc::new(RefCell::new(value)))
|
||||
});
|
||||
})
|
||||
.expect("tried to create a signal in a runtime that has been disposed");
|
||||
(
|
||||
ReadSignal {
|
||||
runtime: self,
|
||||
@@ -151,7 +154,8 @@ impl RuntimeId {
|
||||
.signals
|
||||
.borrow_mut()
|
||||
.insert(Rc::new(RefCell::new(value)))
|
||||
});
|
||||
})
|
||||
.expect("tried to create a signal in a runtime that has been disposed");
|
||||
RwSignal {
|
||||
runtime: self,
|
||||
id,
|
||||
@@ -180,6 +184,7 @@ impl RuntimeId {
|
||||
id.run::<T>(self);
|
||||
id
|
||||
})
|
||||
.expect("tried to create an effect in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -107,7 +107,7 @@ impl Scope {
|
||||
/// has navigated away from the route.)
|
||||
pub fn run_child_scope<T>(self, f: impl FnOnce(Scope) -> T) -> (T, ScopeDisposer) {
|
||||
let (res, child_id, disposer) = self.runtime.run_scope_undisposed(f, Some(self));
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let mut children = runtime.scope_children.borrow_mut();
|
||||
children
|
||||
.entry(self.id)
|
||||
@@ -150,6 +150,7 @@ impl Scope {
|
||||
runtime.observer.set(prev_observer);
|
||||
untracked_result
|
||||
})
|
||||
.expect("tried to run untracked function in a runtime that has been disposed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +164,7 @@ impl Scope {
|
||||
/// 2. run all cleanup functions defined for this scope by [on_cleanup](crate::on_cleanup).
|
||||
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
|
||||
pub fn dispose(self) {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
// dispose of all child scopes
|
||||
let children = {
|
||||
let mut children = runtime.scope_children.borrow_mut();
|
||||
@@ -225,7 +226,7 @@ impl Scope {
|
||||
}
|
||||
|
||||
pub(crate) fn with_scope_property(&self, f: impl FnOnce(&mut Vec<ScopeProperty>)) {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let scopes = runtime.scopes.borrow();
|
||||
let scope = scopes
|
||||
.get(self.id)
|
||||
@@ -233,6 +234,19 @@ impl Scope {
|
||||
f(&mut scope.borrow_mut());
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the the parent Scope, if any.
|
||||
pub fn parent(&self) -> Option<Scope> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.scope_parents.borrow().get(self.id).copied()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|id| Scope {
|
||||
runtime: self.runtime,
|
||||
id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a cleanup function, which will be run when a [Scope] is disposed.
|
||||
@@ -240,7 +254,7 @@ impl Scope {
|
||||
/// It runs after child scopes have been disposed, but before signals, effects, and resources
|
||||
/// are invalidated.
|
||||
pub fn on_cleanup(cx: Scope, cleanup_fn: impl FnOnce() + 'static) {
|
||||
with_runtime(cx.runtime, |runtime| {
|
||||
_ = with_runtime(cx.runtime, |runtime| {
|
||||
let mut cleanups = runtime.scope_cleanups.borrow_mut();
|
||||
let cleanups = cleanups
|
||||
.entry(cx.id)
|
||||
@@ -286,18 +300,18 @@ impl ScopeDisposer {
|
||||
impl Scope {
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
|
||||
pub fn all_resources(&self) -> Vec<ResourceId> {
|
||||
with_runtime(self.runtime, |runtime| runtime.all_resources())
|
||||
with_runtime(self.runtime, |runtime| runtime.all_resources()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope that are
|
||||
/// pending from the server.
|
||||
pub fn pending_resources(&self) -> Vec<ResourceId> {
|
||||
with_runtime(self.runtime, |runtime| runtime.pending_resources())
|
||||
with_runtime(self.runtime, |runtime| runtime.pending_resources()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
|
||||
pub fn serialization_resolvers(&self) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
|
||||
with_runtime(self.runtime, |runtime| runtime.serialization_resolvers())
|
||||
with_runtime(self.runtime, |runtime| runtime.serialization_resolvers()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Registers the given [SuspenseContext](crate::SuspenseContext) with the current scope,
|
||||
@@ -312,7 +326,7 @@ impl Scope {
|
||||
use crate::create_isomorphic_effect;
|
||||
use futures::StreamExt;
|
||||
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let mut shared_context = runtime.shared_context.borrow_mut();
|
||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
@@ -336,7 +350,7 @@ impl Scope {
|
||||
})
|
||||
}
|
||||
|
||||
/// The set of all HTML fragments current pending, by their keys (see [Self::current_fragment_key]).
|
||||
/// The set of all HTML fragments currently pending.
|
||||
/// Returns a tuple of the hydration ID of the previous element, and a pinned `Future` that will yield the
|
||||
/// `<Suspense/>` HTML when all resources are resolved.
|
||||
pub fn pending_fragments(&self) -> HashMap<String, (String, PinnedFuture<String>)> {
|
||||
@@ -344,6 +358,7 @@ impl Scope {
|
||||
let mut shared_context = runtime.shared_context.borrow_mut();
|
||||
std::mem::take(&mut shared_context.pending_fragments)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ where
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub(crate) fn subscribe(&self) {
|
||||
with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
|
||||
_ = with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
|
||||
}
|
||||
|
||||
/// Clones and returns the current value of the signal, and subscribes
|
||||
@@ -286,10 +286,15 @@ where
|
||||
/// Applies the function to the current Signal, if it exists, and subscribes
|
||||
/// the running effect.
|
||||
pub(crate) fn try_with<U>(&self, f: impl FnOnce(&T) -> U) -> Result<U, SignalError> {
|
||||
with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
|
||||
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f)) {
|
||||
Ok(Ok(v)) => Ok(v),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err(SignalError::RuntimeDisposed),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a [Stream] that emits the new value of the signal whenever it changes.
|
||||
/// Generates a [Stream](futures::stream::Stream) that emits the new value of the signal
|
||||
/// whenever it changes.
|
||||
pub fn to_stream(&self) -> impl Stream<Item = T>
|
||||
where
|
||||
T: Clone,
|
||||
@@ -1092,7 +1097,8 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Generates a [Stream] that emits the new value of the signal whenever it changes.
|
||||
/// Generates a [Stream](futures::stream::Stream) that emits the new value of the signal
|
||||
/// whenever it changes.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -1154,6 +1160,8 @@ slotmap::new_key_type! {
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum SignalError {
|
||||
#[error("tried to access a signal in a runtime that had been disposed")]
|
||||
RuntimeDisposed,
|
||||
#[error("tried to access a signal that had been disposed")]
|
||||
Disposed,
|
||||
#[error("error casting signal to type {0}")]
|
||||
@@ -1235,6 +1243,7 @@ impl SignalId {
|
||||
with_runtime(runtime, |runtime| {
|
||||
self.try_with_no_subscription(runtime, f).unwrap()
|
||||
})
|
||||
.expect("tried to access a signal in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
pub(crate) fn with<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&T) -> U) -> U
|
||||
@@ -1242,6 +1251,7 @@ impl SignalId {
|
||||
T: 'static,
|
||||
{
|
||||
with_runtime(runtime, |runtime| self.try_with(runtime, f).unwrap())
|
||||
.expect("tried to access a signal in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
fn update_value<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&mut T) -> U) -> Option<U>
|
||||
@@ -1272,6 +1282,7 @@ impl SignalId {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn update<T, U>(
|
||||
@@ -1307,6 +1318,7 @@ impl SignalId {
|
||||
};
|
||||
updated
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn update_with_no_effect<T, U>(
|
||||
|
||||
@@ -574,6 +574,12 @@ impl<T> From<Memo<T>> for MaybeSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Signal<T>> for MaybeSignal<T> {
|
||||
fn from(value: Signal<T>) -> Self {
|
||||
Self::Dynamic(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnOnce<()> for MaybeSignal<T>
|
||||
where
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
use crate::{create_memo, IntoSignalSetter, RwSignal, Scope, Signal, SignalSetter};
|
||||
|
||||
/// derives a reactive slice from an [RwSignal](crate::RwSignal)
|
||||
/// Derives a reactive slice of an [RwSignal](crate::RwSignal).
|
||||
///
|
||||
/// Slices have the same guarantees as [Memos](crate::Memo),
|
||||
/// Slices have the same guarantees as [Memos](crate::Memo):
|
||||
/// they only emit their value when it has actually been changed.
|
||||
///
|
||||
/// slices need a getter and a setter, and you must make sure that
|
||||
/// Slices need a getter and a setter, and you must make sure that
|
||||
/// the setter and getter only touch their respective field and nothing else.
|
||||
/// They optimally should not have any side effects.
|
||||
///
|
||||
/// you can use slices whenever you want to react to only parts
|
||||
/// of a bigger signal, the prime example would be state management
|
||||
/// where you want all state variables grouped up but also need
|
||||
/// You can use slices whenever you want to react to only parts
|
||||
/// of a bigger signal. The prime example would be state management,
|
||||
/// where you want all state variables grouped together, but also need
|
||||
/// fine-grained signals for each or some of these variables.
|
||||
/// In the example below, setting an auth token will only trigger
|
||||
/// the token signal, but none of the other derived signals.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # let (cx, disposer) = raw_scope_and_disposer(create_runtime());
|
||||
@@ -49,13 +48,14 @@ use crate::{create_memo, IntoSignalSetter, RwSignal, Scope, Signal, SignalSetter
|
||||
/// );
|
||||
/// let count_token_updates = create_rw_signal(cx, 0);
|
||||
/// count_token_updates.with(|counter| assert_eq!(counter, &0));
|
||||
/// create_effect(cx, move |_| {
|
||||
/// token.with(|_| {});
|
||||
/// create_isomorphic_effect(cx, move |_| {
|
||||
/// _ = token.with(|_| {});
|
||||
/// count_token_updates.update(|counter| *counter += 1)
|
||||
/// });
|
||||
/// count_token_updates.with(|counter| assert_eq!(counter, &1));
|
||||
/// set_token.set("this is not a token!".into());
|
||||
/// // token was updated with the new token
|
||||
/// token.with(|token| assert_eq!(token, "this is not a token!"));
|
||||
/// count_token_updates.with(|counter| assert_eq!(counter, &2));
|
||||
/// set_dark_mode.set(true);
|
||||
/// // since token didn't change, there was also no update emitted
|
||||
|
||||
@@ -6,6 +6,7 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/gbj/leptos"
|
||||
description = "RPC for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
leptos_dom = { workspace = true }
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
//!
|
||||
//! ### `#[server]`
|
||||
//!
|
||||
//! The [`#[server]` macro](leptos::leptos_macro::server) allows you to annotate a function to
|
||||
//! The [`#[server]`](https://docs.rs/leptos/latest/leptos/attr.server.html) macro allows you to annotate a function to
|
||||
//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
|
||||
//! crate that is enabled).
|
||||
//!
|
||||
@@ -326,9 +326,19 @@ where
|
||||
let mut write = REGISTERED_SERVER_FUNCTIONS
|
||||
.write()
|
||||
.map_err(|e| ServerFnError::Registration(e.to_string()))?;
|
||||
write.insert(Self::url(), run_server_fn);
|
||||
let prev = write.insert(Self::url(), run_server_fn);
|
||||
|
||||
Ok(())
|
||||
// if there was already a server function with this key,
|
||||
// return Err
|
||||
match prev {
|
||||
Some(_) => Err(ServerFnError::Registration(format!(
|
||||
"There was already a server function registered at {:?}. \
|
||||
This can happen if you use the same server function name in two different modules
|
||||
on `stable` or in `release` mode.",
|
||||
Self::url()
|
||||
))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! # Leptos Meta
|
||||
//!
|
||||
//! Leptos Meta allows you to modify content in a document’s `<head>` from within components
|
||||
//! using the [Leptos](https://github.com/gbj/leptos) web framework.
|
||||
//! using the [Leptos](https://github.com/leptos-rs/leptos) web framework.
|
||||
//!
|
||||
//! Document metadata is updated automatically when running in the browser. For server-side
|
||||
//! rendering, after the component tree is rendered to HTML, [MetaContext::dehydrate] can generate
|
||||
@@ -34,8 +34,16 @@
|
||||
//! }
|
||||
//!
|
||||
//! }
|
||||
//!
|
||||
//! ```
|
||||
//! # Feature Flags
|
||||
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
|
||||
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
|
||||
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
|
||||
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
|
||||
//! of calling signals as functions. Enable this feature to support `stable` Rust.
|
||||
//!
|
||||
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
||||
//! which mode your app is operating in.
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use std::{
|
||||
@@ -64,7 +72,7 @@ pub use title::*;
|
||||
///
|
||||
/// This should generally by provided somewhere in the root of your application using
|
||||
/// [provide_meta_context].
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct MetaContext {
|
||||
pub(crate) title: TitleContext,
|
||||
pub(crate) tags: MetaTagsContext,
|
||||
@@ -78,6 +86,12 @@ pub(crate) struct MetaTagsContext {
|
||||
els: Rc<RefCell<HashMap<String, (HtmlElement<AnyElement>, Scope, Option<web_sys::Element>)>>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MetaTagsContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("MetaTagsContext").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl MetaTagsContext {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn as_string(&self) -> String {
|
||||
@@ -208,10 +222,10 @@ impl MetaContext {
|
||||
/// // `app` contains only the body content w/ hydration stuff, not the meta tags
|
||||
/// assert_eq!(
|
||||
/// app.into_view(cx).render_to_string(cx),
|
||||
/// "<main id=\"_0-1\"><leptos-unit leptos id=_0-2c></leptos-unit><leptos-unit leptos id=_0-4c></leptos-unit><p id=\"_0-5\">Some text</p></main>"
|
||||
/// "<main id=\"_0-1\"><!--hk=_0-2c|leptos-unit--><!--hk=_0-4c|leptos-unit--><p id=\"_0-5\">Some text</p></main>"
|
||||
/// );
|
||||
/// // `MetaContext::dehydrate()` gives you HTML that should be in the `<head>`
|
||||
/// assert_eq!(use_head(cx).dehydrate(), r#"<title>my title</title><link id="leptos-link-1" href="/style.css" rel="stylesheet" leptos-hk="_0-3"/>"#)
|
||||
/// assert_eq!(use_head(cx).dehydrate(), "<title>my title</title><link id=\"leptos-link-1\" href=\"/style.css\" rel=\"stylesheet\" leptos-hk=\"_0-3\"/>")
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::{use_head, TextProp};
|
||||
/// <main>
|
||||
/// <Meta charset="utf-8"/>
|
||||
/// <Meta name="description" content="A Leptos fan site."/>
|
||||
/// <Meta http_equiv="refresh" content="3;url=https://github.com/gbj/leptos"/>
|
||||
/// <Meta http_equiv="refresh" content="3;url=https://github.com/leptos-rs/leptos"/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
README = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Router for the Leptos web framework."
|
||||
|
||||
@@ -19,7 +20,7 @@ log = "0.4"
|
||||
regex = { version = "1", optional = true }
|
||||
bincode = "1"
|
||||
url = { version = "2", optional = true }
|
||||
urlencoding = "2"
|
||||
percent-encoding = "2"
|
||||
thiserror = "1"
|
||||
serde_urlencoded = "0.7"
|
||||
serde = "1"
|
||||
|
||||
@@ -36,6 +36,14 @@ where
|
||||
|
||||
/// An HTML [`a`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)
|
||||
/// progressively enhanced to use client-side routing.
|
||||
///
|
||||
/// Client-side routing also works with ordinary HTML `<a>` tags, but `<A>` does two additional things:
|
||||
/// 1) Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky.
|
||||
/// For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative
|
||||
/// route, but `<a href="1">` likely will not (depending on where it appears in your view.)
|
||||
/// 2) Sets the `aria-current` attribute if this link is the active link (i.e., it’s a link to the page you’re on).
|
||||
/// This is helpful for accessibility and for styling. For example, maybe you want to set the link a
|
||||
/// different color if it’s a link to the page you’re currently on.
|
||||
#[component]
|
||||
pub fn A<H>(
|
||||
cx: Scope,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user