mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
52 Commits
remove-ope
...
revert-538
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0f7d900a1 | ||
|
|
59ad6a4725 | ||
|
|
884dacbc6c | ||
|
|
9c572f7617 | ||
|
|
487dba90d8 | ||
|
|
20f24d2f3a | ||
|
|
20cbc240ee | ||
|
|
f2f52b2533 | ||
|
|
46d6e3f78c | ||
|
|
586f524015 | ||
|
|
79781ec20c | ||
|
|
91f6d9a404 | ||
|
|
76a74ecde2 | ||
|
|
0071a48b8a | ||
|
|
8d42e91eb8 | ||
|
|
00a796d204 | ||
|
|
bde585dc3e | ||
|
|
0a534bd7fd | ||
|
|
50d8eae694 | ||
|
|
e732a4952b | ||
|
|
8a99623fd6 | ||
|
|
7d6c4930e4 | ||
|
|
81d6689cc0 | ||
|
|
989b5b93c3 | ||
|
|
ca510f72c1 | ||
|
|
6dd3be75d1 | ||
|
|
51e11e756a | ||
|
|
1dbcfe2861 | ||
|
|
db3f46c501 | ||
|
|
1cba54d47e | ||
|
|
d1ae3b49cc | ||
|
|
6bab4ad966 | ||
|
|
d4648da5c6 | ||
|
|
cf7deaaea3 | ||
|
|
d0cacecfc6 | ||
|
|
ce2c3ec97c | ||
|
|
b9f05f94ce | ||
|
|
fe7aacb0c8 | ||
|
|
3fd3e73a10 | ||
|
|
7dca740e47 | ||
|
|
73420affed | ||
|
|
7c25f59a68 | ||
|
|
c24874d9c8 | ||
|
|
4759dfcb60 | ||
|
|
ca9419b53f | ||
|
|
765006158a | ||
|
|
8a1adaefaf | ||
|
|
086326324e | ||
|
|
e59ee6329e | ||
|
|
a2b31a51d9 | ||
|
|
b0a98d8b4f | ||
|
|
6931d3904b |
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -30,6 +30,9 @@ jobs:
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
@@ -43,4 +46,3 @@ jobs:
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make ci
|
||||
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@@ -11,6 +11,7 @@ members = [
|
||||
# integrations
|
||||
"integrations/actix",
|
||||
"integrations/axum",
|
||||
"integrations/utils",
|
||||
|
||||
# libraries
|
||||
"meta",
|
||||
@@ -19,17 +20,18 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3"
|
||||
version = "0.2.0-alpha2"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.1.3" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.3" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.3" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.3" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.3" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.3" }
|
||||
leptos_router = { path = "./router", version = "0.1.3" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.3" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.0-alpha2" }
|
||||
leptos_router = { path = "./router", version = "0.2.0-alpha2" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.0-alpha2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.0-alpha2" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -12,13 +12,17 @@ dependencies = ["build", "check-examples", "test"]
|
||||
|
||||
[tasks.build]
|
||||
clear = true
|
||||
dependencies = ["build-all"]
|
||||
dependencies = ["build-all", "build-wasm"]
|
||||
|
||||
[tasks.build-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.build-wasm]
|
||||
clear = true
|
||||
dependencies = [{ name = "build-wasm", path = "leptos" }]
|
||||
|
||||
[tasks.check-examples]
|
||||
clear = true
|
||||
dependencies = [
|
||||
|
||||
@@ -78,7 +78,7 @@ rustup target add wasm32-unknown-unknown
|
||||
|
||||
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"] }`
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.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/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
|
||||
@@ -99,6 +99,10 @@ Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
## FAQs
|
||||
|
||||
### What’s up with the name?
|
||||
|
||||
*Leptos* (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
People usually mean one of three things by this question.
|
||||
|
||||
1
docs/book/.gitignore
vendored
Normal file
1
docs/book/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
book
|
||||
@@ -11,9 +11,9 @@
|
||||
- [Control Flow](./view/06_control_flow.md)
|
||||
- [Error Handling](./view/07_errors.md)
|
||||
- [Parent-Child Communication](./view/08_parent_child.md)
|
||||
- [Passing Children to Components]()
|
||||
- [Interlude: Reactivity and Functions]()
|
||||
- [Testing]()
|
||||
- [Passing Children to Components](./view/09_component_children.md)
|
||||
- [Interlude: Reactivity and Functions](./interlude_functions.md)
|
||||
- [Testing](./testing.md)
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Async]()
|
||||
- [Resource]()
|
||||
|
||||
76
docs/book/src/interlude_functions.md
Normal file
76
docs/book/src/interlude_functions.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Interlude: Reactivity and Functions
|
||||
|
||||
One of our core contributors said to me recently: “I never used closures this often
|
||||
until I started using Leptos.” And it’s true. Closures are at the heart of any Leptos
|
||||
application. It sometimes looks a little silly:
|
||||
|
||||
```rust
|
||||
// a signal holds a value, and can be updated
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
// a derived signal is a function that accesses other signals
|
||||
let double_count = move || count() * 2;
|
||||
let count_is_odd = move || count() & 1 == 1;
|
||||
let text = move || if count_is_odd() {
|
||||
"odd"
|
||||
} else {
|
||||
"even"
|
||||
};
|
||||
|
||||
// an effect automatically tracks the signals it depends on
|
||||
// and re-runs when they change
|
||||
create_effect(cx, move |_| {
|
||||
log!("text = {}", text());
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<p>{move || text().to_uppercase()}</p>
|
||||
}
|
||||
```
|
||||
|
||||
Closures, closures everywhere!
|
||||
|
||||
But why?
|
||||
|
||||
## Functions and UI Frameworks
|
||||
|
||||
Functions are at the heart of every UI framework. And this makes perfect sense. Creating a user interface is basically divided into two phases:
|
||||
|
||||
1. initial rendering
|
||||
2. updates
|
||||
|
||||
In a web framework, the framework does some kind of initial rendering. Then it hands control back over to the browser. When certain events fire (like a mouse click) or asynchronous tasks finish (like an HTTP request finishing), the browser wakes the framework back up to update something. The framework runs some kind of code to update your user interface, and goes back asleep until the browser wakes it up again.
|
||||
|
||||
The key phrase here is “runs some kind of code.” The natural way to “run some kind of code” at an arbitrary point in time—in Rust or in any other programming language—is to call a function. And in fact every UI framework is based on rerunning some kind of function over and over:
|
||||
|
||||
1. virtual DOM (VDOM) frameworks like React, Yew, or Dioxus rerun a component or render function over and over, to generate a virtual DOM tree that can be reconciled with the previous result to patch the DOM
|
||||
2. compiled frameworks like Angular and Svelte divide your component templates into “create” and “update” functions, rerunning the update function when they detect a change to the component’s state
|
||||
3. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, _you_ define the functions that re-run
|
||||
|
||||
That’s what all our components are doing.
|
||||
|
||||
Take our typical `<SimpleCounter/>` example in its simplest form:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn SimpleCounter(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
view! { cx,
|
||||
<button on:click=increment>
|
||||
{value}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `SimpleCounter` function itself runs once. The `value` signal is created once. The framework hands off the `increment` function to the browser as an event listener. When you click the button, the browser calls `increment`, which updates `value` via `set_value`. And that updates the single text node represented in our view by `{value}`.
|
||||
|
||||
Closures are key to reactivity. They provide the framework with the ability to re-run the smallest possible unit of your application in responsive to a change.
|
||||
|
||||
So remember two things:
|
||||
|
||||
1. Your component function is a setup function, not a render function: it only runs once.
|
||||
2. For values in your view template to be reactive, they must be functions: either signals (which implement the `Fn` traits) or closures.
|
||||
180
docs/book/src/testing.md
Normal file
180
docs/book/src/testing.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Testing Your Components
|
||||
|
||||
Testing user interfaces can be relatively tricky, but really important. This article
|
||||
will discuss a couple principles and approaches for testing a Leptos app.
|
||||
|
||||
## 1. Test business logic with ordinary Rust tests
|
||||
|
||||
In many cases, it makes sense to pull the logic out of your components and test
|
||||
it separately. For some simple components, there’s no particular logic to test, but
|
||||
for many it’s worth using a testable wrapping type and implementing the logic in
|
||||
ordinary Rust `impl` blocks.
|
||||
|
||||
For example, instead of embedding logic in a component directly like this:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
|
||||
// ⚠️ this is hard to test because it's embedded in the component
|
||||
let maximum = move || todos.with(|todos| {
|
||||
todos.iter().filter(|todo| todo.completed).sum()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
You could pull that logic out into a separate data structure and test it:
|
||||
|
||||
```rust
|
||||
pub struct Todos(Vec<Todo>);
|
||||
|
||||
impl Todos {
|
||||
pub fn remaining(&self) -> usize {
|
||||
todos.iter().filter(|todo| todo.completed).sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_remaining {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
|
||||
// ✅ this has a test associated with it
|
||||
let maximum = move || todos.with(Todos::remaining);
|
||||
}
|
||||
```
|
||||
|
||||
In general, the less of your logic is wrapped into your components themselves, the
|
||||
more idiomatic your code will feel and the easier it will be to test.
|
||||
|
||||
## 2. Test components with `wasm-bindgen-test`
|
||||
|
||||
[`wasm-bindgen-test`](https://crates.io/crates/wasm-bindgen-test) is a great utility
|
||||
for integrating or end-to-end testing WebAssembly apps in a headless browser.
|
||||
|
||||
To use this testing utility, you need to add `wasm-bindgen-test` to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
```
|
||||
|
||||
You should create tests in a separate `tests` directory. You can then run your tests in the browser of your choice:
|
||||
|
||||
```bash
|
||||
wasm-pack test --firefox
|
||||
```
|
||||
|
||||
> To see the full setup, check out the tests for the [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) example.
|
||||
|
||||
### Writing Your Tests
|
||||
|
||||
Most tests will involve some combination of vanilla DOM manipulation and comparison to a `view`. For example, here’s a test [for the
|
||||
`counter` example](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/mod.rs).
|
||||
|
||||
First, we set up the testing environment.
|
||||
|
||||
```rust
|
||||
use wasm_bindgen_test::*;
|
||||
use counter::*;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
// tell the test runner to run tests in the browser
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
```
|
||||
|
||||
I’m going to create a simpler wrapper for each test case, and mount it there.
|
||||
This makes it easy to encapsulate the test results.
|
||||
|
||||
```rust
|
||||
// like marking a regular test with #[test]
|
||||
#[wasm_bindgen_test]
|
||||
fn clear() {
|
||||
let document = leptos::document();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
|
||||
// start by rendering our counter and mounting it to the DOM
|
||||
// note that we start at the initial value of 10
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
```
|
||||
|
||||
We’ll use some manual DOM operations to grab the `<div>` that wraps
|
||||
the whole component, as well as the `clear` button.
|
||||
|
||||
```rust
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
// this would be easier if they had IDs
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = test_wrapper
|
||||
.query_selector("button")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlElement>();
|
||||
```
|
||||
|
||||
Now we can use ordinary DOM APIs to simulate user interaction.
|
||||
|
||||
```rust
|
||||
// now let's click the `clear` button
|
||||
clear.click();
|
||||
```
|
||||
|
||||
You can test individual DOM element attributes or text node values. Sometimes
|
||||
I like to test the whole view at once. We can do this by testing the element’s
|
||||
`outerHTML` against our expectations.
|
||||
|
||||
```rust
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system to render the test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
That test involved us manually replicating the `view` that’s inside the component.
|
||||
There's actually an easier way to do this... We can just test against a `<SimpleCounter/>`
|
||||
with the initial value `0`. This is where our wrapping element comes in: I’ll just test
|
||||
the wrapper’s `innerHTML` against another comparison case.
|
||||
|
||||
```rust
|
||||
assert_eq!(test_wrapper.inner_html(), {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
This is only a very limited introduction to testing. But I hope it’s useful as you begin to build applications.
|
||||
|
||||
> For more, see [the testing section of the `wasm-bindgen` guide](https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/index.html#testing-on-wasm32-unknown-unknown-with-wasm-bindgen-test).
|
||||
@@ -187,6 +187,8 @@ This rerenders `<Small/>` five times, then `<Big/>` infinitely. If they’re
|
||||
loading resources, creating signals, or even just creating DOM nodes, this is
|
||||
unnecessary work.
|
||||
|
||||
### `<Show/>`
|
||||
|
||||
The [`<Show/>`](https://docs.rs/leptos/latest/leptos/fn.Show.html) component is
|
||||
the answer. You pass it a `when` condition function, a `fallback` to be shown if
|
||||
the `when` function returns `false`, and children to be rendered if `when` is `true`.
|
||||
|
||||
124
docs/book/src/view/09_component_children.md
Normal file
124
docs/book/src/view/09_component_children.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Component Children
|
||||
|
||||
It’s pretty common to want to pass children into a component, just as you can pass
|
||||
children into an HTML element. For example, imagine I have a `<FancyForm/>` component
|
||||
that enhances an HTML `<form>`. I need some way to pass all its inputs.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<Form>
|
||||
<fieldset>
|
||||
<label>
|
||||
"Some Input"
|
||||
<input type="text" name="something"/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<button>"Submit"</button>
|
||||
</Form>
|
||||
}
|
||||
```
|
||||
|
||||
How can you do this in Leptos? There are basically two ways to pass components to
|
||||
other components:
|
||||
|
||||
1. **render props**: properties that are functions that return a view
|
||||
2. the **`children`** prop: a special component property that includes anything
|
||||
you pass as a child to the component.
|
||||
|
||||
In fact, you’ve already seen these both in action in the [`<Show/>`](/view/06_control_flow.html#show) component:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<Show
|
||||
// `when` is a normal prop
|
||||
when=move || value() > 5
|
||||
// `fallback` is a "render prop": a function that returns a view
|
||||
fallback=|cx| view! { cx, <Small/> }
|
||||
>
|
||||
// `<Big/>` (and anything else here)
|
||||
// will be given to the `children` prop
|
||||
<Big/>
|
||||
</Show>
|
||||
}
|
||||
```
|
||||
|
||||
Let’s define a component that takes some children and a render prop.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TakesChildren<F, IV>(
|
||||
cx: Scope,
|
||||
/// Takes a function (type F) that returns anything that can be
|
||||
/// converted into a View (type IV)
|
||||
render_prop: F,
|
||||
/// `children` takes the `Children` type
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> IV,
|
||||
IV: IntoView,
|
||||
{
|
||||
view! { cx,
|
||||
<h2>"Render Prop"</h2>
|
||||
{render_prop()}
|
||||
|
||||
<h2>"Children"</h2>
|
||||
{children(cx)}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`render_prop` and `children` are both functions, so we can call them to generate
|
||||
the appropriate views. `children`, in particular, is an alias for
|
||||
`Box<dyn FnOnce(Scope) -> Fragment>`. (Aren't you glad we named it `Children` instead?)
|
||||
|
||||
> If you need a `Fn` or `FnMut` here because you need to call `children` more than once,
|
||||
> we also provide `ChildrenFn` and `ChildrenMut` aliases.
|
||||
|
||||
We can use the component like this:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
|
||||
// these get passed to `children`
|
||||
"Some text"
|
||||
<span>"A span"</span>
|
||||
</TakesChildren>
|
||||
}
|
||||
```
|
||||
|
||||
## Manipulating Children
|
||||
|
||||
The [`Fragment`](https://docs.rs/leptos/latest/leptos/struct.Fragment.html) type is
|
||||
basically a way of wrapping a `Vec<View>`. You can insert it anywhere into your view.
|
||||
|
||||
But you can also access those inner views directly to manipulate them. For example, here’s
|
||||
a component that takes its children and turns them into an unordered list.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
// Fragment has `nodes` field that contains a Vec<View>
|
||||
let children = children(cx)
|
||||
.nodes
|
||||
.into_iter()
|
||||
.map(|child| view! { cx, <li>{child}</li> })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
view! { cx,
|
||||
<ul>{children}</ul>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Calling it like this will create a list:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<WrappedChildren>
|
||||
"A"
|
||||
"B"
|
||||
"C"
|
||||
</WrappedChildren>
|
||||
}
|
||||
```
|
||||
@@ -1,16 +1,86 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counter::*;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
use counter::*;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn clear() {
|
||||
let document = leptos::document();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
|
||||
// start by rendering our counter and mounting it to the DOM
|
||||
// note that we start at the initial value of 10
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
// this would be easier if they had IDs
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = test_wrapper
|
||||
.query_selector("button")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlElement>();
|
||||
|
||||
// now let's click the `clear` button
|
||||
clear.click();
|
||||
|
||||
// now let's test the <div> against the expected value
|
||||
// we can do this by testing its `outerHTML`
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system, just to render the
|
||||
// test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
);
|
||||
|
||||
// There's actually an easier way to do this...
|
||||
// We can just test against a <SimpleCounter/> with the initial value 0
|
||||
assert_eq!(test_wrapper.inner_html(), {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> });
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> },
|
||||
);
|
||||
|
||||
// You can do testing with vanilla DOM operations
|
||||
let document = leptos::document();
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
@@ -47,4 +117,40 @@ fn inc() {
|
||||
clear.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
|
||||
|
||||
// Or you can test against a sample view!
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
run_scope(create_runtime(), |cx| {
|
||||
let (value, _) = create_signal(cx, 0);
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
.outer_html())
|
||||
);
|
||||
|
||||
inc.click();
|
||||
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// because we've clicked, it's as if the signal is starting at 1
|
||||
let (value, _) = create_signal(cx, 1);
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
.outer_html())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
broadcaster = "1"
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
@@ -25,6 +25,7 @@ leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0.0"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -194,16 +194,16 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect_throw("couldn't connect to SSE stream");
|
||||
.expect("couldn't connect to SSE stream");
|
||||
let s = create_signal_from_stream(
|
||||
cx,
|
||||
source.subscribe("message").unwrap().map(|value| {
|
||||
value
|
||||
.expect_throw("no message event")
|
||||
.expect("no message event")
|
||||
.1
|
||||
.data()
|
||||
.as_string()
|
||||
.expect_throw("expected string value")
|
||||
.expect("expected string value")
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{ev, *};
|
||||
use leptos::{ev, html::*, *};
|
||||
|
||||
pub struct Props {
|
||||
/// The starting value for the counter
|
||||
@@ -25,7 +25,9 @@ pub fn view(cx: Scope, props: Props) -> impl IntoView {
|
||||
.child((
|
||||
cx,
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value -= step))
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value -= step)
|
||||
})
|
||||
.child((cx, "-1")),
|
||||
))
|
||||
.child((
|
||||
@@ -38,7 +40,9 @@ pub fn view(cx: Scope, props: Props) -> impl IntoView {
|
||||
.child((
|
||||
cx,
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value += step))
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value += step)
|
||||
})
|
||||
.child((cx, "+1")),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::{For, ForProps};
|
||||
use leptos::{For, ForProps, *};
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
@@ -66,9 +65,8 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
<For
|
||||
each=counters
|
||||
key=|counter| counter.0
|
||||
view=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
view! {
|
||||
cx,
|
||||
view=move |cx, (id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
view! { cx,
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
@@ -85,9 +83,11 @@ fn Counter(
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap();
|
||||
|
||||
let input = move |ev| set_value(event_target_value(&ev).parse::<i32>().unwrap_or_default());
|
||||
let input = move |ev| {
|
||||
set_value(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
// just an example of how a cleanup function works
|
||||
// this will run when the scope is disposed, i.e., when this row is deleted
|
||||
|
||||
@@ -72,7 +72,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
<For
|
||||
each={move || counters.get()}
|
||||
key={|counter| counter.0}
|
||||
view=move |(id, (value, set_value))| {
|
||||
view=move |cx, (id, (value, set_value))| {
|
||||
view! {
|
||||
cx,
|
||||
<Counter id value set_value/>
|
||||
@@ -91,9 +91,12 @@ fn Counter(
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap();
|
||||
|
||||
let input = move |ev| set_value.set(event_target_value(&ev).parse::<i32>().unwrap_or_default());
|
||||
let input = move |ev| {
|
||||
set_value
|
||||
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li>
|
||||
|
||||
10
examples/error_boundary/Cargo.toml
Normal file
10
examples/error_boundary/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "error_boundary"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
9
examples/error_boundary/Makefile.toml
Normal file
9
examples/error_boundary/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
7
examples/error_boundary/README.md
Normal file
7
examples/error_boundary/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Leptos `<ErrorBoundary/>` Example
|
||||
|
||||
This example shows how to handle basic errors using Leptos.
|
||||
|
||||
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/)
|
||||
8
examples/error_boundary/index.html
Normal file
8
examples/error_boundary/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<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/error_boundary/public/favicon.ico
Normal file
BIN
examples/error_boundary/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
48
examples/error_boundary/src/lib.rs
Normal file
48
examples/error_boundary/src/lib.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, Ok(0));
|
||||
|
||||
// when input changes, try to parse a number from the input
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
|
||||
view! { cx,
|
||||
<h1>"Error Handling"</h1>
|
||||
<label>
|
||||
"Type a number (or something that's not a number!)"
|
||||
<input type="number" on:input=on_input/>
|
||||
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
|
||||
// the fallback will be displayed. Otherwise, the children of the
|
||||
// <ErrorBoundary/> will be displayed.
|
||||
<ErrorBoundary
|
||||
// the fallback receives a signal containing current errors
|
||||
fallback=|cx, errors| view! { cx,
|
||||
<div class="error">
|
||||
<p>"Not a number! Errors: "</p>
|
||||
// we can render a list of errors
|
||||
// as strings, if we'd like
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
"You entered "
|
||||
// because `value` is `Result<i32, _>`,
|
||||
// it will render the `i32` if it is `Ok`,
|
||||
// and render nothing and trigger the error boundary
|
||||
// if it is `Err`. It's a signal, so this will dynamically
|
||||
// update when `value` changes
|
||||
<strong>{value}</strong>
|
||||
</p>
|
||||
</ErrorBoundary>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
12
examples/error_boundary/src/main.rs
Normal file
12
examples/error_boundary/src/main.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use error_boundary::*;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx,
|
||||
<App/>
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -32,45 +32,49 @@ tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.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", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
"axum",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tokio",
|
||||
"leptos_axum",
|
||||
]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "errors_axum"
|
||||
output-name = "errors_axum"
|
||||
# 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.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./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-addr = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::errors::AppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::Errors;
|
||||
use leptos::*;
|
||||
|
||||
use leptos::{Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
@@ -23,14 +21,13 @@ pub fn ErrorTemplate(
|
||||
};
|
||||
|
||||
// Get Errors from Signal
|
||||
let errors = errors.get().0;
|
||||
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<AppError> = errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||
.filter_map(|(_, v)| v.downcast_ref::<AppError>().cloned())
|
||||
.collect();
|
||||
println!("Errors: {errors:#?}");
|
||||
log!("Errors: {errors:#?}");
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
@@ -47,9 +44,9 @@ pub fn ErrorTemplate(
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
key=|(index, _)| *index
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
view=move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! { cx,
|
||||
|
||||
@@ -12,8 +12,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||
use crate::errors::AppError;
|
||||
use crate::landing::{App, AppProps};
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
@@ -23,9 +22,10 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(AppError::NotFound);
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view!{cx, <ErrorTemplate outside_errors=errors.clone()/>});
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move |cx| view!{ cx, <App/> }
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,14 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
cx,
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/errors_axum.css"/>
|
||||
<Router>
|
||||
<Router fallback=|cx| {
|
||||
let mut outside_errors = Errors::default();
|
||||
outside_errors.insert_with_default_key(AppError::NotFound);
|
||||
view! { cx,
|
||||
<ErrorTemplate outside_errors/>
|
||||
}
|
||||
.into_view(cx)
|
||||
}>
|
||||
<header>
|
||||
<h1>"Error Examples:"</h1>
|
||||
</header>
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
max-width: 250px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
border: 1px solid red;
|
||||
color: red;
|
||||
background-color: lightpink;
|
||||
}
|
||||
</style>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use leptos::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -6,18 +7,18 @@ pub struct Cat {
|
||||
url: String,
|
||||
}
|
||||
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>> {
|
||||
if count > 0 {
|
||||
// make the request
|
||||
let res = reqwasm::http::Request::get(&format!(
|
||||
"https://api.thecatapi.com/v1/images/search?limit={}",
|
||||
count
|
||||
"https://api.thecatapi.com/v1/images/search?limit={count}",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| ())?
|
||||
.await?
|
||||
// convert it to JSON
|
||||
.json::<Vec<Cat>>()
|
||||
.await
|
||||
.map_err(|_| ())?
|
||||
.await?
|
||||
// extract the URL field for each cat
|
||||
.into_iter()
|
||||
.map(|cat| cat.url)
|
||||
.collect::<Vec<_>>();
|
||||
@@ -29,9 +30,45 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
|
||||
|
||||
pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
|
||||
|
||||
view! { cx,
|
||||
// we use local_resource here because
|
||||
// 1) anyhow::Result isn't serializable/deserializable
|
||||
// 2) we're not doing server-side rendering in this example anyway
|
||||
// (during SSR, create_resource will begin loading on the server and resolve on the client)
|
||||
let cats = create_local_resource(cx, cat_count, fetch_cats);
|
||||
|
||||
let fallback = move |cx, errors: RwSignal<Errors>| {
|
||||
let error_list = move || {
|
||||
errors.with(|errors| {
|
||||
errors
|
||||
.iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<div class="error">
|
||||
<h2>"Error"</h2>
|
||||
<ul>{error_list}</ul>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
// the renderer can handle Option<_> and Result<_> states
|
||||
// by displaying nothing for None if the resource is still loading
|
||||
// and by using the ErrorBoundary fallback to catch Err(_)
|
||||
// so we'll just implement our happy path and let the framework handle the rest
|
||||
let cats_view = move || {
|
||||
cats.with(|data| {
|
||||
data.iter()
|
||||
.flatten()
|
||||
.map(|cat| view! { cx, <img src={cat}/> })
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<label>
|
||||
"How many cats would you like?"
|
||||
@@ -43,25 +80,11 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
|
||||
{move || {
|
||||
cats.read().map(|data| match data {
|
||||
Err(_) => view! { cx, <pre>"Error"</pre> }.into_view(cx),
|
||||
Ok(cats) => view! { cx,
|
||||
<div>{
|
||||
cats.iter()
|
||||
.map(|src| {
|
||||
view! { cx,
|
||||
<img src={src}/>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}</div>
|
||||
}.into_view(cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
</Transition>
|
||||
<ErrorBoundary fallback>
|
||||
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
|
||||
{cats_view}
|
||||
</Transition>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,14 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = ["serde"] }
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
@@ -47,26 +49,26 @@ skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "hackernews"
|
||||
output-name = "hackernews"
|
||||
# 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.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./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-addr = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
@@ -87,4 +89,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
|
||||
|
||||
@@ -91,7 +91,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
<For
|
||||
each=move || stories.clone()
|
||||
key=|story| story.id
|
||||
view=move |story: api::Story| {
|
||||
view=move |cx, story: api::Story| {
|
||||
view! { cx,
|
||||
<Story story/>
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { cx, <Comment comment /> }
|
||||
view=move |cx, comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
|
||||
<For
|
||||
each=move || comments.clone()
|
||||
key=|comment| comment.id
|
||||
view=move |comment: api::Comment| view! { cx, <Comment comment /> }
|
||||
view=move |cx, comment: api::Comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="css" href="./static/style.css"/>
|
||||
<link data-trunk rel="css" href="/style.css"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -1,5 +1,7 @@
|
||||
use leptos::Errors;
|
||||
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
|
||||
use leptos::{
|
||||
signal_prelude::*, view, Errors, 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
|
||||
@@ -7,21 +9,22 @@ 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>
|
||||
<h1>"Errors"</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each=errors
|
||||
// a unique key for each item as a reference
|
||||
key=|(key, _)| key.clone()
|
||||
// renders each item to a view
|
||||
view= move |cx, (_, error)| {
|
||||
let error_string = error.to_string();
|
||||
view! {
|
||||
cx,
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
.into_view(cx)
|
||||
|
||||
@@ -91,7 +91,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
<For
|
||||
each=move || stories.clone()
|
||||
key=|story| story.id
|
||||
view=move |story: api::Story| {
|
||||
view=move |cx, story: api::Story| {
|
||||
view! { cx,
|
||||
<Story story/>
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { cx, <Comment comment /> }
|
||||
view=move |cx, comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
|
||||
<For
|
||||
each=move || comments.clone()
|
||||
key=|comment| comment.id
|
||||
view=move |comment: api::Comment| view! { cx, <Comment comment /> }
|
||||
view=move |cx, comment: api::Comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@ leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
web-sys = "0.3"
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
mod api;
|
||||
|
||||
use crate::api::*;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use crate::api::{get_contact, get_contacts};
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
struct ExampleContext(i32);
|
||||
|
||||
#[component]
|
||||
pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
log::debug!("rendering <RouterExample/>");
|
||||
|
||||
// contexts are passed down through the route tree
|
||||
provide_context(cx, ExampleContext(0));
|
||||
|
||||
view! { cx,
|
||||
<Router>
|
||||
<nav>
|
||||
@@ -59,6 +63,13 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
log::debug!("rendering <ContactList/>");
|
||||
|
||||
// contexts are passed down through the route tree
|
||||
provide_context(cx, ExampleContext(42));
|
||||
|
||||
on_cleanup(cx, || {
|
||||
log!("cleaning up <ContactList/>");
|
||||
});
|
||||
|
||||
let location = use_location(cx);
|
||||
let contacts = create_resource(cx, move || location.search.get(), get_contacts);
|
||||
let contacts = move || {
|
||||
@@ -86,21 +97,28 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, PartialEq, Clone, Debug)]
|
||||
pub struct ContactParams {
|
||||
id: usize,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Contact(cx: Scope) -> impl IntoView {
|
||||
log::debug!("rendering <Contact/>");
|
||||
|
||||
let params = use_params_map(cx);
|
||||
log::debug!(
|
||||
"ExampleContext should be Some(42). It is {:?}",
|
||||
use_context::<ExampleContext>(cx)
|
||||
);
|
||||
|
||||
on_cleanup(cx, || {
|
||||
log!("cleaning up <Contact/>");
|
||||
});
|
||||
|
||||
let params = use_params::<ContactParams>(cx);
|
||||
let contact = create_resource(
|
||||
cx,
|
||||
move || {
|
||||
params()
|
||||
.get("id")
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.parse::<usize>()
|
||||
.ok()
|
||||
},
|
||||
move || params().map(|params| params.id).ok(),
|
||||
// any of the following would work (they're identical)
|
||||
// move |id| async move { get_contact(id).await }
|
||||
// move |id| get_contact(id),
|
||||
@@ -138,6 +156,16 @@ pub fn Contact(cx: Scope) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn About(cx: Scope) -> impl IntoView {
|
||||
log::debug!("rendering <About/>");
|
||||
|
||||
on_cleanup(cx, || {
|
||||
log!("cleaning up <About/>");
|
||||
});
|
||||
|
||||
log::debug!(
|
||||
"ExampleContext should be Some(0). It is {:?}",
|
||||
use_context::<ExampleContext>(cx)
|
||||
);
|
||||
|
||||
// use_navigate allows you to navigate programmatically by calling a function
|
||||
let navigate = use_navigate(cx);
|
||||
|
||||
@@ -159,6 +187,11 @@ pub fn About(cx: Scope) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn Settings(cx: Scope) -> impl IntoView {
|
||||
log::debug!("rendering <Settings/>");
|
||||
|
||||
on_cleanup(cx, || {
|
||||
log!("cleaning up <Settings/>");
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<>
|
||||
<h1>"Settings"</h1>
|
||||
|
||||
13
examples/ssr_modes/.gitignore
vendored
Normal file
13
examples/ssr_modes/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
pkg
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# node e2e test tools and outputs
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
88
examples/ssr_modes/Cargo.toml
Normal file
88
examples/ssr_modes/Cargo.toml
Normal file
@@ -0,0 +1,88 @@
|
||||
[package]
|
||||
name = "ssr_modes"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "0.2"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:leptos_actix",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "ssr_modes"
|
||||
# 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.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style/main.scss"
|
||||
# Assets source dir. All files found here will be copied and synchronized to site-root.
|
||||
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
|
||||
#
|
||||
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||
assets-dir = "assets"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
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.
|
||||
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||
# This binary name can be checked in Powershell with Get-Command npx
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-dir = "end2end"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
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
|
||||
21
examples/ssr_modes/LICENSE
Normal file
21
examples/ssr_modes/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 henrik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
54
examples/ssr_modes/README.md
Normal file
54
examples/ssr_modes/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Server-Side Rendering Modes
|
||||
|
||||
This example shows the different "rendering modes" that can be used while server-side
|
||||
rendering an application:
|
||||
1. **Synchronous**: Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the client, replacing `fallback` once they're loaded.
|
||||
- *Pros*: App shell appears very quickly: great TTFB (time to first byte).
|
||||
- *Cons*: Resources load relatively slowly; you need to wait for JS + Wasm to load before even making a request.
|
||||
2. **Out-of-order streaming**: Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `Suspense` nodes.
|
||||
- *Pros*: Combines the best of **synchronous** and **`async`**, with a very fast shell and resources that begin loading on the server.
|
||||
- *Cons*: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
|
||||
3. **In-order streaming**: Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
|
||||
- *Pros*: Does not require JS for HTML to appear in correct order.
|
||||
- *Cons*: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
|
||||
of the page will not be interactive until the suspended chunks have loaded.
|
||||
4. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
|
||||
- *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
|
||||
- *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
|
||||
|
||||
## 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)
|
||||
|
||||
1. Install cargo-leptos
|
||||
```bash
|
||||
cargo install --locked cargo-leptos
|
||||
```
|
||||
2. Build the site in watch mode, recompiling on file changes
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
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 `"."`. 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
|
||||
```
|
||||
2. Build the Webassembly used to hydrate the HTML from the server
|
||||
```bash
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
3. Run the server to serve the Webassembly, JS, and HTML
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
BIN
examples/ssr_modes/assets/favicon.ico
Normal file
BIN
examples/ssr_modes/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
184
examples/ssr_modes/src/app.rs
Normal file
184
examples/ssr_modes/src/app.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use lazy_static::lazy_static;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context(cx);
|
||||
|
||||
view! { cx,
|
||||
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
|
||||
<Title text="Welcome to Leptos"/>
|
||||
|
||||
<Router>
|
||||
<main>
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
<Route path="" view=|cx| view! { cx, <HomePage/> }/>
|
||||
|
||||
// We'll load the posts with async rendering, so they can set
|
||||
// the title and metadata *after* loading the data
|
||||
<Route
|
||||
path="/post/:id"
|
||||
view=|cx| view! { cx, <Post/> }
|
||||
ssr=SsrMode::Async
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn HomePage(cx: Scope) -> impl IntoView {
|
||||
// load the posts
|
||||
let posts =
|
||||
create_resource(cx, || (), |_| async { list_post_metadata().await });
|
||||
let posts_view = move || {
|
||||
posts.with(|posts| posts
|
||||
.clone()
|
||||
.map(|posts| {
|
||||
posts.iter()
|
||||
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a></li>})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Great Blog"</h1>
|
||||
<Suspense fallback=move || view! { cx, <p>"Loading posts..."</p> }>
|
||||
<ul>{posts_view}</ul>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PostParams {
|
||||
id: usize,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Post(cx: Scope) -> impl IntoView {
|
||||
let query = use_params::<PostParams>(cx);
|
||||
let id = move || {
|
||||
query.with(|q| {
|
||||
q.as_ref().map(|q| q.id).map_err(|_| PostError::InvalidId)
|
||||
})
|
||||
};
|
||||
let post = create_resource(cx, id, |id| async move {
|
||||
match id {
|
||||
Err(e) => Err(e),
|
||||
Ok(id) => get_post(id)
|
||||
.await
|
||||
.map(|data| data.ok_or(PostError::PostNotFound))
|
||||
.map_err(|_| PostError::ServerError)
|
||||
.flatten(),
|
||||
}
|
||||
});
|
||||
|
||||
let post_view = move || {
|
||||
post.with(|post| {
|
||||
post.clone().map(|post| {
|
||||
view! { cx,
|
||||
// render content
|
||||
<h1>{&post.title}</h1>
|
||||
<p>{&post.content}</p>
|
||||
|
||||
// since we're using async rendering for this page,
|
||||
// this metadata should be included in the actual HTML <head>
|
||||
// when it's first served
|
||||
<Title text=post.title/>
|
||||
<Meta name="description" content=post.content/>
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<Suspense fallback=move || view! { cx, <p>"Loading post..."</p> }>
|
||||
<ErrorBoundary fallback=|cx, errors| {
|
||||
view! { cx,
|
||||
<div class="error">
|
||||
<h1>"Something went wrong."</h1>
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, error)| view! { cx, <li>{error.to_string()} </li> })
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}>
|
||||
{post_view}
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
// Dummy API
|
||||
lazy_static! {
|
||||
static ref POSTS: Vec<Post> = vec![
|
||||
Post {
|
||||
id: 0,
|
||||
title: "My first post".to_string(),
|
||||
content: "This is my first post".to_string(),
|
||||
},
|
||||
Post {
|
||||
id: 1,
|
||||
title: "My second post".to_string(),
|
||||
content: "This is my second post".to_string(),
|
||||
},
|
||||
Post {
|
||||
id: 2,
|
||||
title: "My third post".to_string(),
|
||||
content: "This is my third post".to_string(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PostError {
|
||||
#[error("Invalid post ID.")]
|
||||
InvalidId,
|
||||
#[error("Post not found.")]
|
||||
PostNotFound,
|
||||
#[error("Server error.")]
|
||||
ServerError,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Post {
|
||||
id: usize,
|
||||
title: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PostMetadata {
|
||||
id: usize,
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[server(ListPostMetadata, "/api")]
|
||||
pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
Ok(POSTS
|
||||
.iter()
|
||||
.map(|data| PostMetadata {
|
||||
id: data.id,
|
||||
title: data.title.clone(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[server(GetPost, "/api")]
|
||||
pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
Ok(POSTS.iter().find(|post| post.id == id).cloned())
|
||||
}
|
||||
25
examples/ssr_modes/src/lib.rs
Normal file
25
examples/ssr_modes/src/lib.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
#![feature(result_flattening)]
|
||||
|
||||
pub mod app;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
use leptos::*;
|
||||
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(move |cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
42
examples/ssr_modes/src/main.rs
Normal file
42
examples/ssr_modes/src/main.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use ssr_modes::app::*;
|
||||
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> });
|
||||
|
||||
GetPost::register();
|
||||
ListPostMetadata::register();
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||
// see lib.rs for hydration function instead
|
||||
}
|
||||
3
examples/ssr_modes/style/main.scss
Normal file
3
examples/ssr_modes/style/main.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Cargo Leptos</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!-- INJECT HEAD -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- INJECT BODY -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6.2", optional = true }
|
||||
actix-web = { version = "4.2.1", optional = true, features = ["openssl", "macros"] }
|
||||
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
|
||||
anyhow = "1.0.68"
|
||||
broadcaster = "1.0.0"
|
||||
console_log = "0.2.0"
|
||||
@@ -29,6 +29,7 @@ sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
@@ -49,26 +50,26 @@ skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "todo_app_sqlite"
|
||||
output-name = "todo_app_sqlite"
|
||||
# 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.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./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-addr = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
|
||||
@@ -36,22 +36,26 @@ sqlx = { version = "0.6.2", features = [
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.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", "dep:leptos_axum"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
"axum",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tokio",
|
||||
"sqlx",
|
||||
"leptos_axum",
|
||||
]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::errors::TodoAppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::Errors;
|
||||
use leptos::*;
|
||||
|
||||
use leptos::{Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
@@ -23,14 +21,12 @@ pub fn ErrorTemplate(
|
||||
};
|
||||
|
||||
// Get Errors from Signal
|
||||
let errors = errors.get().0;
|
||||
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<TodoAppError> = errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
|
||||
.filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().cloned())
|
||||
.collect();
|
||||
println!("Errors: {errors:#?}");
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
@@ -51,7 +47,7 @@ pub fn ErrorTemplate(
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
view= move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
|
||||
@@ -11,7 +11,7 @@ console_error_panic_hook = "0.1.7"
|
||||
uuid = { version = "1", features = ["v4", "js", "serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
web-sys = { version = "0.3", features = ["Storage"] }
|
||||
web-sys = { version = "0.3.60", features = ["Storage"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{web_sys::HtmlInputElement, *};
|
||||
use leptos::{html::Input, leptos_dom::helpers::location_hash, *};
|
||||
use storage::TodoSerialized;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -12,12 +12,15 @@ const STORAGE_KEY: &str = "todos-leptos";
|
||||
// Basic operations to manipulate the todo list: nothing really interesting here
|
||||
impl Todos {
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
let starting_todos = if let Ok(Some(storage)) = window().local_storage() {
|
||||
let starting_todos = if let Ok(Some(storage)) = window().local_storage()
|
||||
{
|
||||
storage
|
||||
.get_item(STORAGE_KEY)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|value| serde_json::from_str::<Vec<TodoSerialized>>(&value).ok())
|
||||
.and_then(|value| {
|
||||
serde_json::from_str::<Vec<TodoSerialized>>(&value).ok()
|
||||
})
|
||||
.map(|values| {
|
||||
values
|
||||
.into_iter()
|
||||
@@ -89,7 +92,12 @@ impl Todo {
|
||||
Self::new_with_completed(cx, id, title, false)
|
||||
}
|
||||
|
||||
pub fn new_with_completed(cx: Scope, id: Uuid, title: String, completed: bool) -> Self {
|
||||
pub fn new_with_completed(
|
||||
cx: Scope,
|
||||
id: Uuid,
|
||||
title: String,
|
||||
completed: bool,
|
||||
) -> Self {
|
||||
// RwSignal combines the getter and setter in one struct, rather than separating
|
||||
// the getter from the setter. This makes it more convenient in some cases, such
|
||||
// as when we're putting the signals into a struct and passing it around. There's
|
||||
@@ -129,22 +137,24 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
|
||||
// Handle the three filter modes: All, Active, and Completed
|
||||
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();
|
||||
let new_mode =
|
||||
location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
|
||||
let input_ref = NodeRef::<Input>::new(cx);
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
let input = input_ref.get().unwrap();
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
let title = event_target_value(&ev);
|
||||
let title = input.value();
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(cx, Uuid::new_v4(), title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
target.set_value("");
|
||||
input.set_value("");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -184,7 +194,8 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
|
||||
.iter()
|
||||
.map(TodoSerialized::from)
|
||||
.collect::<Vec<_>>();
|
||||
let json = serde_json::to_string(&objs).expect("couldn't serialize Todos");
|
||||
let json =
|
||||
serde_json::to_string(&objs).expect("couldn't serialize Todos");
|
||||
if storage.set_item(STORAGE_KEY, &json).is_err() {
|
||||
log::error!("error while trying to set item in localStorage");
|
||||
}
|
||||
@@ -201,6 +212,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
|
||||
placeholder="What needs to be done?"
|
||||
autofocus
|
||||
on:keydown=add_todo
|
||||
node_ref=input_ref
|
||||
/>
|
||||
</header>
|
||||
<section
|
||||
@@ -216,7 +228,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |todo: Todo| view! { cx, <Todo todo /> }
|
||||
view=move |cx, todo: Todo| view! { cx, <Todo todo /> }
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -262,7 +274,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::<HtmlElement<Input>>::new(cx);
|
||||
let todo_input = NodeRef::<Input>::new(cx);
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
use crate::Todo;
|
||||
use leptos::Scope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use leptos::{
|
||||
signal_prelude::*,
|
||||
Scope,
|
||||
};
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TodoSerialized {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub completed: bool,
|
||||
pub id: Uuid,
|
||||
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.get(),
|
||||
}
|
||||
fn from(todo: &Todo) -> Self {
|
||||
Self {
|
||||
id: todo.id,
|
||||
title: todo.title.get(),
|
||||
completed: todo.completed.get(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ futures = "0.3"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
parking_lot = "0.12.1"
|
||||
regex = "1.7.0"
|
||||
|
||||
@@ -13,9 +13,14 @@ use actix_web::{
|
||||
web::Bytes,
|
||||
*,
|
||||
};
|
||||
use futures::{Future, StreamExt};
|
||||
use futures::{Future, Stream, StreamExt};
|
||||
use http::StatusCode;
|
||||
use leptos::*;
|
||||
use leptos::{
|
||||
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
*,
|
||||
};
|
||||
use leptos_integration_utils::{build_async_response, html_parts};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
@@ -32,11 +37,19 @@ pub struct ResponseParts {
|
||||
|
||||
impl ResponseParts {
|
||||
/// Insert a header, overwriting any previous value with the same key
|
||||
pub fn insert_header(&mut self, key: header::HeaderName, value: header::HeaderValue) {
|
||||
pub fn insert_header(
|
||||
&mut self,
|
||||
key: header::HeaderName,
|
||||
value: header::HeaderValue,
|
||||
) {
|
||||
self.headers.insert(key, value);
|
||||
}
|
||||
/// Append a header, leaving any header with the same key intact
|
||||
pub fn append_header(&mut self, key: header::HeaderName, value: header::HeaderValue) {
|
||||
pub fn append_header(
|
||||
&mut self,
|
||||
key: header::HeaderName,
|
||||
value: header::HeaderValue,
|
||||
) {
|
||||
self.headers.append(key, value);
|
||||
}
|
||||
}
|
||||
@@ -60,13 +73,21 @@ impl ResponseOptions {
|
||||
res_parts.status = Some(status);
|
||||
}
|
||||
/// Insert a header, overwriting any previous value with the same key
|
||||
pub fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue) {
|
||||
pub fn insert_header(
|
||||
&self,
|
||||
key: header::HeaderName,
|
||||
value: header::HeaderValue,
|
||||
) {
|
||||
let mut writeable = self.0.write();
|
||||
let res_parts = &mut *writeable;
|
||||
res_parts.headers.insert(key, value);
|
||||
}
|
||||
/// Append a header, leaving any header with the same key intact
|
||||
pub fn append_header(&self, key: header::HeaderName, value: header::HeaderValue) {
|
||||
pub fn append_header(
|
||||
&self,
|
||||
key: header::HeaderName,
|
||||
value: header::HeaderValue,
|
||||
) {
|
||||
let mut writeable = self.0.write();
|
||||
let res_parts = &mut *writeable;
|
||||
res_parts.headers.append(key, value);
|
||||
@@ -77,12 +98,14 @@ impl ResponseOptions {
|
||||
/// 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 fn redirect(cx: leptos::Scope, path: &str) {
|
||||
let response_options = use_context::<ResponseOptions>(cx).unwrap();
|
||||
response_options.set_status(StatusCode::FOUND);
|
||||
response_options.insert_header(
|
||||
header::LOCATION,
|
||||
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
|
||||
);
|
||||
if let Some(response_options) = use_context::<ResponseOptions>(cx) {
|
||||
response_options.set_status(StatusCode::FOUND);
|
||||
response_options.insert_header(
|
||||
header::LOCATION,
|
||||
header::HeaderValue::from_str(path)
|
||||
.expect("Failed to create HeaderValue"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
|
||||
@@ -173,7 +196,8 @@ pub fn handle_server_fns_with_context(
|
||||
|
||||
match server_fn(cx, body).await {
|
||||
Ok(serialized) => {
|
||||
let res_options = use_context::<ResponseOptions>(cx).unwrap();
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
@@ -183,7 +207,8 @@ pub fn handle_server_fns_with_context(
|
||||
let mut res_parts = res_options.0.write();
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header == Some("application/x-www-form-urlencoded")
|
||||
|| accept_header
|
||||
== Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
res = HttpResponse::Ok();
|
||||
@@ -221,7 +246,9 @@ pub fn handle_server_fns_with_context(
|
||||
res.body(Bytes::from(data))
|
||||
}
|
||||
Payload::Url(data) => {
|
||||
res.content_type("application/x-www-form-urlencoded");
|
||||
res.content_type(
|
||||
"application/x-www-form-urlencoded",
|
||||
);
|
||||
res.body(data)
|
||||
}
|
||||
Payload::Json(data) => {
|
||||
@@ -230,13 +257,15 @@ pub fn handle_server_fns_with_context(
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
Err(e) => HttpResponse::InternalServerError()
|
||||
.body(e.to_string()),
|
||||
}
|
||||
} else {
|
||||
HttpResponse::BadRequest().body(format!(
|
||||
"Could not find a server function at the route {:?}. \
|
||||
\n\nIt's likely that you need to call ServerFn::register() on the \
|
||||
server function type, somewhere in your `main` function.",
|
||||
\n\nIt's likely that you need to call \
|
||||
ServerFn::register() on the server function type, \
|
||||
somewhere in your `main` function.",
|
||||
req.path()
|
||||
))
|
||||
}
|
||||
@@ -246,23 +275,25 @@ pub fn handle_server_fns_with_context(
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application. The stream
|
||||
/// will include fallback content for any `<Suspense/>` nodes, and be immediately interactive,
|
||||
/// but requires some client-side JavaScript.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
/// rendering it, and includes any meta tags injected using [leptos_meta].
|
||||
///
|
||||
/// The HTML stream is rendered using [render_to_stream], and includes everything described in
|
||||
/// the documentation for that function.
|
||||
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
|
||||
/// includes everything described in the documentation for that function.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{HttpServer, App};
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use std::{env,net::SocketAddr};
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
@@ -272,11 +303,17 @@ pub fn handle_server_fns_with_context(
|
||||
/// let addr = conf.leptos_options.site_addr.clone();
|
||||
/// HttpServer::new(move || {
|
||||
/// let leptos_options = &conf.leptos_options;
|
||||
///
|
||||
///
|
||||
/// App::new()
|
||||
/// // {tail:.*} passes the remainder of the URL as the route
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream(leptos_options.to_owned(), |cx| view! { cx, <MyApp/> }))
|
||||
/// .route(
|
||||
/// "/{tail:.*}",
|
||||
/// leptos_actix::render_app_to_stream(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |cx| view! { cx, <MyApp/> },
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
/// .bind(&addr)?
|
||||
/// .run()
|
||||
@@ -301,6 +338,133 @@ where
|
||||
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 in-order HTML stream of your application.
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
/// rendering it, and includes any meta tags injected using [leptos_meta].
|
||||
///
|
||||
/// The HTML stream is rendered using [render_to_stream_in_order], and includes everything described in
|
||||
/// the documentation for that function.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
/// let addr = conf.leptos_options.site_addr.clone();
|
||||
/// HttpServer::new(move || {
|
||||
/// let leptos_options = &conf.leptos_options;
|
||||
///
|
||||
/// App::new()
|
||||
/// // {tail:.*} passes the remainder of the URL as the route
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route(
|
||||
/// "/{tail:.*}",
|
||||
/// leptos_actix::render_app_to_stream_in_order(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |cx| view! { cx, <MyApp/> },
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
/// .bind(&addr)?
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## 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_in_order<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_to_stream_in_order_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], asynchronously rendering an HTML page after all
|
||||
/// `async` [Resource](leptos::Resource)s have loaded.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to the app’s context before
|
||||
/// rendering it, and includes any meta tags injected using [leptos_meta].
|
||||
///
|
||||
/// The HTML stream is rendered using [render_to_string_async], and includes everything described in
|
||||
/// the documentation for that function.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
/// let addr = conf.leptos_options.site_addr.clone();
|
||||
/// HttpServer::new(move || {
|
||||
/// let leptos_options = &conf.leptos_options;
|
||||
///
|
||||
/// App::new()
|
||||
/// // {tail:.*} passes the remainder of the URL as the route
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route(
|
||||
/// "/{tail:.*}",
|
||||
/// leptos_actix::render_app_async(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |cx| view! { cx, <MyApp/> },
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
/// .bind(&addr)?
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## 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_async<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_async_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.
|
||||
///
|
||||
@@ -342,25 +506,115 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an in-order 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_in_order_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 {
|
||||
let app = {
|
||||
let app_fn = app_fn.clone();
|
||||
let res_options = res_options.clone();
|
||||
move |cx| {
|
||||
provide_contexts(cx, &req, res_options);
|
||||
(app_fn)(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
stream_app_in_order(&options, app, res_options, additional_context)
|
||||
.await
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], asynchronously serving the page once all `async`
|
||||
/// [Resource](leptos::Resource)s have loaded.
|
||||
///
|
||||
/// 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_async_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 {
|
||||
let app = {
|
||||
let app_fn = app_fn.clone();
|
||||
let res_options = res_options.clone();
|
||||
move |cx| {
|
||||
provide_contexts(cx, &req, res_options);
|
||||
(app_fn)(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
render_app_async_helper(
|
||||
&options,
|
||||
app,
|
||||
res_options,
|
||||
additional_context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
/// rendering it, and includes any meta tags injected using [leptos_meta].
|
||||
///
|
||||
/// The HTML stream is rendered using [render_to_stream], and includes everything described in
|
||||
/// the documentation for that function.
|
||||
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
|
||||
/// includes everything described in the documentation for that function.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{HttpServer, App};
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use std::{env,net::SocketAddr};
|
||||
/// use leptos_actix::DataResponse;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope, data: &'static str) -> impl IntoView {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
@@ -370,14 +624,21 @@ where
|
||||
/// let addr = conf.leptos_options.site_addr.clone();
|
||||
/// HttpServer::new(move || {
|
||||
/// let leptos_options = &conf.leptos_options;
|
||||
///
|
||||
///
|
||||
/// App::new()
|
||||
/// // {tail:.*} passes the remainder of the URL as the route
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route("/{tail:.*}", leptos_actix::render_preloaded_data_app(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |req| async move { Ok(DataResponse::Data("async func that can preload data")) },
|
||||
/// |cx, data| view! { cx, <MyApp data/> })
|
||||
/// .route(
|
||||
/// "/{tail:.*}",
|
||||
/// leptos_actix::render_preloaded_data_app(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |req| async move {
|
||||
/// Ok(DataResponse::Data(
|
||||
/// "async func that can preload data",
|
||||
/// ))
|
||||
/// },
|
||||
/// |cx, data| view! { cx, <MyApp data/> },
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
/// .bind(&addr)?
|
||||
@@ -393,6 +654,9 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[deprecated = "You can now use `render_app_async` with `create_resource` and \
|
||||
`<Suspense/>` to achieve async rendering without manually \
|
||||
preloading data."]
|
||||
pub fn render_preloaded_data_app<Data, Fut, IV>(
|
||||
options: LeptosOptions,
|
||||
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
|
||||
@@ -430,7 +694,11 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn provide_contexts(cx: leptos::Scope, req: &HttpRequest, res_options: ResponseOptions) {
|
||||
fn provide_contexts(
|
||||
cx: leptos::Scope,
|
||||
req: &HttpRequest,
|
||||
res_options: ResponseOptions,
|
||||
) {
|
||||
let path = leptos_corrected_path(req);
|
||||
|
||||
let integration = ServerIntegration { path };
|
||||
@@ -457,25 +725,44 @@ async fn stream_app(
|
||||
res_options: ResponseOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
) -> HttpResponse<BoxBody> {
|
||||
let (stream, runtime, scope) = render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| {
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
let head = meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
let body_meta = meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.body.as_string())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body{body_meta}>").into()
|
||||
},
|
||||
additional_context,
|
||||
);
|
||||
let (stream, runtime, scope) =
|
||||
render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| generate_head_metadata(cx).into(),
|
||||
additional_context,
|
||||
);
|
||||
|
||||
build_stream_response(options, res_options, stream, runtime, scope).await
|
||||
}
|
||||
|
||||
async fn stream_app_in_order(
|
||||
options: &LeptosOptions,
|
||||
app: impl FnOnce(leptos::Scope) -> View + 'static,
|
||||
res_options: ResponseOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
) -> HttpResponse<BoxBody> {
|
||||
let (stream, runtime, scope) =
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| {
|
||||
generate_head_metadata(cx).into()
|
||||
},
|
||||
additional_context,
|
||||
);
|
||||
|
||||
build_stream_response(options, res_options, stream, runtime, scope).await
|
||||
}
|
||||
|
||||
async fn build_stream_response(
|
||||
options: &LeptosOptions,
|
||||
res_options: ResponseOptions,
|
||||
stream: impl Stream<Item = String> + 'static,
|
||||
runtime: RuntimeId,
|
||||
scope: ScopeId,
|
||||
) -> HttpResponse {
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let (head, tail) = html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
let (head, tail) =
|
||||
html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
let mut stream = Box::pin(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
@@ -493,11 +780,13 @@ async fn stream_app(
|
||||
|
||||
let res_options = res_options.0.read();
|
||||
|
||||
let (status, mut headers) = (res_options.status, res_options.headers.clone());
|
||||
let (status, mut headers) =
|
||||
(res_options.status, res_options.headers.clone());
|
||||
let status = status.unwrap_or_default();
|
||||
|
||||
let complete_stream =
|
||||
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()]).chain(stream);
|
||||
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
|
||||
.chain(stream);
|
||||
let mut res = HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.streaming(complete_stream);
|
||||
@@ -514,71 +803,48 @@ async fn stream_app(
|
||||
res
|
||||
}
|
||||
|
||||
fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (String, String) {
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
let output_name = &options.output_name;
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
|
||||
wasm_output_name.push_str("_bg");
|
||||
async fn render_app_async_helper(
|
||||
options: &LeptosOptions,
|
||||
app: impl FnOnce(leptos::Scope) -> View + 'static,
|
||||
res_options: ResponseOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
) -> HttpResponse<BoxBody> {
|
||||
let (stream, runtime, scope) =
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |_| "".into(),
|
||||
additional_context,
|
||||
);
|
||||
|
||||
let html = build_async_response(stream, options, runtime, scope).await;
|
||||
|
||||
let res_options = res_options.0.read();
|
||||
|
||||
let (status, mut headers) =
|
||||
(res_options.status, res_options.headers.clone());
|
||||
let status = status.unwrap_or_default();
|
||||
|
||||
let mut res = HttpResponse::Ok().content_type("text/html").body(html);
|
||||
|
||||
// Add headers manipulated in the response
|
||||
for (key, value) in headers.drain() {
|
||||
if let Some(key) = key {
|
||||
res.headers_mut().append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let site_ip = &options.site_addr.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
|
||||
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
let msg = JSON.parse(ev.data);
|
||||
if (msg.all) window.location.reload();
|
||||
if (msg.css) {{
|
||||
const link = document.querySelector("link#leptos");
|
||||
if (link) {{
|
||||
let href = link.getAttribute('href').split('?')[0];
|
||||
let newHref = href + '?version=' + new Date().getMilliseconds();
|
||||
link.setAttribute('href', newHref);
|
||||
}} else {{
|
||||
console.warn("Could not find link#leptos");
|
||||
}}
|
||||
}};
|
||||
}};
|
||||
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
|
||||
}})()
|
||||
</script>
|
||||
"#
|
||||
),
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let html_metadata = meta_context
|
||||
.and_then(|mc| mc.html.as_string())
|
||||
.unwrap_or_default();
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html{html_metadata}>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
let tail = "</body></html>".to_string();
|
||||
|
||||
(head, tail)
|
||||
// Set status to what is returned in the function
|
||||
let res_status = res.status_mut();
|
||||
*res_status = status;
|
||||
// Return the response
|
||||
res
|
||||
}
|
||||
|
||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
|
||||
pub fn generate_route_list<IV>(app_fn: impl FnOnce(leptos::Scope) -> IV + 'static) -> Vec<String>
|
||||
pub fn generate_route_list<IV>(
|
||||
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
|
||||
) -> Vec<(String, SsrMode)>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -586,12 +852,12 @@ where
|
||||
|
||||
// Empty strings screw with Actix pathing, they need to be "/"
|
||||
routes = routes
|
||||
.iter()
|
||||
.map(|s| {
|
||||
.into_iter()
|
||||
.map(|(s, mode)| {
|
||||
if s.is_empty() {
|
||||
return "/".to_string();
|
||||
return ("/".to_string(), mode);
|
||||
}
|
||||
s.to_string()
|
||||
(s, mode)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -601,14 +867,14 @@ where
|
||||
// Match `:some_word` but only capture `some_word` in the groups to replace with `{some_word}`
|
||||
let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap();
|
||||
|
||||
let routes: Vec<String> = routes
|
||||
.iter()
|
||||
.map(|s| wildcard_re.replace_all(s, "{tail:.*}").to_string())
|
||||
.map(|s| capture_re.replace_all(&s, "{$1}").to_string())
|
||||
let routes: Vec<(String, SsrMode)> = routes
|
||||
.into_iter()
|
||||
.map(|(s, m)| (wildcard_re.replace_all(&s, "{tail:.*}").to_string(), m))
|
||||
.map(|(s, m)| (capture_re.replace_all(&s, "{$1}").to_string(), m))
|
||||
.collect();
|
||||
|
||||
if routes.is_empty() {
|
||||
vec!["/".to_string()]
|
||||
vec![("/".to_string(), Default::default())]
|
||||
} else {
|
||||
routes
|
||||
}
|
||||
@@ -619,18 +885,22 @@ pub enum DataResponse<T> {
|
||||
Response(actix_web::dev::Response<BoxBody>),
|
||||
}
|
||||
|
||||
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
|
||||
/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid
|
||||
/// having to use wildcards or manually define all routes in multiple places.
|
||||
pub trait LeptosRoutes {
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<String>,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
|
||||
#[deprecated = "You can now use `leptos_routes` and a `<Route \
|
||||
mode=SsrMode::Async/>`
|
||||
to achieve async rendering without manually preloading \
|
||||
data."]
|
||||
fn leptos_preloaded_data_routes<Data, Fut, IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
@@ -646,7 +916,7 @@ pub trait LeptosRoutes {
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<String>,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -658,22 +928,23 @@ pub trait LeptosRoutes {
|
||||
/// to those paths to Leptos's renderer.
|
||||
impl<T> LeptosRoutes for actix_web::App<T>
|
||||
where
|
||||
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<String>,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
for path in paths.iter() {
|
||||
router = router.route(path, render_app_to_stream(options.clone(), app_fn.clone()));
|
||||
}
|
||||
router
|
||||
self.leptos_routes_with_context(options, paths, |_| {}, app_fn)
|
||||
}
|
||||
|
||||
fn leptos_preloaded_data_routes<Data, Fut, IV>(
|
||||
@@ -693,7 +964,12 @@ where
|
||||
for path in paths.iter() {
|
||||
router = router.route(
|
||||
path,
|
||||
render_preloaded_data_app(options.clone(), data_fn.clone(), app_fn.clone()),
|
||||
#[allow(deprecated)]
|
||||
render_preloaded_data_app(
|
||||
options.clone(),
|
||||
data_fn.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
router
|
||||
@@ -702,7 +978,7 @@ where
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<String>,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -710,14 +986,28 @@ where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
for path in paths.iter() {
|
||||
for (path, mode) in paths.iter() {
|
||||
router = router.route(
|
||||
path,
|
||||
render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
match mode {
|
||||
SsrMode::OutOfOrder => render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
SsrMode::InOrder => {
|
||||
render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
)
|
||||
}
|
||||
SsrMode::Async => render_app_async_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
router
|
||||
|
||||
@@ -16,5 +16,6 @@ leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_config = { workspace = true }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
15
integrations/utils/Cargo.toml
Normal file
15
integrations/utils/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "leptos_integration_utils"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Utilities to help build server integrations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_config = { workspace = true }
|
||||
100
integrations/utils/src/lib.rs
Normal file
100
integrations/utils/src/lib.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use futures::{Stream, StreamExt};
|
||||
use leptos::{use_context, RuntimeId, ScopeId};
|
||||
use leptos_config::LeptosOptions;
|
||||
use leptos_meta::MetaContext;
|
||||
|
||||
pub fn html_parts(
|
||||
options: &LeptosOptions,
|
||||
meta: Option<&MetaContext>,
|
||||
) -> (String, &'static str) {
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let output_name = &options.output_name;
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let site_ip = &options.site_addr.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
|
||||
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
let msg = JSON.parse(ev.data);
|
||||
if (msg.all) window.location.reload();
|
||||
if (msg.css) {{
|
||||
let found = false;
|
||||
document.querySelectorAll("link").forEach((link) => {{
|
||||
if (link.getAttribute('href').includes(msg.css)) {{
|
||||
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
|
||||
link.setAttribute('href', newHref);
|
||||
found = true;
|
||||
}}
|
||||
}});
|
||||
if (!found) console.warn(`CSS hot-reload: Could not find a <link href=/\"${{msg.css}}\"> element`);
|
||||
}};
|
||||
}};
|
||||
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
|
||||
}})()
|
||||
</script>
|
||||
"#
|
||||
),
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let html_metadata =
|
||||
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html{html_metadata}>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
let tail = "</body></html>";
|
||||
(head, tail)
|
||||
}
|
||||
|
||||
pub async fn build_async_response(
|
||||
stream: impl Stream<Item = String> + 'static,
|
||||
options: &LeptosOptions,
|
||||
runtime: RuntimeId,
|
||||
scope: ScopeId,
|
||||
) -> String {
|
||||
let mut buf = String::new();
|
||||
let mut stream = Box::pin(stream);
|
||||
while let Some(chunk) = stream.next().await {
|
||||
buf.push_str(&chunk);
|
||||
}
|
||||
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
// in async, we load the meta content *now*, after the suspenses have resolved
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
let head_meta = meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
let body_meta = meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.body.as_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
runtime.dispose();
|
||||
|
||||
format!("{head}{head_meta}</head><body{body_meta}>{buf}{tail}")
|
||||
}
|
||||
21
leptos/Makefile.toml
Normal file
21
leptos/Makefile.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[tasks.build-wasm]
|
||||
clear = true
|
||||
dependencies = ["build-hydrate", "build-csr"]
|
||||
|
||||
[tasks.build-hydrate]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"build",
|
||||
"--no-default-features",
|
||||
"--features=hydrate",
|
||||
"--target=wasm32-unknown-unknown",
|
||||
]
|
||||
|
||||
[tasks.build-csr]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"build",
|
||||
"--no-default-features",
|
||||
"--features=csr",
|
||||
"--target=wasm32-unknown-unknown",
|
||||
]
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::Children;
|
||||
use leptos_dom::{Errors, IntoView};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
|
||||
use leptos_macro::{component, view};
|
||||
use leptos_reactive::{
|
||||
create_rw_signal, provide_context, signal_prelude::*, RwSignal, Scope,
|
||||
};
|
||||
|
||||
/// When you render a `Result<_, _>` in your view, in the `Err` case it will
|
||||
/// render nothing, and search up through the view tree for an `<ErrorBoundary/>`.
|
||||
@@ -45,8 +47,16 @@ where
|
||||
// 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, errors).into_view(cx),
|
||||
move || {
|
||||
match errors.with(Errors::is_empty) {
|
||||
true => children.clone().into_view(cx),
|
||||
false => view! { cx,
|
||||
<>
|
||||
{fallback(cx, errors)}
|
||||
<leptos-error-boundary style="display: none">{children.clone()}</leptos-error-boundary>
|
||||
</>
|
||||
}
|
||||
.into_view(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ use std::hash::Hash;
|
||||
/// // a unique key for each item
|
||||
/// key=|counter| counter.id
|
||||
/// // renders each item to a view
|
||||
/// view=move |counter: Counter| {
|
||||
/// view=move |cx, counter: Counter| {
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <button>"Value: " {move || counter.count.get()}</button>
|
||||
@@ -54,7 +54,7 @@ pub fn For<IF, I, T, EF, N, KF, K>(
|
||||
where
|
||||
IF: Fn() -> I + 'static,
|
||||
I: IntoIterator<Item = T>,
|
||||
EF: Fn(T) -> N + 'static,
|
||||
EF: Fn(Scope, T) -> N + 'static,
|
||||
N: IntoView,
|
||||
KF: Fn(&T) -> K + 'static,
|
||||
K: Eq + Hash + 'static,
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
|
||||
//! - [`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.
|
||||
//! - [`error_boundary`](https://github.com/leptos-rs/leptos/tree/main/examples/error_boundary) shows how to use
|
||||
//! `Result` types to handle errors.
|
||||
//! - [`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/leptos-rs/leptos/tree/main/examples/todomvc) implements the classic to-do
|
||||
@@ -130,7 +132,7 @@
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
//! todo!()
|
||||
//! todo!()
|
||||
//! }
|
||||
//!
|
||||
//! pub fn main() {
|
||||
@@ -139,16 +141,32 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub use leptos_config::*;
|
||||
pub use leptos_dom;
|
||||
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
pub use leptos_dom::*;
|
||||
pub use leptos_config::{self, get_configuration, LeptosOptions};
|
||||
#[cfg(not(all(
|
||||
target_arch = "wasm32",
|
||||
any(feature = "csr", feature = "hydrate")
|
||||
)))]
|
||||
/// Utilities for server-side rendering HTML.
|
||||
pub mod ssr {
|
||||
pub use leptos_dom::{ssr::*, ssr_in_order::*};
|
||||
}
|
||||
pub use leptos_dom::{
|
||||
self, create_node_ref, debug_warn, document, error, ev,
|
||||
helpers::{
|
||||
event_target, event_target_checked, event_target_value,
|
||||
request_animation_frame, request_idle_callback, set_interval,
|
||||
set_timeout, window_event_listener,
|
||||
},
|
||||
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
|
||||
Class, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,
|
||||
IntoProperty, IntoView, NodeRef, Property, View,
|
||||
};
|
||||
pub use leptos_macro::*;
|
||||
pub use leptos_reactive::*;
|
||||
pub use leptos_server;
|
||||
pub use leptos_server::*;
|
||||
|
||||
pub use tracing;
|
||||
pub use leptos_server::{
|
||||
self, create_action, create_multi_action, create_server_action,
|
||||
create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError,
|
||||
};
|
||||
pub use typed_builder;
|
||||
mod error_boundary;
|
||||
pub use error_boundary::*;
|
||||
@@ -159,10 +177,11 @@ pub use show::*;
|
||||
mod suspense;
|
||||
pub use suspense::*;
|
||||
mod transition;
|
||||
#[cfg(debug_assertions)]
|
||||
#[doc(hidden)]
|
||||
pub use tracing;
|
||||
pub use transition::*;
|
||||
|
||||
pub use leptos_reactive::debug_warn;
|
||||
|
||||
extern crate self as leptos;
|
||||
|
||||
/// The most common type for the `children` property on components,
|
||||
@@ -176,3 +195,22 @@ pub type ChildrenFn = Box<dyn Fn(Scope) -> Fragment>;
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once, but may mutate the children.
|
||||
pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
|
||||
|
||||
/// A type for taking anything that implements [`IntoAttribute`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyHeading(
|
||||
/// cx: Scope,
|
||||
/// text: String,
|
||||
/// #[prop(optional, into)] class: Option<AttributeValue>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <h1 class=class>{text}</h1>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub type AttributeValue = Box<dyn IntoAttribute>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use leptos::component;
|
||||
use leptos_dom::{Fragment, IntoView};
|
||||
use leptos_reactive::{create_memo, Scope};
|
||||
use leptos_reactive::{create_memo, signal_prelude::*, Scope};
|
||||
|
||||
/// A component that will show its children when the `when` condition is `true`,
|
||||
/// and show the fallback when it is `false`, without rerendering every time
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_dom::HydrationCtx;
|
||||
use leptos_dom::{DynChild, Fragment, IntoView};
|
||||
use leptos_dom::{DynChild, Fragment, HydrationCtx, IntoView};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{provide_context, Scope, SuspenseContext};
|
||||
use std::rc::Rc;
|
||||
@@ -63,8 +62,6 @@ where
|
||||
F: Fn() -> E + 'static,
|
||||
E: IntoView,
|
||||
{
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
let id_before_suspense = HydrationCtx::peek();
|
||||
let context = SuspenseContext::new(cx);
|
||||
|
||||
// provide this SuspenseContext to any resources below it
|
||||
@@ -86,13 +83,16 @@ where
|
||||
fallback().into_view(cx)
|
||||
}
|
||||
} else {
|
||||
use leptos_reactive::signal_prelude::*;
|
||||
|
||||
// run the child; we'll probably throw this away, but it will register resource reads
|
||||
let child = orig_child(cx).into_view(cx);
|
||||
let after_original_child = HydrationCtx::id();
|
||||
|
||||
let initial = {
|
||||
// no resources were read under this, so just return the child
|
||||
if context.pending_resources.get() == 0 {
|
||||
child.clone()
|
||||
child
|
||||
}
|
||||
// show the fallback, but also prepare to stream HTML
|
||||
else {
|
||||
@@ -100,10 +100,11 @@ where
|
||||
|
||||
cx.register_suspense(
|
||||
context,
|
||||
&id_before_suspense.to_string(),
|
||||
¤t_id.to_string(),
|
||||
// out-of-order streaming
|
||||
{
|
||||
let current_id = current_id.clone();
|
||||
let orig_child = Rc::clone(&orig_child);
|
||||
move || {
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
DynChild::new(move || orig_child(cx))
|
||||
@@ -111,6 +112,16 @@ where
|
||||
.render_to_string(cx)
|
||||
.to_string()
|
||||
}
|
||||
},
|
||||
// in-order streaming
|
||||
{
|
||||
let current_id = current_id.clone();
|
||||
move || {
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
DynChild::new(move || orig_child(cx))
|
||||
.into_view(cx)
|
||||
.into_stream_chunks(cx)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -119,8 +130,7 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
|
||||
HydrationCtx::continue_from(after_original_child);
|
||||
initial
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ use std::{cell::RefCell, rc::Rc};
|
||||
/// # use leptos::*;
|
||||
/// # if false {
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// async fn fetch_cats(how_many: u32) -> Option<Vec<String>> { Some(vec![]) }
|
||||
/// async fn fetch_cats(how_many: u32) -> Option<Vec<String>> {
|
||||
/// Some(vec![])
|
||||
/// }
|
||||
///
|
||||
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
/// let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
@@ -16,7 +16,11 @@ 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: <!--hk=_0-4o|leptos-dyn-child-start-->0<!--hk=_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 +54,21 @@ fn ssr_test_with_components() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<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>"
|
||||
"<div class=\"counters\" \
|
||||
id=\"_0-1\"><!--hk=_0-1-0o|leptos-counter-start--><div \
|
||||
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
id=\"_0-1-3\">Value: \
|
||||
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
|
||||
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5\">+1</button></div><!\
|
||||
--hk=_0-1-0c|leptos-counter-end--><!\
|
||||
--hk=_0-1-5-0o|leptos-counter-start--><div \
|
||||
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
|
||||
id=\"_0-1-5-3\">Value: \
|
||||
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
|
||||
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5-5\">+1</button></div><!\
|
||||
--hk=_0-1-5-0c|leptos-counter-end--></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -84,7 +102,22 @@ fn ssr_test_with_snake_case_components() {
|
||||
|
||||
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>"
|
||||
"<div class=\"counters\" \
|
||||
id=\"_0-1\"><!\
|
||||
--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>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -103,7 +136,7 @@ fn test_classes() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<div id=\"_0-1\" class=\"my big red car\"></div>"
|
||||
"<div class=\"my big red car\" id=\"_0-1\"></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -114,7 +147,7 @@ fn ssr_with_styles() {
|
||||
use leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let (_, set_value) = create_signal(cx, 0);
|
||||
let styles = "myclass";
|
||||
let rendered = view! {
|
||||
cx, class = styles,
|
||||
@@ -125,7 +158,8 @@ fn ssr_with_styles() {
|
||||
|
||||
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>"
|
||||
"<div class=\"myclass\" id=\"_0-1\"><button class=\"btn myclass\" \
|
||||
id=\"_0-2\">-1</button></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -136,7 +170,7 @@ fn ssr_option() {
|
||||
use leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let (_, _) = create_signal(cx, 0);
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<option/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::{net::AddrParseError, num::ParseIntError};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, Clone)]
|
||||
|
||||
@@ -5,9 +5,7 @@ pub mod errors;
|
||||
use crate::errors::LeptosConfigError;
|
||||
use config::{Config, File, FileFormat};
|
||||
use regex::Regex;
|
||||
use std::convert::TryFrom;
|
||||
use std::fs;
|
||||
use std::{env::VarError, net::SocketAddr, str::FromStr};
|
||||
use std::{convert::TryFrom, env::VarError, fs, net::SocketAddr, str::FromStr};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
@@ -53,18 +51,26 @@ pub struct LeptosOptions {
|
||||
impl LeptosOptions {
|
||||
fn try_from_env() -> Result<Self, LeptosConfigError> {
|
||||
Ok(LeptosOptions {
|
||||
output_name: std::env::var("LEPTOS_OUTPUT_NAME")
|
||||
.map_err(|e| LeptosConfigError::EnvVarError(format!("LEPTOS_OUTPUT_NAME: {e}")))?,
|
||||
output_name: std::env::var("LEPTOS_OUTPUT_NAME").map_err(|e| {
|
||||
LeptosConfigError::EnvVarError(format!(
|
||||
"LEPTOS_OUTPUT_NAME: {e}"
|
||||
))
|
||||
})?,
|
||||
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
|
||||
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
|
||||
env: Env::default(),
|
||||
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?.parse()?,
|
||||
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?.parse()?,
|
||||
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
|
||||
.parse()?,
|
||||
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?
|
||||
.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn env_w_default(key: &str, default: &str) -> Result<String, LeptosConfigError> {
|
||||
fn env_w_default(
|
||||
key: &str,
|
||||
default: &str,
|
||||
) -> Result<String, LeptosConfigError> {
|
||||
match std::env::var(key) {
|
||||
Ok(val) => Ok(val),
|
||||
Err(VarError::NotPresent) => Ok(default.to_string()),
|
||||
@@ -93,7 +99,8 @@ fn from_str(input: &str) -> Result<Env, String> {
|
||||
"dev" | "development" => Ok(Env::DEV),
|
||||
"prod" | "production" => Ok(Env::PROD),
|
||||
_ => Err(format!(
|
||||
"{input} is not a supported environment. Use either `dev` or `production`.",
|
||||
"{input} is not a supported environment. Use either `dev` or \
|
||||
`production`.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -132,11 +139,15 @@ impl TryFrom<String> for Env {
|
||||
/// 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> {
|
||||
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)?;
|
||||
let text = fs::read_to_string(path)
|
||||
.map_err(|_| LeptosConfigError::ConfigNotFound)?;
|
||||
|
||||
let re: Regex = Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
|
||||
let re: Regex =
|
||||
Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
|
||||
let start = match re.find(&text) {
|
||||
Some(found) => found.start(),
|
||||
None => return Err(LeptosConfigError::ConfigSectionNotFound),
|
||||
@@ -154,7 +165,9 @@ pub async fn get_configuration(path: Option<&str>) -> Result<ConfFile, LeptosCon
|
||||
// Layer on the environment-specific values.
|
||||
// Add in settings from environment variables (with a prefix of LEPTOS and '_' as separator)
|
||||
// E.g. `LEPTOS_RELOAD_PORT=5001 would set `LeptosOptions.reload_port`
|
||||
.add_source(config::Environment::with_prefix("LEPTOS").separator("_"))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("LEPTOS").separator("_"),
|
||||
)
|
||||
.build()?;
|
||||
|
||||
settings
|
||||
|
||||
@@ -8,6 +8,7 @@ repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "DOM operations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
async-recursion = "1"
|
||||
cfg-if = "1"
|
||||
drain_filter_polyfill = "0.1"
|
||||
educe = "0.4"
|
||||
|
||||
@@ -32,7 +32,7 @@ fn view_fn(cx: Scope) -> impl IntoView {
|
||||
<For
|
||||
each=|| vec![0, 1, 2, 3, 4, 5, 6, 7]
|
||||
key=|i| *i
|
||||
view=|i| view! { cx, {i} }
|
||||
view=|cx, i| view! { cx, {i} }
|
||||
/>
|
||||
}
|
||||
.into_view(cx);
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
max_width = 80
|
||||
imports_granularity = "Crate"
|
||||
tab_spaces = 2
|
||||
format_strings = true
|
||||
@@ -5,8 +5,8 @@ mod fragment;
|
||||
mod unit;
|
||||
|
||||
use crate::{
|
||||
hydration::{HydrationCtx, HydrationKey},
|
||||
Comment, IntoView, View,
|
||||
hydration::{HydrationCtx, HydrationKey},
|
||||
Comment, IntoView, View,
|
||||
};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use crate::{mount_child, prepare_to_move, MountKind, Mountable};
|
||||
@@ -28,231 +28,234 @@ use wasm_bindgen::JsCast;
|
||||
#[derive(educe::Educe)]
|
||||
#[educe(Default, Clone, PartialEq, Eq)]
|
||||
pub enum CoreComponent {
|
||||
/// The [Unit] component.
|
||||
#[educe(Default)]
|
||||
Unit(UnitRepr),
|
||||
/// The [DynChild] component.
|
||||
DynChild(DynChildRepr),
|
||||
/// The [Each] component.
|
||||
Each(EachRepr),
|
||||
/// The [Unit] component.
|
||||
#[educe(Default)]
|
||||
Unit(UnitRepr),
|
||||
/// The [DynChild] component.
|
||||
DynChild(DynChildRepr),
|
||||
/// The [Each] component.
|
||||
Each(EachRepr),
|
||||
}
|
||||
|
||||
impl fmt::Debug for CoreComponent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Unit(u) => u.fmt(f),
|
||||
Self::DynChild(dc) => dc.fmt(f),
|
||||
Self::Each(e) => e.fmt(f),
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Unit(u) => u.fmt(f),
|
||||
Self::DynChild(dc) => dc.fmt(f),
|
||||
Self::Each(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom leptos component.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct ComponentRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Rc<OnceCell<()>>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) name: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: Comment,
|
||||
/// The children of the component.
|
||||
pub children: Vec<View>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Rc<OnceCell<()>>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) name: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: Comment,
|
||||
/// The children of the component.
|
||||
pub children: Vec<View>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ComponentRepr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
|
||||
if self.children.is_empty() {
|
||||
#[cfg(debug_assertions)]
|
||||
return write!(f, "<{} />", self.name);
|
||||
if self.children.is_empty() {
|
||||
#[cfg(debug_assertions)]
|
||||
return write!(f, "<{} />", self.name);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return f.write_str("<Component />");
|
||||
} else {
|
||||
#[cfg(debug_assertions)]
|
||||
writeln!(f, "<{}>", self.name)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
f.write_str("<Component>")?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
return f.write_str("<Component />");
|
||||
} else {
|
||||
#[cfg(debug_assertions)]
|
||||
writeln!(f, "<{}>", self.name)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
f.write_str("<Component>")?;
|
||||
|
||||
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
||||
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
||||
|
||||
for child in &self.children {
|
||||
writeln!(pad_adapter, "{child:#?}")?;
|
||||
}
|
||||
for child in &self.children {
|
||||
writeln!(pad_adapter, "{child:#?}")?;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
write!(f, "</{}>", self.name)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
f.write_str("</Component>")?;
|
||||
#[cfg(debug_assertions)]
|
||||
write!(f, "</{}>", self.name)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
f.write_str("</Component>")?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for ComponentRepr {
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
if self.mounted.get().is_none() {
|
||||
self.mounted.set(()).unwrap();
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
if self.mounted.get().is_none() {
|
||||
self.mounted.set(()).unwrap();
|
||||
|
||||
self
|
||||
.document_fragment
|
||||
.unchecked_ref::<web_sys::Node>()
|
||||
.to_owned()
|
||||
self.document_fragment
|
||||
.unchecked_ref::<web_sys::Node>()
|
||||
.to_owned()
|
||||
}
|
||||
// We need to prepare all children to move
|
||||
else {
|
||||
let opening = self.get_opening_node();
|
||||
|
||||
prepare_to_move(
|
||||
&self.document_fragment,
|
||||
&opening,
|
||||
&self.closing.node,
|
||||
);
|
||||
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
}
|
||||
}
|
||||
// We need to prepare all children to move
|
||||
else {
|
||||
let opening = self.get_opening_node();
|
||||
|
||||
prepare_to_move(&self.document_fragment, &opening, &self.closing.node);
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self._opening.node.clone();
|
||||
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
#[cfg(not(debug_assertions))]
|
||||
return if let Some(child) = self.children.get(0) {
|
||||
child.get_opening_node()
|
||||
} else {
|
||||
self.closing.node.clone()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self._opening.node.clone();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return if let Some(child) = self.children.get(0) {
|
||||
child.get_opening_node()
|
||||
} else {
|
||||
self.closing.node.clone()
|
||||
};
|
||||
}
|
||||
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for ComponentRepr {
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<Component />", skip_all, fields(name = %self.name)))]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
for child in &self.children {
|
||||
mount_child(MountKind::Before(&self.closing.node), child);
|
||||
}
|
||||
}
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<Component />", skip_all, fields(name = %self.name)))]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
for child in &self.children {
|
||||
mount_child(MountKind::Before(&self.closing.node), child);
|
||||
}
|
||||
}
|
||||
|
||||
View::Component(self)
|
||||
}
|
||||
View::Component(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentRepr {
|
||||
/// Creates a new [`Component`].
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self::new_with_id(name, HydrationCtx::id())
|
||||
}
|
||||
|
||||
/// Creates a new [`Component`] with the given hydration ID.
|
||||
pub fn new_with_id(
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
id: HydrationKey,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
|
||||
let markers = (
|
||||
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Owned(format!("<{name}>")), &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let document_fragment = {
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
|
||||
// Insert the comments into the document fragment
|
||||
// so they can serve as our references when inserting
|
||||
// future nodes
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
#[cfg(debug_assertions)]
|
||||
fragment
|
||||
.append_with_node_2(&markers.1.node, &markers.0.node)
|
||||
.expect("append to not err");
|
||||
#[cfg(not(debug_assertions))]
|
||||
fragment
|
||||
.append_with_node_1(&markers.0.node)
|
||||
.expect("append to not err");
|
||||
}
|
||||
|
||||
fragment
|
||||
};
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Default::default(),
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: markers.1,
|
||||
closing: markers.0,
|
||||
#[cfg(debug_assertions)]
|
||||
name,
|
||||
children: Vec::with_capacity(1),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
/// Creates a new [`Component`].
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self::new_with_id(name, HydrationCtx::id())
|
||||
}
|
||||
|
||||
/// Creates a new [`Component`] with the given hydration ID.
|
||||
pub fn new_with_id(
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
id: HydrationKey,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
|
||||
let markers = (
|
||||
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Owned(format!("<{name}>")), &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let document_fragment = {
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
|
||||
// Insert the comments into the document fragment
|
||||
// so they can serve as our references when inserting
|
||||
// future nodes
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
#[cfg(debug_assertions)]
|
||||
fragment
|
||||
.append_with_node_2(&markers.1.node, &markers.0.node)
|
||||
.expect("append to not err");
|
||||
#[cfg(not(debug_assertions))]
|
||||
fragment
|
||||
.append_with_node_1(&markers.0.node)
|
||||
.expect("append to not err");
|
||||
}
|
||||
|
||||
fragment
|
||||
};
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Default::default(),
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: markers.1,
|
||||
closing: markers.0,
|
||||
#[cfg(debug_assertions)]
|
||||
name,
|
||||
children: Vec::with_capacity(1),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A user-defined `leptos` component.
|
||||
pub struct Component<F, V>
|
||||
where
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
id: HydrationKey,
|
||||
name: Cow<'static, str>,
|
||||
children_fn: F,
|
||||
id: HydrationKey,
|
||||
name: Cow<'static, str>,
|
||||
children_fn: F,
|
||||
}
|
||||
|
||||
impl<F, V> Component<F, V>
|
||||
where
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
/// Creates a new component.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>, f: F) -> Self {
|
||||
Self {
|
||||
id: HydrationCtx::next_component(),
|
||||
name: name.into(),
|
||||
children_fn: f,
|
||||
/// Creates a new component.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>, f: F) -> Self {
|
||||
Self {
|
||||
id: HydrationCtx::next_component(),
|
||||
name: name.into(),
|
||||
children_fn: f,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, V> IntoView for Component<F, V>
|
||||
where
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
#[track_caller]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
let Self {
|
||||
id,
|
||||
name,
|
||||
children_fn,
|
||||
} = self;
|
||||
#[track_caller]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
let Self {
|
||||
id,
|
||||
name,
|
||||
children_fn,
|
||||
} = self;
|
||||
|
||||
let mut repr = ComponentRepr::new_with_id(name, id);
|
||||
let mut repr = ComponentRepr::new_with_id(name, id);
|
||||
|
||||
// disposed automatically when the parent scope is disposed
|
||||
let (child, _) =
|
||||
cx.run_child_scope(|cx| cx.untrack(|| children_fn(cx).into_view(cx)));
|
||||
// disposed automatically when the parent scope is disposed
|
||||
let (child, _) = cx
|
||||
.run_child_scope(|cx| cx.untrack(|| children_fn(cx).into_view(cx)));
|
||||
|
||||
repr.children.push(child);
|
||||
repr.children.push(child);
|
||||
|
||||
repr.into_view(cx)
|
||||
}
|
||||
repr.into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
hydration::{HydrationCtx, HydrationKey},
|
||||
Comment, IntoView, View,
|
||||
hydration::{HydrationCtx, HydrationKey},
|
||||
Comment, IntoView, View,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::Scope;
|
||||
@@ -16,320 +16,351 @@ cfg_if! {
|
||||
/// The internal representation of the [`DynChild`] core-component.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct DynChildRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: Comment,
|
||||
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: Comment,
|
||||
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
}
|
||||
|
||||
impl fmt::Debug for DynChildRepr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
|
||||
f.write_str("<DynChild>\n")?;
|
||||
f.write_str("<DynChild>\n")?;
|
||||
|
||||
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
||||
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
||||
|
||||
writeln!(
|
||||
pad_adapter,
|
||||
"{:#?}",
|
||||
self.child.borrow().deref().deref().as_ref().unwrap()
|
||||
)?;
|
||||
writeln!(
|
||||
pad_adapter,
|
||||
"{:#?}",
|
||||
self.child.borrow().deref().deref().as_ref().unwrap()
|
||||
)?;
|
||||
|
||||
f.write_str("</DynChild>")
|
||||
}
|
||||
f.write_str("</DynChild>")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for DynChildRepr {
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
if self.document_fragment.child_nodes().length() != 0 {
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
} else {
|
||||
let opening = self.get_opening_node();
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
if self.document_fragment.child_nodes().length() != 0 {
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
} else {
|
||||
let opening = self.get_opening_node();
|
||||
|
||||
prepare_to_move(&self.document_fragment, &opening, &self.closing.node);
|
||||
prepare_to_move(
|
||||
&self.document_fragment,
|
||||
&opening,
|
||||
&self.closing.node,
|
||||
);
|
||||
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self.opening.node.clone();
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self.opening.node.clone();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return self
|
||||
.child
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_opening_node();
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
return self
|
||||
.child
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_opening_node();
|
||||
}
|
||||
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl DynChildRepr {
|
||||
fn new_with_id(id: HydrationKey) -> Self {
|
||||
let markers = (
|
||||
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Borrowed("<DynChild>"), &id, false),
|
||||
);
|
||||
fn new_with_id(id: HydrationKey) -> Self {
|
||||
let markers = (
|
||||
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Borrowed("<DynChild>"), &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let document_fragment = {
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let document_fragment = {
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
|
||||
// Insert the comments into the document fragment
|
||||
// so they can serve as our references when inserting
|
||||
// future nodes
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
#[cfg(debug_assertions)]
|
||||
fragment
|
||||
.append_with_node_2(&markers.1.node, &markers.0.node)
|
||||
.unwrap();
|
||||
#[cfg(not(debug_assertions))]
|
||||
fragment.append_with_node_1(&markers.0.node).unwrap();
|
||||
}
|
||||
// Insert the comments into the document fragment
|
||||
// so they can serve as our references when inserting
|
||||
// future nodes
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
#[cfg(debug_assertions)]
|
||||
fragment
|
||||
.append_with_node_2(&markers.1.node, &markers.0.node)
|
||||
.unwrap();
|
||||
#[cfg(not(debug_assertions))]
|
||||
fragment.append_with_node_1(&markers.0.node).unwrap();
|
||||
}
|
||||
|
||||
fragment
|
||||
};
|
||||
fragment
|
||||
};
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: markers.1,
|
||||
child: Default::default(),
|
||||
closing: markers.0,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: markers.1,
|
||||
child: Default::default(),
|
||||
closing: markers.0,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents any [`View`] that can change over time.
|
||||
pub struct DynChild<CF, N>
|
||||
where
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
id: crate::HydrationKey,
|
||||
child_fn: CF,
|
||||
id: crate::HydrationKey,
|
||||
child_fn: CF,
|
||||
}
|
||||
|
||||
impl<CF, N> DynChild<CF, N>
|
||||
where
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
/// Creates a new dynamic child which will re-render whenever it's
|
||||
/// signal dependencies change.
|
||||
pub fn new(child_fn: CF) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), child_fn)
|
||||
}
|
||||
/// Creates a new dynamic child which will re-render whenever it's
|
||||
/// signal dependencies change.
|
||||
#[track_caller]
|
||||
pub fn new(child_fn: CF) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), child_fn)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
|
||||
Self { id, child_fn }
|
||||
}
|
||||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
pub fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
|
||||
Self { id, child_fn }
|
||||
}
|
||||
}
|
||||
|
||||
impl<CF, N> IntoView for DynChild<CF, N>
|
||||
where
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "<DynChild />", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
// concrete inner function
|
||||
fn create_dyn_view(
|
||||
cx: Scope,
|
||||
component: DynChildRepr,
|
||||
child_fn: Box<dyn Fn() -> View>,
|
||||
) -> DynChildRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let closing = component.closing.node.clone();
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "<DynChild />", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
// concrete inner function
|
||||
fn create_dyn_view(
|
||||
cx: Scope,
|
||||
component: DynChildRepr,
|
||||
child_fn: Box<dyn Fn() -> View>,
|
||||
) -> DynChildRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let closing = component.closing.node.clone();
|
||||
|
||||
let child = component.child.clone();
|
||||
let child = component.child.clone();
|
||||
|
||||
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
||||
let span = tracing::Span::current();
|
||||
#[cfg(all(
|
||||
debug_assertions,
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
let span = tracing::Span::current();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
create_effect(
|
||||
cx,
|
||||
move |prev_run: Option<(Option<web_sys::Node>, ScopeDisposer)>| {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.enter();
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
create_effect(
|
||||
cx,
|
||||
move |prev_run: Option<(
|
||||
Option<web_sys::Node>,
|
||||
ScopeDisposer,
|
||||
)>| {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.enter();
|
||||
|
||||
let (new_child, disposer) =
|
||||
cx.run_child_scope(|cx| child_fn().into_view(cx));
|
||||
let (new_child, disposer) =
|
||||
cx.run_child_scope(|cx| child_fn().into_view(cx));
|
||||
|
||||
let mut child_borrow = child.borrow_mut();
|
||||
let mut child_borrow = child.borrow_mut();
|
||||
|
||||
// Is this at least the second time we are loading a child?
|
||||
if let Some((prev_t, prev_disposer)) = prev_run {
|
||||
let child = child_borrow.take().unwrap();
|
||||
// Is this at least the second time we are loading a child?
|
||||
if let Some((prev_t, prev_disposer)) = prev_run {
|
||||
let child = child_borrow.take().unwrap();
|
||||
|
||||
// Dispose of the scope
|
||||
prev_disposer.dispose();
|
||||
// Dispose of the scope
|
||||
prev_disposer.dispose();
|
||||
|
||||
// We need to know if our child wasn't moved elsewhere.
|
||||
// If it was, `DynChild` no longer "owns" that child, and
|
||||
// is therefore no longer sound to unmount it from the DOM
|
||||
// or to reuse it in the case of a text node
|
||||
// We need to know if our child wasn't moved elsewhere.
|
||||
// If it was, `DynChild` no longer "owns" that child, and
|
||||
// is therefore no longer sound to unmount it from the DOM
|
||||
// or to reuse it in the case of a text node
|
||||
|
||||
// TODO check does this still detect moves correctly?
|
||||
let was_child_moved = prev_t.is_none()
|
||||
&& child.get_closing_node().next_sibling().as_ref()
|
||||
!= Some(&closing);
|
||||
// TODO check does this still detect moves correctly?
|
||||
let was_child_moved = prev_t.is_none()
|
||||
&& child.get_closing_node().next_sibling().as_ref()
|
||||
!= Some(&closing);
|
||||
|
||||
// If the previous child was a text node, we would like to
|
||||
// make use of it again if our current child is also a text
|
||||
// node
|
||||
let ret = if let Some(prev_t) = prev_t {
|
||||
// Here, our child is also a text node
|
||||
if let Some(new_t) = new_child.get_text() {
|
||||
if !was_child_moved && child != new_child {
|
||||
prev_t
|
||||
.unchecked_ref::<web_sys::Text>()
|
||||
.set_data(&new_t.content);
|
||||
// If the previous child was a text node, we would like to
|
||||
// make use of it again if our current child is also a text
|
||||
// node
|
||||
let ret = if let Some(prev_t) = prev_t {
|
||||
// Here, our child is also a text node
|
||||
if let Some(new_t) = new_child.get_text() {
|
||||
if !was_child_moved && child != new_child {
|
||||
prev_t
|
||||
.unchecked_ref::<web_sys::Text>()
|
||||
.set_data(&new_t.content);
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(Some(prev_t), disposer)
|
||||
} else {
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
(Some(prev_t), disposer)
|
||||
} else {
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
|
||||
**child_borrow = Some(new_child.clone());
|
||||
**child_borrow = Some(new_child.clone());
|
||||
|
||||
(Some(new_t.node.clone()), disposer)
|
||||
}
|
||||
}
|
||||
// Child is not a text node, so we can remove the previous
|
||||
// text node
|
||||
else {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the text
|
||||
closing
|
||||
.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
}
|
||||
(Some(new_t.node.clone()), disposer)
|
||||
}
|
||||
}
|
||||
// Child is not a text node, so we can remove the previous
|
||||
// text node
|
||||
else {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the text
|
||||
closing
|
||||
.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
}
|
||||
|
||||
// Mount the new child, and we're done
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
// Mount the new child, and we're done
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(None, disposer)
|
||||
}
|
||||
}
|
||||
// Otherwise, the new child can still be a text node,
|
||||
// but we know the previous child was not, so no special
|
||||
// treatment here
|
||||
else {
|
||||
// Technically, I think this check shouldn't be necessary, but
|
||||
// I can imagine some edge case that the child changes while
|
||||
// hydration is ongoing
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the child
|
||||
let start = child.get_opening_node();
|
||||
let end = &closing;
|
||||
(None, disposer)
|
||||
}
|
||||
}
|
||||
// Otherwise, the new child can still be a text node,
|
||||
// but we know the previous child was not, so no special
|
||||
// treatment here
|
||||
else {
|
||||
// Technically, I think this check shouldn't be necessary, but
|
||||
// I can imagine some edge case that the child changes while
|
||||
// hydration is ongoing
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the child
|
||||
let start = child.get_opening_node();
|
||||
let end = &closing;
|
||||
|
||||
unmount_child(&start, end);
|
||||
}
|
||||
unmount_child(&start, end);
|
||||
}
|
||||
|
||||
// Mount the new child
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
}
|
||||
// Mount the new child
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
}
|
||||
|
||||
// We want to reuse text nodes, so hold onto it if
|
||||
// our child is one
|
||||
let t = new_child.get_text().map(|t| t.node.clone());
|
||||
// We want to reuse text nodes, so hold onto it if
|
||||
// our child is one
|
||||
let t =
|
||||
new_child.get_text().map(|t| t.node.clone());
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(t, disposer)
|
||||
};
|
||||
(t, disposer)
|
||||
};
|
||||
|
||||
ret
|
||||
}
|
||||
// Otherwise, we know for sure this is our first time
|
||||
else {
|
||||
// We need to remove the text created from SSR
|
||||
if HydrationCtx::is_hydrating() && new_child.get_text().is_some() {
|
||||
let t = closing
|
||||
.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>();
|
||||
ret
|
||||
}
|
||||
// Otherwise, we know for sure this is our first time
|
||||
else {
|
||||
// We need to remove the text created from SSR
|
||||
if HydrationCtx::is_hydrating()
|
||||
&& new_child.get_text().is_some()
|
||||
{
|
||||
let t = closing
|
||||
.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>();
|
||||
|
||||
// See note on ssr.rs when matching on `DynChild`
|
||||
// for more details on why we need to do this for
|
||||
// release
|
||||
if !cfg!(debug_assertions) {
|
||||
t.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
}
|
||||
// See note on ssr.rs when matching on `DynChild`
|
||||
// for more details on why we need to do this for
|
||||
// release
|
||||
if !cfg!(debug_assertions) {
|
||||
t.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
}
|
||||
|
||||
t.remove();
|
||||
t.remove();
|
||||
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
}
|
||||
|
||||
// If we are not hydrating, we simply mount the child
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
}
|
||||
|
||||
// We want to update text nodes, rather than replace them, so
|
||||
// make sure to hold onto the text node
|
||||
let t = new_child.get_text().map(|t| t.node.clone());
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(t, disposer)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let new_child = child_fn().into_view(cx);
|
||||
|
||||
**child.borrow_mut() = Some(new_child);
|
||||
}
|
||||
|
||||
// If we are not hydrating, we simply mount the child
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
}
|
||||
component
|
||||
}
|
||||
|
||||
// We want to update text nodes, rather than replace them, so
|
||||
// make sure to hold onto the text node
|
||||
let t = new_child.get_text().map(|t| t.node.clone());
|
||||
// monomorphized outer function
|
||||
let Self { id, child_fn } = self;
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
let component = DynChildRepr::new_with_id(id);
|
||||
let component = create_dyn_view(
|
||||
cx,
|
||||
component,
|
||||
Box::new(move || child_fn().into_view(cx)),
|
||||
);
|
||||
|
||||
(t, disposer)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let new_child = child_fn().into_view(cx);
|
||||
|
||||
**child.borrow_mut() = Some(new_child);
|
||||
}
|
||||
|
||||
component
|
||||
View::CoreComponent(crate::CoreComponent::DynChild(component))
|
||||
}
|
||||
|
||||
// monomorphized outer function
|
||||
let Self { id, child_fn } = self;
|
||||
|
||||
let component = DynChildRepr::new_with_id(id);
|
||||
let component = create_dyn_view(
|
||||
cx,
|
||||
component,
|
||||
Box::new(move || child_fn().into_view(cx)),
|
||||
);
|
||||
|
||||
View::CoreComponent(crate::CoreComponent::DynChild(component))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,87 +1,162 @@
|
||||
use crate::{HydrationCtx, IntoView};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::{use_context, RwSignal};
|
||||
use leptos_reactive::{signal_prelude::*, 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<String, Arc<dyn Error + Send + Sync>>);
|
||||
pub struct Errors(HashMap<ErrorKey, Arc<dyn Error + Send + Sync>>);
|
||||
|
||||
/// A unique key for an error that occurs at a particular location in the user interface.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ErrorKey(String);
|
||||
|
||||
impl<T> From<T> for ErrorKey
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
fn from(key: T) -> ErrorKey {
|
||||
ErrorKey(key.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Errors {
|
||||
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
|
||||
type IntoIter = IntoIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
IntoIter(self.0.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
/// An owning iterator over all the errors contained in the [Errors] struct.
|
||||
pub struct IntoIter(
|
||||
std::collections::hash_map::IntoIter<
|
||||
ErrorKey,
|
||||
Arc<dyn Error + Send + Sync>,
|
||||
>,
|
||||
);
|
||||
|
||||
impl Iterator for IntoIter {
|
||||
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
|
||||
|
||||
fn next(
|
||||
&mut self,
|
||||
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over all the errors contained in the [Errors] struct.
|
||||
pub struct Iter<'a>(
|
||||
std::collections::hash_map::Iter<
|
||||
'a,
|
||||
ErrorKey,
|
||||
Arc<dyn Error + Send + Sync>,
|
||||
>,
|
||||
);
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = (&'a ErrorKey, &'a Arc<dyn Error + Send + Sync>);
|
||||
|
||||
fn next(
|
||||
&mut self,
|
||||
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> IntoView for Result<T, E>
|
||||
where
|
||||
T: IntoView + 'static,
|
||||
E: Error + Send + Sync + 'static,
|
||||
T: IntoView + 'static,
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||
let id = HydrationCtx::peek().previous;
|
||||
let errors = use_context::<RwSignal<Errors>>(cx);
|
||||
match self {
|
||||
Ok(stuff) => {
|
||||
if let Some(errors) = errors {
|
||||
errors.update(|errors| {
|
||||
errors.0.remove(&id);
|
||||
});
|
||||
}
|
||||
stuff.into_view(cx)
|
||||
}
|
||||
Err(error) => {
|
||||
match errors {
|
||||
Some(errors) => {
|
||||
errors.update({
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let id = id.clone();
|
||||
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(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use leptos_reactive::{on_cleanup, queue_microtask};
|
||||
on_cleanup(cx, move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
errors.remove::<E>(&id);
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||
let id = ErrorKey(HydrationCtx::peek().previous);
|
||||
let errors = use_context::<RwSignal<Errors>>(cx);
|
||||
match self {
|
||||
Ok(stuff) => {
|
||||
if let Some(errors) = errors {
|
||||
errors.update(|errors| {
|
||||
errors.0.remove(&id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
stuff.into_view(cx)
|
||||
}
|
||||
Err(error) => {
|
||||
match errors {
|
||||
Some(errors) => {
|
||||
errors.update({
|
||||
#[cfg(all(
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
let id = id.clone();
|
||||
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(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use leptos_reactive::{on_cleanup, queue_microtask};
|
||||
on_cleanup(cx, move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
errors.remove(&id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
#[cfg(debug_assertions)]
|
||||
warn!(
|
||||
"No ErrorBoundary components found! Returning \
|
||||
errors will not be handled and will silently \
|
||||
disappear"
|
||||
);
|
||||
}
|
||||
}
|
||||
().into_view(cx)
|
||||
}
|
||||
}
|
||||
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: String, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
self.0.insert(key, Arc::new(error));
|
||||
}
|
||||
/// Add an error with the default key for errors outside the reactive system
|
||||
pub fn insert_with_default_key<E>(&mut self, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
self.0.insert(String::new(), Arc::new(error));
|
||||
}
|
||||
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn remove<E>(&mut self, key: &str)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
self.0.remove(key);
|
||||
}
|
||||
/// Returns `true` if there are no errors.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn insert<E>(&mut self, key: ErrorKey, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
self.0.insert(key, Arc::new(error));
|
||||
}
|
||||
|
||||
/// Add an error with the default key for errors outside the reactive system
|
||||
pub fn insert_with_default_key<E>(&mut self, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
self.0.insert(Default::default(), Arc::new(error));
|
||||
}
|
||||
|
||||
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn remove(
|
||||
&mut self,
|
||||
key: &ErrorKey,
|
||||
) -> Option<Arc<dyn Error + Send + Sync>> {
|
||||
self.0.remove(key)
|
||||
}
|
||||
|
||||
/// An iterator over all the errors, in arbitrary order.
|
||||
pub fn iter(&self) -> Iter<'_> {
|
||||
Iter(self.0.iter())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +1,78 @@
|
||||
use leptos_reactive::Scope;
|
||||
|
||||
use crate::{
|
||||
hydration::HydrationKey, ComponentRepr, HydrationCtx, IntoView, View,
|
||||
hydration::HydrationKey, ComponentRepr, HydrationCtx, IntoView, View,
|
||||
};
|
||||
use leptos_reactive::Scope;
|
||||
|
||||
/// Trait for converting any iterable into a [`Fragment`].
|
||||
pub trait IntoFragment {
|
||||
/// Consumes this type, returning [`Fragment`].
|
||||
fn into_fragment(self, cx: Scope) -> Fragment;
|
||||
/// Consumes this type, returning [`Fragment`].
|
||||
fn into_fragment(self, cx: Scope) -> Fragment;
|
||||
}
|
||||
|
||||
impl<I, V> IntoFragment for I
|
||||
where
|
||||
I: IntoIterator<Item = V>,
|
||||
V: IntoView,
|
||||
I: IntoIterator<Item = V>,
|
||||
V: IntoView,
|
||||
{
|
||||
fn into_fragment(self, cx: Scope) -> Fragment {
|
||||
self.into_iter().map(|v| v.into_view(cx)).collect()
|
||||
}
|
||||
fn into_fragment(self, cx: Scope) -> Fragment {
|
||||
self.into_iter().map(|v| v.into_view(cx)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a group of [`views`](View).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Fragment {
|
||||
id: HydrationKey,
|
||||
/// The nodes contained in the fragment.
|
||||
pub nodes: Vec<View>,
|
||||
id: HydrationKey,
|
||||
/// The nodes contained in the fragment.
|
||||
pub nodes: Vec<View>,
|
||||
}
|
||||
|
||||
impl FromIterator<View> for Fragment {
|
||||
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
|
||||
Fragment::new(iter.into_iter().collect())
|
||||
}
|
||||
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
|
||||
Fragment::new(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<View> for Fragment {
|
||||
fn from(view: View) -> Self {
|
||||
Fragment::new(vec![view])
|
||||
}
|
||||
fn from(view: View) -> Self {
|
||||
Fragment::new(vec![view])
|
||||
}
|
||||
}
|
||||
|
||||
impl Fragment {
|
||||
/// Creates a new [`Fragment`] from a [`Vec<Node>`].
|
||||
pub fn new(nodes: Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes)
|
||||
}
|
||||
/// Creates a new [`Fragment`] from a [`Vec<Node>`].
|
||||
pub fn new(nodes: Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes)
|
||||
}
|
||||
|
||||
/// Creates a new [`Fragment`] from a function that returns [`Vec<Node>`].
|
||||
pub fn lazy(nodes: impl FnOnce() -> Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes())
|
||||
}
|
||||
/// Creates a new [`Fragment`] from a function that returns [`Vec<Node>`].
|
||||
pub fn lazy(nodes: impl FnOnce() -> Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes())
|
||||
}
|
||||
|
||||
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
|
||||
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
|
||||
Self { id, nodes }
|
||||
}
|
||||
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
|
||||
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
|
||||
Self { id, nodes }
|
||||
}
|
||||
|
||||
/// Gives access to the [View] children contained within the fragment.
|
||||
pub fn as_children(&self) -> &[View] {
|
||||
&self.nodes
|
||||
}
|
||||
/// Gives access to the [View] children contained within the fragment.
|
||||
pub fn as_children(&self) -> &[View] {
|
||||
&self.nodes
|
||||
}
|
||||
|
||||
/// Returns the fragment's hydration ID.
|
||||
pub fn id(&self) -> &HydrationKey {
|
||||
&self.id
|
||||
}
|
||||
/// Returns the fragment's hydration ID.
|
||||
pub fn id(&self) -> &HydrationKey {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for Fragment {
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "</>", skip_all, fields(children = self.nodes.len())))]
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> View {
|
||||
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "</>", skip_all, fields(children = self.nodes.len())))]
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> View {
|
||||
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
|
||||
|
||||
frag.children = self.nodes;
|
||||
frag.children = self.nodes;
|
||||
|
||||
frag.into_view(cx)
|
||||
}
|
||||
frag.into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,42 +15,42 @@ use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View};
|
||||
/// The internal representation of the [`Unit`] core-component.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct UnitRepr {
|
||||
comment: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
comment: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
}
|
||||
|
||||
impl fmt::Debug for UnitRepr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("<() />")
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("<() />")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UnitRepr {
|
||||
fn default() -> Self {
|
||||
let id = HydrationCtx::id();
|
||||
fn default() -> Self {
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
Self {
|
||||
comment: Comment::new("<() />", &id, true),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
Self {
|
||||
comment: Comment::new("<() />", &id, true),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for UnitRepr {
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
}
|
||||
|
||||
/// The unit `()` leptos counterpart.
|
||||
@@ -58,13 +58,13 @@ impl Mountable for UnitRepr {
|
||||
pub struct Unit;
|
||||
|
||||
impl IntoView for Unit {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "<() />", skip_all)
|
||||
)]
|
||||
fn into_view(self, _: leptos_reactive::Scope) -> crate::View {
|
||||
let component = UnitRepr::default();
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "<() />", skip_all)
|
||||
)]
|
||||
fn into_view(self, _: leptos_reactive::Scope) -> crate::View {
|
||||
let component = UnitRepr::default();
|
||||
|
||||
View::CoreComponent(CoreComponent::Unit(component))
|
||||
}
|
||||
View::CoreComponent(CoreComponent::Unit(component))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,146 +3,171 @@ pub mod typed;
|
||||
use std::{borrow::Cow, cell::RefCell, collections::HashSet};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::{
|
||||
convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue,
|
||||
UnwrapThrowExt,
|
||||
convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue,
|
||||
UnwrapThrowExt,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
pub static GLOBAL_EVENTS: RefCell<HashSet<Cow<'static, str>>> = RefCell::new(HashSet::new());
|
||||
pub(crate) static GLOBAL_EVENTS: RefCell<HashSet<Cow<'static, str>>> = RefCell::new(HashSet::new());
|
||||
}
|
||||
|
||||
// Used in template macro
|
||||
#[doc(hidden)]
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
|
||||
target: &web_sys::Element,
|
||||
event: E,
|
||||
#[allow(unused_mut)] // used for tracing in debug
|
||||
mut event_handler: impl FnMut(E::EventType) + 'static,
|
||||
) {
|
||||
let event_name = event.name();
|
||||
|
||||
if event.bubbles() {
|
||||
add_event_listener(target, event_name, event_handler);
|
||||
} else {
|
||||
add_event_listener_undelegated(target, &event_name, event_handler);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds an event listener to the target DOM element using implicit event delegation.
|
||||
#[doc(hidden)]
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn add_event_listener<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
|
||||
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
|
||||
target: &web_sys::Element,
|
||||
event_name: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
|
||||
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
let key = event_delegation_key(&event_name);
|
||||
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
|
||||
add_delegated_event_listener(event_name);
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
let key = event_delegation_key(&event_name);
|
||||
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
|
||||
add_delegated_event_listener(event_name);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn add_event_listener_undelegated<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: &str,
|
||||
mut cb: impl FnMut(E) + 'static,
|
||||
pub(crate) fn add_event_listener_undelegated<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: &str,
|
||||
mut cb: impl FnMut(E) + 'static,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let event_name = intern(event_name);
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
let event_name = intern(event_name);
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
}
|
||||
|
||||
// cf eventHandler in ryansolid/dom-expressions
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn add_delegated_event_listener(event_name: Cow<'static, str>) {
|
||||
GLOBAL_EVENTS.with(|global_events| {
|
||||
let mut events = global_events.borrow_mut();
|
||||
if !events.contains(&event_name) {
|
||||
// create global handler
|
||||
let key = JsValue::from_str(&event_delegation_key(&event_name));
|
||||
let handler = move |ev: web_sys::Event| {
|
||||
let target = ev.target();
|
||||
let node = ev.composed_path().get(0);
|
||||
let mut node = if node.is_undefined() || node.is_null() {
|
||||
JsValue::from(target)
|
||||
} else {
|
||||
node
|
||||
};
|
||||
GLOBAL_EVENTS.with(|global_events| {
|
||||
let mut events = global_events.borrow_mut();
|
||||
if !events.contains(&event_name) {
|
||||
// create global handler
|
||||
let key = JsValue::from_str(&event_delegation_key(&event_name));
|
||||
let handler = move |ev: web_sys::Event| {
|
||||
let target = ev.target();
|
||||
let node = ev.composed_path().get(0);
|
||||
let mut node = if node.is_undefined() || node.is_null() {
|
||||
JsValue::from(target)
|
||||
} else {
|
||||
node
|
||||
};
|
||||
|
||||
// TODO reverse Shadow DOM retargetting
|
||||
// TODO reverse Shadow DOM retargetting
|
||||
|
||||
// TODO simulate currentTarget
|
||||
// TODO simulate currentTarget
|
||||
|
||||
while !node.is_null() {
|
||||
let node_is_disabled =
|
||||
js_sys::Reflect::get(&node, &JsValue::from_str("disabled"))
|
||||
.unwrap_throw()
|
||||
.is_truthy();
|
||||
if !node_is_disabled {
|
||||
let maybe_handler =
|
||||
js_sys::Reflect::get(&node, &key).unwrap_throw();
|
||||
if !maybe_handler.is_undefined() {
|
||||
let f = maybe_handler.unchecked_ref::<js_sys::Function>();
|
||||
let _ = f.call1(&node, &ev);
|
||||
while !node.is_null() {
|
||||
let node_is_disabled = js_sys::Reflect::get(
|
||||
&node,
|
||||
&JsValue::from_str("disabled"),
|
||||
)
|
||||
.unwrap_throw()
|
||||
.is_truthy();
|
||||
if !node_is_disabled {
|
||||
let maybe_handler =
|
||||
js_sys::Reflect::get(&node, &key).unwrap_throw();
|
||||
if !maybe_handler.is_undefined() {
|
||||
let f = maybe_handler
|
||||
.unchecked_ref::<js_sys::Function>();
|
||||
let _ = f.call1(&node, &ev);
|
||||
|
||||
if ev.cancel_bubble() {
|
||||
return;
|
||||
if ev.cancel_bubble() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// navigate up tree
|
||||
let host =
|
||||
js_sys::Reflect::get(&node, &JsValue::from_str("host"))
|
||||
.unwrap_throw();
|
||||
if host.is_truthy()
|
||||
&& host != node
|
||||
&& host.dyn_ref::<web_sys::Node>().is_some()
|
||||
{
|
||||
node = host;
|
||||
} else if let Some(parent) =
|
||||
node.unchecked_into::<web_sys::Node>().parent_node()
|
||||
{
|
||||
node = parent.into()
|
||||
} else {
|
||||
node = JsValue::null()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let handler = move |e| {
|
||||
let _guard = span.enter();
|
||||
handler(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// navigate up tree
|
||||
let host = js_sys::Reflect::get(&node, &JsValue::from_str("host"))
|
||||
.unwrap_throw();
|
||||
if host.is_truthy()
|
||||
&& host != node
|
||||
&& host.dyn_ref::<web_sys::Node>().is_some()
|
||||
{
|
||||
node = host;
|
||||
} else if let Some(parent) =
|
||||
node.unchecked_into::<web_sys::Node>().parent_node()
|
||||
{
|
||||
node = parent.into()
|
||||
} else {
|
||||
node = JsValue::null()
|
||||
}
|
||||
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
|
||||
let handler = Closure::wrap(handler).into_js_value();
|
||||
_ = crate::window().add_event_listener_with_callback(
|
||||
&event_name,
|
||||
handler.unchecked_ref(),
|
||||
);
|
||||
|
||||
// register that we've created handler
|
||||
events.insert(event_name);
|
||||
}
|
||||
};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let handler = move |e| {
|
||||
let _guard = span.enter();
|
||||
handler(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
|
||||
let handler = Closure::wrap(handler).into_js_value();
|
||||
_ = crate::window()
|
||||
.add_event_listener_with_callback(&event_name, handler.unchecked_ref());
|
||||
|
||||
// register that we've created handler
|
||||
events.insert(event_name);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn event_delegation_key(event_name: &str) -> String {
|
||||
let event_name = intern(event_name);
|
||||
let mut n = String::from("$$$");
|
||||
n.push_str(event_name);
|
||||
n
|
||||
let event_name = intern(event_name);
|
||||
let mut n = String::from("$$$");
|
||||
n.push_str(event_name);
|
||||
n
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
//! Collection of typed events.
|
||||
//! Types for all DOM events.
|
||||
|
||||
use std::{borrow::Cow, marker::PhantomData};
|
||||
use wasm_bindgen::convert::FromWasmAbi;
|
||||
|
||||
/// A trait for converting types into [web_sys events](web_sys).
|
||||
pub trait EventDescriptor: Clone {
|
||||
/// The [`web_sys`] event type, such as [`web_sys::MouseEvent`].
|
||||
type EventType: FromWasmAbi;
|
||||
/// The [`web_sys`] event type, such as [`web_sys::MouseEvent`].
|
||||
type EventType: FromWasmAbi;
|
||||
|
||||
/// The name of the event, such as `click` or `mouseover`.
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
/// The name of the event, such as `click` or `mouseover`.
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
|
||||
/// Indicates if this event bubbles. For example, `click` bubbles,
|
||||
/// but `focus` does not.
|
||||
///
|
||||
/// If this method returns true, then the event will be delegated globally,
|
||||
/// otherwise, event listeners will be directly attached to the element.
|
||||
fn bubbles(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// Indicates if this event bubbles. For example, `click` bubbles,
|
||||
/// but `focus` does not.
|
||||
///
|
||||
/// If this method returns true, then the event will be delegated globally,
|
||||
/// otherwise, event listeners will be directly attached to the element.
|
||||
fn bubbles(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides the [`EventDescriptor::bubbles`] method to always return
|
||||
@@ -28,54 +28,54 @@ pub trait EventDescriptor: Clone {
|
||||
pub struct undelegated<Ev: EventDescriptor>(pub Ev);
|
||||
|
||||
impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
|
||||
type EventType = Ev::EventType;
|
||||
type EventType = Ev::EventType;
|
||||
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
self.0.name()
|
||||
}
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
self.0.name()
|
||||
}
|
||||
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom event.
|
||||
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
|
||||
name: Cow<'static, str>,
|
||||
_event_type: PhantomData<E>,
|
||||
name: Cow<'static, str>,
|
||||
_event_type: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E: FromWasmAbi> Clone for Custom<E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
_event_type: PhantomData,
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
_event_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
|
||||
type EventType = E;
|
||||
type EventType = E;
|
||||
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FromWasmAbi> Custom<E> {
|
||||
/// Creates a custom event type that can be used within
|
||||
/// [`HtmlElement::on`](crate::HtmlElement::on), for events
|
||||
/// which are not covered in the [`ev`](crate::ev) module.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
_event_type: PhantomData,
|
||||
/// Creates a custom event type that can be used within
|
||||
/// [`HtmlElement::on`](crate::HtmlElement::on), for events
|
||||
/// which are not covered in the [`ev`](crate::ev) module.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
_event_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! generate_event_types {
|
||||
@@ -268,3 +268,13 @@ generate_event_types! {
|
||||
readystatechange: Event,
|
||||
visibilitychange: Event,
|
||||
}
|
||||
|
||||
// Export `web_sys` event types
|
||||
pub use web_sys::{
|
||||
AnimationEvent, BeforeUnloadEvent, CompositionEvent, DeviceMotionEvent,
|
||||
DeviceOrientationEvent, DragEvent, ErrorEvent, FocusEvent, GamepadEvent,
|
||||
HashChangeEvent, InputEvent, KeyboardEvent, MouseEvent,
|
||||
PageTransitionEvent, PointerEvent, PopStateEvent, ProgressEvent,
|
||||
PromiseRejectionEvent, SecurityPolicyViolationEvent, StorageEvent,
|
||||
SubmitEvent, TouchEvent, TransitionEvent, UiEvent, WheelEvent,
|
||||
};
|
||||
|
||||
@@ -1,56 +1,58 @@
|
||||
//! A variety of DOM utility functions.
|
||||
|
||||
use crate::{is_server, window};
|
||||
use std::time::Duration;
|
||||
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
|
||||
|
||||
/// Sets a property on a DOM element.
|
||||
pub fn set_property(
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
value: &Option<JsValue>,
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
value: &Option<JsValue>,
|
||||
) {
|
||||
let key = JsValue::from_str(prop_name);
|
||||
match value {
|
||||
Some(value) => _ = js_sys::Reflect::set(el, &key, value),
|
||||
None => _ = js_sys::Reflect::delete_property(el, &key),
|
||||
};
|
||||
let key = JsValue::from_str(prop_name);
|
||||
match value {
|
||||
Some(value) => _ = js_sys::Reflect::set(el, &key, value),
|
||||
None => _ = js_sys::Reflect::delete_property(el, &key),
|
||||
};
|
||||
}
|
||||
|
||||
/// Gets the value of a property set on a DOM element.
|
||||
pub fn get_property(
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let key = JsValue::from_str(prop_name);
|
||||
js_sys::Reflect::get(el, &key)
|
||||
let key = JsValue::from_str(prop_name);
|
||||
js_sys::Reflect::get(el, &key)
|
||||
}
|
||||
|
||||
/// Returns the current [`window.location`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
|
||||
pub fn location() -> web_sys::Location {
|
||||
window().location()
|
||||
window().location()
|
||||
}
|
||||
|
||||
/// Current [`window.location.hash`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location)
|
||||
/// without the beginning #.
|
||||
pub fn location_hash() -> Option<String> {
|
||||
if is_server() {
|
||||
None
|
||||
} else {
|
||||
location().hash().ok().map(|hash| hash.replace('#', ""))
|
||||
}
|
||||
if is_server() {
|
||||
None
|
||||
} else {
|
||||
location().hash().ok().map(|hash| hash.replace('#', ""))
|
||||
}
|
||||
}
|
||||
|
||||
/// Current [`window.location.pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
|
||||
pub fn location_pathname() -> Option<String> {
|
||||
location().pathname().ok()
|
||||
location().pathname().ok()
|
||||
}
|
||||
|
||||
/// Helper function to extract [`Event.target`](https://developer.mozilla.org/en-US/docs/Web/API/Event/target)
|
||||
/// from any event.
|
||||
pub fn event_target<T>(event: &web_sys::Event) -> T
|
||||
where
|
||||
T: JsCast,
|
||||
T: JsCast,
|
||||
{
|
||||
event.target().unwrap_throw().unchecked_into::<T>()
|
||||
event.target().unwrap_throw().unchecked_into::<T>()
|
||||
}
|
||||
|
||||
/// Helper function to extract `event.target.value` from an event.
|
||||
@@ -58,60 +60,60 @@ where
|
||||
/// This is useful in the `on:input` or `on:change` listeners for an `<input>` element.
|
||||
pub fn event_target_value<T>(event: &T) -> String
|
||||
where
|
||||
T: JsCast,
|
||||
T: JsCast,
|
||||
{
|
||||
event
|
||||
.unchecked_ref::<web_sys::Event>()
|
||||
.target()
|
||||
.unwrap_throw()
|
||||
.unchecked_into::<web_sys::HtmlInputElement>()
|
||||
.value()
|
||||
event
|
||||
.unchecked_ref::<web_sys::Event>()
|
||||
.target()
|
||||
.unwrap_throw()
|
||||
.unchecked_into::<web_sys::HtmlInputElement>()
|
||||
.value()
|
||||
}
|
||||
|
||||
/// Helper function to extract `event.target.checked` from an event.
|
||||
///
|
||||
/// This is useful in the `on:change` listeners for an `<input type="checkbox">` element.
|
||||
pub fn event_target_checked(ev: &web_sys::Event) -> bool {
|
||||
ev.target()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlInputElement>()
|
||||
.checked()
|
||||
ev.target()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlInputElement>()
|
||||
.checked()
|
||||
}
|
||||
|
||||
/// Runs the given function between the next repaint
|
||||
/// using [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::once_into_js(cb);
|
||||
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
|
||||
let cb = Closure::once_into_js(cb);
|
||||
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
|
||||
}
|
||||
|
||||
/// Queues the given function during an idle period
|
||||
/// using [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
pub fn request_idle_callback(cb: impl Fn() + 'static) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
|
||||
}
|
||||
|
||||
/// Executes the given function after the given duration of time has passed.
|
||||
@@ -121,21 +123,21 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
|
||||
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
);
|
||||
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
|
||||
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle that is generated by [set_interval] and can be used to clear the interval.
|
||||
@@ -143,11 +145,11 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
pub struct IntervalHandle(i32);
|
||||
|
||||
impl IntervalHandle {
|
||||
/// Cancels the repeating event to which this refers.
|
||||
/// See [`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)
|
||||
pub fn clear(&self) {
|
||||
window().clear_interval_with_handle(self.0);
|
||||
}
|
||||
/// Cancels the repeating event to which this refers.
|
||||
/// See [`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)
|
||||
pub fn clear(&self) {
|
||||
window().clear_interval_with_handle(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls.
|
||||
@@ -157,26 +159,26 @@ impl IntervalHandle {
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_interval(
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
) -> Result<IntervalHandle, JsValue> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
let handle = window()
|
||||
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)?;
|
||||
Ok(IntervalHandle(handle))
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
let handle = window()
|
||||
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)?;
|
||||
Ok(IntervalHandle(handle))
|
||||
}
|
||||
|
||||
/// Adds an event listener to the `Window`.
|
||||
@@ -185,34 +187,34 @@ pub fn set_interval(
|
||||
instrument(level = "trace", skip_all, fields(event_name = %event_name))
|
||||
)]
|
||||
pub fn window_event_listener(
|
||||
event_name: &str,
|
||||
cb: impl Fn(web_sys::Event) + 'static,
|
||||
event_name: &str,
|
||||
cb: impl Fn(web_sys::Event) + 'static,
|
||||
) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !is_server() {
|
||||
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
|
||||
if !is_server() {
|
||||
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
|
||||
|
||||
let cb = Closure::wrap(handler).into_js_value();
|
||||
_ =
|
||||
window().add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
}
|
||||
let cb = Closure::wrap(handler).into_js_value();
|
||||
_ = window()
|
||||
.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// This exists only to enable type inference on event listeners when in SSR mode.
|
||||
pub fn ssr_event_listener<E: crate::ev::EventDescriptor + 'static>(
|
||||
event: E,
|
||||
event_handler: impl FnMut(E::EventType) + 'static,
|
||||
event: E,
|
||||
event_handler: impl FnMut(E::EventType) + 'static,
|
||||
) {
|
||||
_ = event;
|
||||
_ = event_handler;
|
||||
_ = event;
|
||||
_ = event_handler;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -51,25 +51,25 @@ cfg_if! {
|
||||
/// A stable identifer within the server-rendering or hydration process.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct HydrationKey {
|
||||
/// The key of the previous component.
|
||||
pub previous: String,
|
||||
/// The element offset within the current component.
|
||||
pub offset: usize,
|
||||
/// The key of the previous component.
|
||||
pub previous: String,
|
||||
/// The element offset within the current component.
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
impl Display for HydrationKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}{}", self.previous, self.offset)
|
||||
}
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}{}", self.previous, self.offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HydrationKey {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
previous: "0-".to_string(),
|
||||
offset: 0,
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
previous: "0-".to_string(),
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local!(static ID: RefCell<HydrationKey> = Default::default());
|
||||
@@ -78,65 +78,65 @@ thread_local!(static ID: RefCell<HydrationKey> = Default::default());
|
||||
pub struct HydrationCtx;
|
||||
|
||||
impl HydrationCtx {
|
||||
/// Get the next `id` without incrementing it.
|
||||
pub fn peek() -> HydrationKey {
|
||||
ID.with(|id| id.borrow().clone())
|
||||
}
|
||||
|
||||
/// Increments the current hydration `id` and returns it
|
||||
pub fn id() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.offset = id.offset.wrapping_add(1);
|
||||
id.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// Resets the hydration `id` for the next component, and returns it
|
||||
pub fn next_component() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
let offset = id.offset;
|
||||
id.previous.push_str(&offset.to_string());
|
||||
id.previous.push('-');
|
||||
id.offset = 0;
|
||||
id.clone()
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub fn reset_id() {
|
||||
ID.with(|id| *id.borrow_mut() = Default::default());
|
||||
}
|
||||
|
||||
/// Resums hydration from the provided `id`. Usefull for
|
||||
/// `Suspense` and other fancy things.
|
||||
pub fn continue_from(id: HydrationKey) {
|
||||
ID.with(|i| *i.borrow_mut() = id);
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn stop_hydrating() {
|
||||
IS_HYDRATING.with(|is_hydrating| {
|
||||
std::mem::take(&mut *is_hydrating.borrow_mut());
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn is_hydrating() -> bool {
|
||||
IS_HYDRATING.with(|is_hydrating| **is_hydrating.borrow())
|
||||
}
|
||||
|
||||
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
|
||||
#[cfg(debug_assertions)]
|
||||
return format!("_{id}{}", if closing { 'c' } else { 'o' });
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let _ = closing;
|
||||
|
||||
format!("_{id}")
|
||||
/// Get the next `id` without incrementing it.
|
||||
pub fn peek() -> HydrationKey {
|
||||
ID.with(|id| id.borrow().clone())
|
||||
}
|
||||
|
||||
/// Increments the current hydration `id` and returns it
|
||||
pub fn id() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.offset = id.offset.wrapping_add(1);
|
||||
id.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// Resets the hydration `id` for the next component, and returns it
|
||||
pub fn next_component() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
let offset = id.offset;
|
||||
id.previous.push_str(&offset.to_string());
|
||||
id.previous.push('-');
|
||||
id.offset = 0;
|
||||
id.clone()
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub fn reset_id() {
|
||||
ID.with(|id| *id.borrow_mut() = Default::default());
|
||||
}
|
||||
|
||||
/// Resumes hydration from the provided `id`. Useful for
|
||||
/// `Suspense` and other fancy things.
|
||||
pub fn continue_from(id: HydrationKey) {
|
||||
ID.with(|i| *i.borrow_mut() = id);
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn stop_hydrating() {
|
||||
IS_HYDRATING.with(|is_hydrating| {
|
||||
std::mem::take(&mut *is_hydrating.borrow_mut());
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn is_hydrating() -> bool {
|
||||
IS_HYDRATING.with(|is_hydrating| **is_hydrating.borrow())
|
||||
}
|
||||
|
||||
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
|
||||
#[cfg(debug_assertions)]
|
||||
return format!("_{id}{}", if closing { 'c' } else { 'o' });
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let _ = closing;
|
||||
|
||||
format!("_{id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,45 +44,45 @@ macro_rules! debug_warn {
|
||||
/// Log a string to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser).
|
||||
pub fn console_log(s: &str) {
|
||||
if is_server() {
|
||||
println!("{s}");
|
||||
} else {
|
||||
web_sys::console::log_1(&JsValue::from_str(s));
|
||||
}
|
||||
if is_server() {
|
||||
println!("{s}");
|
||||
} else {
|
||||
web_sys::console::log_1(&JsValue::from_str(s));
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a warning to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser).
|
||||
pub fn console_warn(s: &str) {
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an error to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser).
|
||||
pub fn console_error(s: &str) {
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an error to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser), but only in a debug build.
|
||||
pub fn console_debug_warn(s: &str) {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
} else {
|
||||
let _ = s;
|
||||
}
|
||||
}
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
} else {
|
||||
let _ = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,158 +1,236 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use leptos_reactive::Scope;
|
||||
use std::rc::Rc;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
/// Represents the different possible values an attribute node could have.
|
||||
///
|
||||
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
|
||||
/// macro’s use. You usually won't need to interact with it directly.
|
||||
/// macro’s use. You usually won't need to interact with it directly, but it can be useful for defining
|
||||
/// permissive APIs for certain components.
|
||||
#[derive(Clone)]
|
||||
pub enum Attribute {
|
||||
/// A plain string value.
|
||||
String(String),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
|
||||
Fn(Scope, Rc<dyn Fn() -> Attribute>),
|
||||
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
|
||||
Option(Scope, Option<String>),
|
||||
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
|
||||
Bool(bool),
|
||||
/// A plain string value.
|
||||
String(String),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
|
||||
Fn(Scope, Rc<dyn Fn() -> Attribute>),
|
||||
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
|
||||
Option(Scope, Option<String>),
|
||||
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
impl Attribute {
|
||||
/// Converts the attribute to its HTML value at that moment, including the attribute name,
|
||||
/// so it can be rendered on the server.
|
||||
pub fn as_value_string(&self, attr_name: &'static str) -> String {
|
||||
match self {
|
||||
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
|
||||
Attribute::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(_, f) = value {
|
||||
value = f();
|
||||
/// Converts the attribute to its HTML value at that moment, including the attribute name,
|
||||
/// so it can be rendered on the server.
|
||||
pub fn as_value_string(&self, attr_name: &'static str) -> String {
|
||||
match self {
|
||||
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
|
||||
Attribute::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(_, f) = value {
|
||||
value = f();
|
||||
}
|
||||
value.as_value_string(attr_name)
|
||||
}
|
||||
Attribute::Option(_, value) => value
|
||||
.as_ref()
|
||||
.map(|value| format!("{attr_name}=\"{value}\""))
|
||||
.unwrap_or_default(),
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
attr_name.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
value.as_value_string(attr_name)
|
||||
}
|
||||
Attribute::Option(_, value) => value
|
||||
.as_ref()
|
||||
.map(|value| format!("{attr_name}=\"{value}\""))
|
||||
.unwrap_or_default(),
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
attr_name.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the attribute to its HTML value at that moment, not including
|
||||
/// the attribute name, so it can be rendered on the server.
|
||||
pub fn as_nameless_value_string(&self) -> Option<String> {
|
||||
match self {
|
||||
Attribute::String(value) => Some(value.to_string()),
|
||||
Attribute::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(_, f) = value {
|
||||
value = f();
|
||||
/// Converts the attribute to its HTML value at that moment, not including
|
||||
/// the attribute name, so it can be rendered on the server.
|
||||
pub fn as_nameless_value_string(&self) -> Option<String> {
|
||||
match self {
|
||||
Attribute::String(value) => Some(value.to_string()),
|
||||
Attribute::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(_, f) = value {
|
||||
value = f();
|
||||
}
|
||||
value.as_nameless_value_string()
|
||||
}
|
||||
Attribute::Option(_, value) => {
|
||||
value.as_ref().map(|value| value.to_string())
|
||||
}
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
Some("".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
value.as_nameless_value_string()
|
||||
}
|
||||
Attribute::Option(_, value) => {
|
||||
value.as_ref().map(|value| value.to_string())
|
||||
}
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
Some("".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Attribute {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::String(l0), Self::String(r0)) => l0 == r0,
|
||||
(Self::Fn(_, _), Self::Fn(_, _)) => false,
|
||||
(Self::Option(_, l0), Self::Option(_, r0)) => l0 == r0,
|
||||
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::String(l0), Self::String(r0)) => l0 == r0,
|
||||
(Self::Fn(_, _), Self::Fn(_, _)) => false,
|
||||
(Self::Option(_, l0), Self::Option(_, r0)) => l0 == r0,
|
||||
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Attribute {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
|
||||
Self::Fn(_, _) => f.debug_tuple("Fn").finish(),
|
||||
Self::Option(_, arg0) => f.debug_tuple("Option").field(arg0).finish(),
|
||||
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
|
||||
Self::Fn(_, _) => f.debug_tuple("Fn").finish(),
|
||||
Self::Option(_, arg0) => {
|
||||
f.debug_tuple("Option").field(arg0).finish()
|
||||
}
|
||||
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts some type into an [Attribute].
|
||||
///
|
||||
/// This is implemented by default for Rust primitive and string types.
|
||||
pub trait IntoAttribute {
|
||||
/// Converts the object into an [Attribute].
|
||||
fn into_attribute(self, cx: Scope) -> Attribute;
|
||||
/// Converts the object into an [Attribute].
|
||||
fn into_attribute(self, cx: Scope) -> Attribute;
|
||||
/// Helper function for dealing with `Box<dyn IntoAttribute>`.
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute;
|
||||
}
|
||||
|
||||
impl<T: IntoAttribute + 'static> From<T> for Box<dyn IntoAttribute> {
|
||||
fn from(value: T) -> Self {
|
||||
Box::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Attribute {
|
||||
#[inline]
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, _: Scope) -> Attribute {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_into_attr_boxed {
|
||||
() => {
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
self.into_attribute(cx)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Attribute> {
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
self.unwrap_or(Attribute::Option(cx, None))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for String {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(self)
|
||||
}
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(self)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for bool {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::Bool(self)
|
||||
}
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::Bool(self)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<String> {
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self)
|
||||
}
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl<T, U> IntoAttribute for T
|
||||
where
|
||||
T: Fn() -> U + 'static,
|
||||
U: IntoAttribute,
|
||||
T: Fn() -> U + 'static,
|
||||
U: IntoAttribute,
|
||||
{
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
let modified_fn = Rc::new(move || (self)().into_attribute(cx));
|
||||
Attribute::Fn(cx, modified_fn)
|
||||
}
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
let modified_fn = Rc::new(move || (self)().into_attribute(cx));
|
||||
Attribute::Fn(cx, modified_fn)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl<T: IntoAttribute> IntoAttribute for (Scope, T) {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self.1.into_attribute(self.0)
|
||||
}
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self.1.into_attribute(self.0)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for (Scope, Option<Box<dyn IntoAttribute>>) {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
match self.1 {
|
||||
Some(bx) => bx.into_attribute_boxed(self.0),
|
||||
None => Attribute::Option(self.0, None),
|
||||
}
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for (Scope, Box<dyn IntoAttribute>) {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self.1.into_attribute_boxed(self.0)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
macro_rules! attr_type {
|
||||
($attr_type:ty) => {
|
||||
impl IntoAttribute for $attr_type {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(self.to_string())
|
||||
}
|
||||
}
|
||||
($attr_type:ty) => {
|
||||
impl IntoAttribute for $attr_type {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(self.to_string())
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<$attr_type> {
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self.map(|n| n.to_string()))
|
||||
}
|
||||
}
|
||||
};
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
self.into_attribute(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<$attr_type> {
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self.map(|n| n.to_string()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
self.into_attribute(cx)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
attr_type!(&String);
|
||||
@@ -176,65 +254,66 @@ attr_type!(char);
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::borrow::Cow;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn attribute_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Attribute,
|
||||
#[doc(hidden)]
|
||||
pub fn attribute_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Attribute,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
match value {
|
||||
Attribute::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) {
|
||||
attribute_expression(&el, &name, new.clone());
|
||||
use leptos_reactive::create_render_effect;
|
||||
match value {
|
||||
Attribute::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) {
|
||||
attribute_expression(&el, &name, new.clone());
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
_ => attribute_expression(el, &name, value),
|
||||
};
|
||||
_ => attribute_expression(el, &name, value),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn attribute_expression(
|
||||
el: &web_sys::Element,
|
||||
attr_name: &str,
|
||||
value: Attribute,
|
||||
el: &web_sys::Element,
|
||||
attr_name: &str,
|
||||
value: Attribute,
|
||||
) {
|
||||
match value {
|
||||
Attribute::String(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(value);
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
}
|
||||
Attribute::Option(_, value) => {
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(&value.unwrap_or_default());
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
match value {
|
||||
Some(value) => {
|
||||
match value {
|
||||
Attribute::String(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
None => el.remove_attribute(attr_name).unwrap_throw(),
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(value);
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
}
|
||||
}
|
||||
Attribute::Option(_, value) => {
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(&value.unwrap_or_default());
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
match value {
|
||||
Some(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
None => el.remove_attribute(attr_name).unwrap_throw(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Attribute::Bool(value) => {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
if value {
|
||||
el.set_attribute(attr_name, attr_name).unwrap_throw();
|
||||
} else {
|
||||
el.remove_attribute(attr_name).unwrap_throw();
|
||||
}
|
||||
}
|
||||
_ => panic!("Remove nested Fn in Attribute"),
|
||||
}
|
||||
Attribute::Bool(value) => {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
if value {
|
||||
el.set_attribute(attr_name, attr_name).unwrap_throw();
|
||||
} else {
|
||||
el.remove_attribute(attr_name).unwrap_throw();
|
||||
}
|
||||
}
|
||||
_ => panic!("Remove nested Fn in Attribute"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,101 +7,103 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
/// in [`Element.classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList).
|
||||
///
|
||||
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
|
||||
/// macro’s use. You usually won't need to interact with it directly.
|
||||
/// macro’s use. You usually won't need to interact with it directly, but it can be useful for defining
|
||||
/// permissive APIs for certain components.
|
||||
pub enum Class {
|
||||
/// Whether the class is present.
|
||||
Value(bool),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
|
||||
Fn(Scope, Box<dyn Fn() -> bool>),
|
||||
/// Whether the class is present.
|
||||
Value(bool),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
|
||||
Fn(Scope, Box<dyn Fn() -> bool>),
|
||||
}
|
||||
|
||||
/// Converts some type into a [Class].
|
||||
pub trait IntoClass {
|
||||
/// Converts the object into a [Class].
|
||||
fn into_class(self, cx: Scope) -> Class;
|
||||
/// Converts the object into a [Class].
|
||||
fn into_class(self, cx: Scope) -> Class;
|
||||
}
|
||||
|
||||
impl IntoClass for bool {
|
||||
fn into_class(self, _cx: Scope) -> Class {
|
||||
Class::Value(self)
|
||||
}
|
||||
fn into_class(self, _cx: Scope) -> Class {
|
||||
Class::Value(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoClass for T
|
||||
where
|
||||
T: Fn() -> bool + 'static,
|
||||
T: Fn() -> bool + 'static,
|
||||
{
|
||||
fn into_class(self, cx: Scope) -> Class {
|
||||
let modified_fn = Box::new(self);
|
||||
Class::Fn(cx, modified_fn)
|
||||
}
|
||||
fn into_class(self, cx: Scope) -> Class {
|
||||
let modified_fn = Box::new(self);
|
||||
Class::Fn(cx, modified_fn)
|
||||
}
|
||||
}
|
||||
|
||||
impl Class {
|
||||
/// Converts the class to its HTML value at that moment so it can be rendered on the server.
|
||||
pub fn as_value_string(&self, class_name: &'static str) -> &'static str {
|
||||
match self {
|
||||
Class::Value(value) => {
|
||||
if *value {
|
||||
class_name
|
||||
} else {
|
||||
""
|
||||
/// Converts the class to its HTML value at that moment so it can be rendered on the server.
|
||||
pub fn as_value_string(&self, class_name: &'static str) -> &'static str {
|
||||
match self {
|
||||
Class::Value(value) => {
|
||||
if *value {
|
||||
class_name
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
Class::Fn(_, f) => {
|
||||
let value = f();
|
||||
if value {
|
||||
class_name
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Class::Fn(_, f) => {
|
||||
let value = f();
|
||||
if value {
|
||||
class_name
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoClass> IntoClass for (Scope, T) {
|
||||
fn into_class(self, _: Scope) -> Class {
|
||||
self.1.into_class(self.0)
|
||||
}
|
||||
fn into_class(self, _: Scope) -> Class {
|
||||
self.1.into_class(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn class_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Class,
|
||||
#[doc(hidden)]
|
||||
pub fn class_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Class,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
let class_list = el.class_list();
|
||||
match value {
|
||||
Class::Fn(cx, f) => {
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) && (old.is_some() || new) {
|
||||
class_expression(&class_list, &name, new)
|
||||
let class_list = el.class_list();
|
||||
match value {
|
||||
Class::Fn(cx, f) => {
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) && (old.is_some() || new) {
|
||||
class_expression(&class_list, &name, new)
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Class::Value(value) => class_expression(&class_list, &name, value),
|
||||
};
|
||||
Class::Value(value) => class_expression(&class_list, &name, value),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn class_expression(
|
||||
class_list: &web_sys::DomTokenList,
|
||||
class_name: &str,
|
||||
value: bool,
|
||||
class_list: &web_sys::DomTokenList,
|
||||
class_name: &str,
|
||||
value: bool,
|
||||
) {
|
||||
let class_name = wasm_bindgen::intern(class_name);
|
||||
if value {
|
||||
class_list.add_1(class_name).unwrap_throw();
|
||||
} else {
|
||||
class_list.remove_1(class_name).unwrap_throw();
|
||||
}
|
||||
let class_name = wasm_bindgen::intern(class_name);
|
||||
if value {
|
||||
class_list.add_1(class_name).unwrap_throw();
|
||||
} else {
|
||||
class_list.remove_1(class_name).unwrap_throw();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,53 +7,54 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
/// allowing you to do fine-grained updates to single fields.
|
||||
///
|
||||
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
|
||||
/// macro’s use. You usually won't need to interact with it directly.
|
||||
/// macro’s use. You usually won't need to interact with it directly, but it can be useful for defining
|
||||
/// permissive APIs for certain components.
|
||||
pub enum Property {
|
||||
/// A static JavaScript value.
|
||||
Value(JsValue),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
|
||||
Fn(Scope, Box<dyn Fn() -> JsValue>),
|
||||
/// A static JavaScript value.
|
||||
Value(JsValue),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
|
||||
Fn(Scope, Box<dyn Fn() -> JsValue>),
|
||||
}
|
||||
|
||||
/// Converts some type into a [Property].
|
||||
///
|
||||
/// This is implemented by default for Rust primitive types, [String] and friends, and [JsValue].
|
||||
pub trait IntoProperty {
|
||||
/// Converts the object into a [Property].
|
||||
fn into_property(self, cx: Scope) -> Property;
|
||||
/// Converts the object into a [Property].
|
||||
fn into_property(self, cx: Scope) -> Property;
|
||||
}
|
||||
|
||||
impl<T, U> IntoProperty for T
|
||||
where
|
||||
T: Fn() -> U + 'static,
|
||||
U: Into<JsValue>,
|
||||
T: Fn() -> U + 'static,
|
||||
U: Into<JsValue>,
|
||||
{
|
||||
fn into_property(self, cx: Scope) -> Property {
|
||||
let modified_fn = Box::new(move || self().into());
|
||||
Property::Fn(cx, modified_fn)
|
||||
}
|
||||
fn into_property(self, cx: Scope) -> Property {
|
||||
let modified_fn = Box::new(move || self().into());
|
||||
Property::Fn(cx, modified_fn)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoProperty> IntoProperty for (Scope, T) {
|
||||
fn into_property(self, _: Scope) -> Property {
|
||||
self.1.into_property(self.0)
|
||||
}
|
||||
fn into_property(self, _: Scope) -> Property {
|
||||
self.1.into_property(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! prop_type {
|
||||
($prop_type:ty) => {
|
||||
impl IntoProperty for $prop_type {
|
||||
fn into_property(self, _cx: Scope) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
}
|
||||
($prop_type:ty) => {
|
||||
impl IntoProperty for $prop_type {
|
||||
fn into_property(self, _cx: Scope) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoProperty for Option<$prop_type> {
|
||||
fn into_property(self, _cx: Scope) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
impl IntoProperty for Option<$prop_type> {
|
||||
fn into_property(self, _cx: Scope) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
prop_type!(JsValue);
|
||||
@@ -81,39 +82,40 @@ use std::borrow::Cow;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn property_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Property,
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Property,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
match value {
|
||||
Property::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
if old.as_ref() != Some(&new)
|
||||
&& !(old.is_none() && new == wasm_bindgen::JsValue::UNDEFINED)
|
||||
{
|
||||
property_expression(&el, prop_name, new.clone())
|
||||
match value {
|
||||
Property::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
if old.as_ref() != Some(&new)
|
||||
&& !(old.is_none()
|
||||
&& new == wasm_bindgen::JsValue::UNDEFINED)
|
||||
{
|
||||
property_expression(&el, prop_name, new.clone())
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Property::Value(value) => {
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
property_expression(el, prop_name, value)
|
||||
}
|
||||
};
|
||||
Property::Value(value) => {
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
property_expression(el, prop_name, value)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn property_expression(
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
value: JsValue,
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
value: JsValue,
|
||||
) {
|
||||
js_sys::Reflect::set(el, &JsValue::from_str(prop_name), &value)
|
||||
.unwrap_throw();
|
||||
js_sys::Reflect::set(el, &JsValue::from_str(prop_name), &value)
|
||||
.unwrap_throw();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! MathML elements.
|
||||
//! Exports types for working with MathML elements.
|
||||
|
||||
use super::{ElementDescriptor, HtmlElement};
|
||||
use crate::HydrationCtx;
|
||||
@@ -10,7 +10,7 @@ cfg_if! {
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
use wasm_bindgen::JsCast;
|
||||
} else {
|
||||
use super::{HydrationKey, HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
|
||||
use super::{HydrationKey, html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,69 +178,69 @@ macro_rules! generate_math_tags {
|
||||
}
|
||||
|
||||
generate_math_tags![
|
||||
/// MathML element.
|
||||
math,
|
||||
/// MathML element.
|
||||
mi,
|
||||
/// MathML element.
|
||||
mn,
|
||||
/// MathML element.
|
||||
mo,
|
||||
/// MathML element.
|
||||
ms,
|
||||
/// MathML element.
|
||||
mspace,
|
||||
/// MathML element.
|
||||
mtext,
|
||||
/// MathML element.
|
||||
menclose,
|
||||
/// MathML element.
|
||||
merror,
|
||||
/// MathML element.
|
||||
mfenced,
|
||||
/// MathML element.
|
||||
mfrac,
|
||||
/// MathML element.
|
||||
mpadded,
|
||||
/// MathML element.
|
||||
mphantom,
|
||||
/// MathML element.
|
||||
mroot,
|
||||
/// MathML element.
|
||||
mrow,
|
||||
/// MathML element.
|
||||
msqrt,
|
||||
/// MathML element.
|
||||
mstyle,
|
||||
/// MathML element.
|
||||
mmultiscripts,
|
||||
/// MathML element.
|
||||
mover,
|
||||
/// MathML element.
|
||||
mprescripts,
|
||||
/// MathML element.
|
||||
msub,
|
||||
/// MathML element.
|
||||
msubsup,
|
||||
/// MathML element.
|
||||
msup,
|
||||
/// MathML element.
|
||||
munder,
|
||||
/// MathML element.
|
||||
munderover,
|
||||
/// MathML element.
|
||||
mtable,
|
||||
/// MathML element.
|
||||
mtd,
|
||||
/// MathML element.
|
||||
mtr,
|
||||
/// MathML element.
|
||||
maction,
|
||||
/// MathML element.
|
||||
annotation,
|
||||
/// MathML element.
|
||||
annotation
|
||||
- xml,
|
||||
/// MathML element.
|
||||
semantics,
|
||||
/// MathML element.
|
||||
math,
|
||||
/// MathML element.
|
||||
mi,
|
||||
/// MathML element.
|
||||
mn,
|
||||
/// MathML element.
|
||||
mo,
|
||||
/// MathML element.
|
||||
ms,
|
||||
/// MathML element.
|
||||
mspace,
|
||||
/// MathML element.
|
||||
mtext,
|
||||
/// MathML element.
|
||||
menclose,
|
||||
/// MathML element.
|
||||
merror,
|
||||
/// MathML element.
|
||||
mfenced,
|
||||
/// MathML element.
|
||||
mfrac,
|
||||
/// MathML element.
|
||||
mpadded,
|
||||
/// MathML element.
|
||||
mphantom,
|
||||
/// MathML element.
|
||||
mroot,
|
||||
/// MathML element.
|
||||
mrow,
|
||||
/// MathML element.
|
||||
msqrt,
|
||||
/// MathML element.
|
||||
mstyle,
|
||||
/// MathML element.
|
||||
mmultiscripts,
|
||||
/// MathML element.
|
||||
mover,
|
||||
/// MathML element.
|
||||
mprescripts,
|
||||
/// MathML element.
|
||||
msub,
|
||||
/// MathML element.
|
||||
msubsup,
|
||||
/// MathML element.
|
||||
msup,
|
||||
/// MathML element.
|
||||
munder,
|
||||
/// MathML element.
|
||||
munderover,
|
||||
/// MathML element.
|
||||
mtable,
|
||||
/// MathML element.
|
||||
mtd,
|
||||
/// MathML element.
|
||||
mtr,
|
||||
/// MathML element.
|
||||
maction,
|
||||
/// MathML element.
|
||||
annotation,
|
||||
/// MathML element.
|
||||
annotation
|
||||
- xml,
|
||||
/// MathML element.
|
||||
semantics,
|
||||
];
|
||||
@@ -1,91 +1,165 @@
|
||||
use leptos_reactive::{create_rw_signal, RwSignal, Scope};
|
||||
use crate::{html::ElementDescriptor, HtmlElement};
|
||||
use leptos_reactive::{
|
||||
create_effect, create_rw_signal, signal_prelude::*, RwSignal, Scope,
|
||||
};
|
||||
use std::cell::Cell;
|
||||
|
||||
/// Contains a shared reference to a DOM node creating while using the `view`
|
||||
/// Contains a shared reference to a DOM node created while using the `view`
|
||||
/// macro to create your UI.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
///
|
||||
/// use leptos::html::Input;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
/// let input_ref = NodeRef::<HtmlElement<Input>>::new(cx);
|
||||
/// let input_ref = create_node_ref::<Input>(cx);
|
||||
///
|
||||
/// let on_click = move |_| {
|
||||
/// let node = input_ref
|
||||
/// .get()
|
||||
/// .expect("input_ref should be loaded by now");
|
||||
/// // `node` is strongly typed
|
||||
/// // it is dereferenced to an `HtmlInputElement` automatically
|
||||
/// log!("value is {:?}", node.value())
|
||||
/// };
|
||||
/// let on_click = move |_| {
|
||||
/// let node =
|
||||
/// input_ref.get().expect("input_ref should be loaded by now");
|
||||
/// // `node` is strongly typed
|
||||
/// // it is dereferenced to an `HtmlInputElement` automatically
|
||||
/// log!("value is {:?}", node.value())
|
||||
/// };
|
||||
///
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <div>
|
||||
/// // `node_ref` loads the input
|
||||
/// <input _ref=input_ref type="text"/>
|
||||
/// // the button consumes it
|
||||
/// <button on:click=on_click>"Click me"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <div>
|
||||
/// // `node_ref` loads the input
|
||||
/// <input _ref=input_ref type="text"/>
|
||||
/// // the button consumes it
|
||||
/// <button on:click=on_click>"Click me"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct NodeRef<T: Clone + 'static>(RwSignal<Option<T>>);
|
||||
pub struct NodeRef<T: ElementDescriptor + 'static>(
|
||||
RwSignal<Option<HtmlElement<T>>>,
|
||||
);
|
||||
|
||||
impl<T: Clone + 'static> NodeRef<T> {
|
||||
/// Creates an empty reference.
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
Self(create_rw_signal(cx, None))
|
||||
}
|
||||
|
||||
/// Gets the element that is currently stored in the reference.
|
||||
///
|
||||
/// This tracks reactively, so that node references can be used in effects.
|
||||
/// Initially, the value will be `None`, but once it is loaded the effect
|
||||
/// will rerun and its value will be `Some(Element)`.
|
||||
#[track_caller]
|
||||
pub fn get(&self) -> Option<T> {
|
||||
self.0.get()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Loads an element into the reference. This tracks reactively,
|
||||
/// so that effects that use the node reference will rerun once it is loaded,
|
||||
/// i.e., effects can be forward-declared.
|
||||
#[track_caller]
|
||||
pub fn load(&self, node: &T) {
|
||||
self.0.update(|current| {
|
||||
if current.is_some() {
|
||||
crate::debug_warn!(
|
||||
"You are setting a NodeRef that has already been filled. It’s \
|
||||
possible this is intentional, but it’s also possible that you’re \
|
||||
accidentally using the same NodeRef for multiple _ref attributes."
|
||||
);
|
||||
}
|
||||
*current = Some(node.clone());
|
||||
});
|
||||
}
|
||||
/// Creates a shared reference to a DOM node created while using the `view`
|
||||
/// macro to create your UI.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
///
|
||||
/// use leptos::html::Input;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
/// let input_ref = create_node_ref::<Input>(cx);
|
||||
///
|
||||
/// let on_click = move |_| {
|
||||
/// let node =
|
||||
/// input_ref.get().expect("input_ref should be loaded by now");
|
||||
/// // `node` is strongly typed
|
||||
/// // it is dereferenced to an `HtmlInputElement` automatically
|
||||
/// log!("value is {:?}", node.value())
|
||||
/// };
|
||||
///
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <div>
|
||||
/// // `node_ref` loads the input
|
||||
/// <input _ref=input_ref type="text"/>
|
||||
/// // the button consumes it
|
||||
/// <button on:click=on_click>"Click me"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn create_node_ref<T: ElementDescriptor + 'static>(
|
||||
cx: Scope,
|
||||
) -> NodeRef<T> {
|
||||
NodeRef(create_rw_signal(cx, None))
|
||||
}
|
||||
|
||||
impl<T: Clone + 'static> Copy for NodeRef<T> {}
|
||||
impl<T: ElementDescriptor + 'static> NodeRef<T> {
|
||||
/// Creates an empty reference.
|
||||
#[deprecated = "Use `create_node_ref` instead of `NodeRef::new()`."]
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
Self(create_rw_signal(cx, None))
|
||||
}
|
||||
|
||||
/// Gets the element that is currently stored in the reference.
|
||||
///
|
||||
/// This tracks reactively, so that node references can be used in effects.
|
||||
/// Initially, the value will be `None`, but once it is loaded the effect
|
||||
/// will rerun and its value will be `Some(Element)`.
|
||||
#[track_caller]
|
||||
pub fn get(&self) -> Option<HtmlElement<T>>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.0.get()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Loads an element into the reference. This tracks reactively,
|
||||
/// so that effects that use the node reference will rerun once it is loaded,
|
||||
/// i.e., effects can be forward-declared.
|
||||
#[track_caller]
|
||||
pub fn load(&self, node: &HtmlElement<T>)
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.0.update(|current| {
|
||||
if current.is_some() {
|
||||
crate::debug_warn!(
|
||||
"You are setting a NodeRef that has already been filled. \
|
||||
It’s possible this is intentional, but it’s also \
|
||||
possible that you’re accidentally using the same NodeRef \
|
||||
for multiple _ref attributes."
|
||||
);
|
||||
}
|
||||
*current = Some(node.clone());
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs the provided closure when the `NodeRef` has been connected
|
||||
/// with it's [`HtmlElement`].
|
||||
pub fn on_load<F>(self, cx: Scope, f: F)
|
||||
where
|
||||
T: Clone,
|
||||
F: FnOnce(HtmlElement<T>) + 'static,
|
||||
{
|
||||
let f = Cell::new(Some(f));
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
if let Some(node_ref) = self.get() {
|
||||
f.take().unwrap()(node_ref);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ElementDescriptor> Clone for NodeRef<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ElementDescriptor + 'static> Copy for NodeRef<T> {}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(not(feature = "stable"))] {
|
||||
impl<T: Clone + 'static> FnOnce<()> for NodeRef<T> {
|
||||
type Output = Option<T>;
|
||||
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
|
||||
type Output = Option<HtmlElement<T>>;
|
||||
|
||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + 'static> FnMut<()> for NodeRef<T> {
|
||||
impl<T: Clone + ElementDescriptor + 'static> FnMut<()> for NodeRef<T> {
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + 'static> Fn<()> for NodeRef<T> {
|
||||
impl<T: Clone + ElementDescriptor + Clone + 'static> Fn<()> for NodeRef<T> {
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
#![cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
|
||||
//! Server-side HTML rendering utilities.
|
||||
|
||||
use crate::{CoreComponent, HydrationCtx, IntoView, View};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::{stream::FuturesUnordered, Stream, StreamExt};
|
||||
use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
|
||||
use itertools::Itertools;
|
||||
use leptos_reactive::*;
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, pin::Pin};
|
||||
|
||||
type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
|
||||
/// Renders the given function to a static HTML string.
|
||||
///
|
||||
/// ```
|
||||
/// # cfg_if::cfg_if! { if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
|
||||
/// # use leptos::*;
|
||||
/// let html = render_to_string(|cx| view! { cx,
|
||||
/// let html = leptos::ssr::render_to_string(|cx| view! { cx,
|
||||
/// <p>"Hello, world!"</p>
|
||||
/// });
|
||||
/// // static HTML includes some hydration info
|
||||
@@ -21,19 +25,19 @@ use std::borrow::Cow;
|
||||
/// ```
|
||||
pub fn render_to_string<F, N>(f: F) -> String
|
||||
where
|
||||
F: FnOnce(Scope) -> N + 'static,
|
||||
N: IntoView,
|
||||
F: FnOnce(Scope) -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
let runtime = leptos_reactive::create_runtime();
|
||||
HydrationCtx::reset_id();
|
||||
let runtime = leptos_reactive::create_runtime();
|
||||
HydrationCtx::reset_id();
|
||||
|
||||
let html = leptos_reactive::run_scope(runtime, |cx| {
|
||||
f(cx).into_view(cx).render_to_string(cx)
|
||||
});
|
||||
let html = leptos_reactive::run_scope(runtime, |cx| {
|
||||
f(cx).into_view(cx).render_to_string(cx)
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
html.into()
|
||||
html.into()
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings.
|
||||
@@ -49,9 +53,9 @@ where
|
||||
/// 3) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
pub fn render_to_stream(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
render_to_stream_with_prefix(view, |_| "".into())
|
||||
render_to_stream_with_prefix(view, |_| "".into())
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings. After the `view` runs, the `prefix` will run with
|
||||
@@ -69,13 +73,13 @@ pub fn render_to_stream(
|
||||
/// 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(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
let (stream, runtime, _) =
|
||||
render_to_stream_with_prefix_undisposed(view, prefix);
|
||||
runtime.dispose();
|
||||
stream
|
||||
let (stream, runtime, _) =
|
||||
render_to_stream_with_prefix_undisposed(view, prefix);
|
||||
runtime.dispose();
|
||||
stream
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
|
||||
@@ -94,10 +98,10 @@ pub fn render_to_stream_with_prefix(
|
||||
/// 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(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
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| {})
|
||||
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
|
||||
@@ -116,50 +120,50 @@ pub fn render_to_stream_with_prefix_undisposed(
|
||||
/// 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,
|
||||
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();
|
||||
HydrationCtx::reset_id();
|
||||
|
||||
// create the runtime
|
||||
let runtime = create_runtime();
|
||||
// create the runtime
|
||||
let runtime = create_runtime();
|
||||
|
||||
let (
|
||||
(shell, prefix, pending_resources, pending_fragments, serializers),
|
||||
scope,
|
||||
_,
|
||||
) = 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);
|
||||
let (
|
||||
(shell, prefix, pending_resources, pending_fragments, serializers),
|
||||
scope,
|
||||
_,
|
||||
) = 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);
|
||||
|
||||
let resources = cx.pending_resources();
|
||||
let pending_resources = serde_json::to_string(&resources).unwrap();
|
||||
let prefix = prefix(cx);
|
||||
let resources = cx.pending_resources();
|
||||
let pending_resources = serde_json::to_string(&resources).unwrap();
|
||||
let prefix = prefix(cx);
|
||||
|
||||
(
|
||||
shell,
|
||||
prefix,
|
||||
pending_resources,
|
||||
cx.pending_fragments(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
(
|
||||
shell,
|
||||
prefix,
|
||||
pending_resources,
|
||||
cx.pending_fragments(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let fragments = FuturesUnordered::new();
|
||||
for (fragment_id, (fut, _)) in pending_fragments {
|
||||
fragments.push(async move { (fragment_id, fut.await) })
|
||||
}
|
||||
});
|
||||
|
||||
let fragments = FuturesUnordered::new();
|
||||
for (fragment_id, (key_before, fut)) in pending_fragments {
|
||||
fragments.push(async move { (fragment_id, key_before, fut.await) })
|
||||
}
|
||||
|
||||
// resources and fragments
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
// TODO can remove id_before_suspense entirely now
|
||||
let fragments = fragments.map(|(fragment_id, _, html)| {
|
||||
// resources and fragments
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
// TODO can remove id_before_suspense entirely now
|
||||
let fragments = fragments.map(|(fragment_id, html)| {
|
||||
format!(
|
||||
r#"
|
||||
<template id="{fragment_id}f">{html}</template>
|
||||
@@ -185,24 +189,13 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
"#
|
||||
)
|
||||
});
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = serializers.map(|(id, json)| {
|
||||
let id = serde_json::to_string(&id).unwrap();
|
||||
format!(
|
||||
r#"<script>
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
|
||||
}} else {{
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
|
||||
}}
|
||||
</script>"#,
|
||||
)
|
||||
});
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = render_serializers(serializers);
|
||||
|
||||
// HTML for the view function and script to store resources
|
||||
let stream = futures::stream::once(async move {
|
||||
format!(
|
||||
r#"
|
||||
// HTML for the view function and script to store resources
|
||||
let stream = futures::stream::once(async move {
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
{shell}
|
||||
<script>
|
||||
@@ -211,258 +204,286 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
.chain(fragments)
|
||||
.chain(resources);
|
||||
)
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
.chain(fragments)
|
||||
.chain(resources);
|
||||
|
||||
(stream, runtime, scope)
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// Consumes the node and renders it into an HTML string.
|
||||
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
|
||||
self.render_to_string_helper()
|
||||
}
|
||||
/// Consumes the node and renders it into an HTML string.
|
||||
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
|
||||
self.render_to_string_helper()
|
||||
}
|
||||
|
||||
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
View::Text(node) => node.content,
|
||||
View::Component(node) => {
|
||||
let content = || {
|
||||
node
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|node| node.render_to_string_helper())
|
||||
.join("")
|
||||
};
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&node.id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true),
|
||||
name = to_kebab_case(&node.name)
|
||||
).into()
|
||||
} else {
|
||||
format!(
|
||||
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) => (
|
||||
u.id.clone(),
|
||||
"",
|
||||
false,
|
||||
Box::new(move || {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!(
|
||||
"<!--hk={}|leptos-unit-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
format!("<!--hk={}-->", HydrationCtx::to_string(&u.id, true))
|
||||
.into()
|
||||
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
),
|
||||
CoreComponent::DynChild(node) => {
|
||||
let child = node.child.take();
|
||||
(
|
||||
node.id,
|
||||
"dyn-child",
|
||||
true,
|
||||
Box::new(move || {
|
||||
if let Some(child) = *child {
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
if !cfg!(debug_assertions) {
|
||||
format!("<!>{}", t.content).into()
|
||||
} else {
|
||||
t.content
|
||||
}
|
||||
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
View::Text(node) => node.content,
|
||||
View::Component(node) => {
|
||||
let content = || {
|
||||
node.children
|
||||
.into_iter()
|
||||
.map(|node| node.render_to_string_helper())
|
||||
.join("")
|
||||
};
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&node.id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true),
|
||||
name = to_kebab_case(&node.name)
|
||||
).into()
|
||||
} else {
|
||||
child.render_to_string_helper()
|
||||
format!(
|
||||
r#"{}<!--hk={}-->"#,
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true)
|
||||
).into()
|
||||
}
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
}
|
||||
View::Suspense(id, node) => format!(
|
||||
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
|
||||
View::CoreComponent(node).render_to_string_helper()
|
||||
)
|
||||
}
|
||||
CoreComponent::Each(node) => {
|
||||
let children = node.children.take();
|
||||
(
|
||||
node.id,
|
||||
"each",
|
||||
true,
|
||||
Box::new(move || {
|
||||
children
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|node| {
|
||||
let id = node.id;
|
||||
.into(),
|
||||
View::CoreComponent(node) => {
|
||||
let (id, name, wrap, content) = match node {
|
||||
CoreComponent::Unit(u) => (
|
||||
u.id.clone(),
|
||||
"",
|
||||
false,
|
||||
Box::new(move || {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!(
|
||||
"<!--hk={}|leptos-unit-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
let content = || node.child.render_to_string_helper();
|
||||
#[cfg(not(debug_assertions))]
|
||||
format!(
|
||||
"<!--hk={}-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
)
|
||||
.into()
|
||||
})
|
||||
as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
),
|
||||
CoreComponent::DynChild(node) => {
|
||||
let child = node.child.take();
|
||||
(
|
||||
node.id,
|
||||
"dyn-child",
|
||||
true,
|
||||
Box::new(move || {
|
||||
if let Some(child) = *child {
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
if !cfg!(debug_assertions) {
|
||||
format!("<!>{}", t.content).into()
|
||||
} else {
|
||||
t.content
|
||||
}
|
||||
} else {
|
||||
child.render_to_string_helper()
|
||||
}
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
})
|
||||
as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
)
|
||||
}
|
||||
CoreComponent::Each(node) => {
|
||||
let children = node.children.take();
|
||||
(
|
||||
node.id,
|
||||
"each",
|
||||
true,
|
||||
Box::new(move || {
|
||||
children
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|node| {
|
||||
let id = node.id;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!(
|
||||
let content = || {
|
||||
node.child.render_to_string_helper()
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!(
|
||||
"<!--hk={}|leptos-each-item-start-->{}<!\
|
||||
--hk={}|leptos-each-item-end-->",
|
||||
HydrationCtx::to_string(&id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
format!(
|
||||
"{}<!--hk={}-->",
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
)
|
||||
})
|
||||
.join("")
|
||||
.into()
|
||||
})
|
||||
as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
format!(
|
||||
"{}<!--hk={}-->",
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
)
|
||||
})
|
||||
.join("")
|
||||
.into()
|
||||
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
)
|
||||
}
|
||||
};
|
||||
if wrap {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
format!(
|
||||
r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true),
|
||||
).into()
|
||||
} else {
|
||||
let _ = name;
|
||||
|
||||
if wrap {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
format!(
|
||||
r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true),
|
||||
).into()
|
||||
} else {
|
||||
let _ = name;
|
||||
|
||||
format!(
|
||||
r#"{}<!--hk={}-->"#,
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
).into()
|
||||
format!(
|
||||
r#"{}<!--hk={}-->"#,
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
).into()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
View::Element(el) => {
|
||||
if let Some(prerendered) = el.prerendered {
|
||||
prerendered
|
||||
} else {
|
||||
let tag_name = el.name;
|
||||
View::Element(el) => {
|
||||
if let Some(prerendered) = el.prerendered {
|
||||
prerendered
|
||||
} else {
|
||||
let tag_name = el.name;
|
||||
|
||||
let mut inner_html = None;
|
||||
let mut inner_html = None;
|
||||
|
||||
let attrs = el
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter_map(|(name, value)| -> Option<Cow<'static, str>> {
|
||||
if value.is_empty() {
|
||||
Some(format!(" {name}").into())
|
||||
} else if name == "inner_html" {
|
||||
inner_html = Some(value);
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
format!(
|
||||
let attrs = el
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(name, value)| -> Option<Cow<'static, str>> {
|
||||
if value.is_empty() {
|
||||
Some(format!(" {name}").into())
|
||||
} else if name == "inner_html" {
|
||||
inner_html = Some(value);
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
format!(
|
||||
" {name}=\"{}\"",
|
||||
html_escape::encode_double_quoted_attribute(&value)
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
})
|
||||
.join("");
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.join("");
|
||||
|
||||
if el.is_void {
|
||||
format!("<{tag_name}{attrs}/>").into()
|
||||
} else if let Some(inner_html) = inner_html {
|
||||
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>").into()
|
||||
} else {
|
||||
let children = el
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|node| node.render_to_string_helper())
|
||||
.join("");
|
||||
if el.is_void {
|
||||
format!("<{tag_name}{attrs}/>").into()
|
||||
} else if let Some(inner_html) = inner_html {
|
||||
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>")
|
||||
.into()
|
||||
} else {
|
||||
let children = el
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|node| node.render_to_string_helper())
|
||||
.join("");
|
||||
|
||||
format!("<{tag_name}{attrs}>{children}</{tag_name}>").into()
|
||||
}
|
||||
format!("<{tag_name}{attrs}>{children}</{tag_name}>")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Transparent(_) => Default::default(),
|
||||
}
|
||||
}
|
||||
View::Transparent(_) => Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn to_kebab_case(name: &str) -> String {
|
||||
if name.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut new_name = String::with_capacity(name.len() + 8);
|
||||
|
||||
let mut chars = name.chars();
|
||||
|
||||
new_name.push(
|
||||
chars
|
||||
.next()
|
||||
.map(|mut c| {
|
||||
if c.is_ascii() {
|
||||
c.make_ascii_lowercase();
|
||||
}
|
||||
|
||||
c
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
for mut char in chars {
|
||||
if char.is_ascii_uppercase() {
|
||||
char.make_ascii_lowercase();
|
||||
|
||||
new_name.push('-');
|
||||
pub(crate) fn to_kebab_case(name: &str) -> String {
|
||||
if name.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
new_name.push(char);
|
||||
}
|
||||
let mut new_name = String::with_capacity(name.len() + 8);
|
||||
|
||||
new_name
|
||||
let mut chars = name.chars();
|
||||
|
||||
new_name.push(
|
||||
chars
|
||||
.next()
|
||||
.map(|mut c| {
|
||||
if c.is_ascii() {
|
||||
c.make_ascii_lowercase();
|
||||
}
|
||||
|
||||
c
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
for mut char in chars {
|
||||
if char.is_ascii_uppercase() {
|
||||
char.make_ascii_lowercase();
|
||||
|
||||
new_name.push('-');
|
||||
}
|
||||
|
||||
new_name.push(char);
|
||||
}
|
||||
|
||||
new_name
|
||||
}
|
||||
|
||||
pub(crate) fn render_serializers(
|
||||
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
|
||||
) -> impl Stream<Item = String> {
|
||||
serializers.map(|(id, json)| {
|
||||
let id = serde_json::to_string(&id).unwrap();
|
||||
format!(
|
||||
r#"<script>
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
|
||||
}} else {{
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
|
||||
}}
|
||||
</script>"#,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn escape_attr<T>(value: &T) -> Cow<'_, str>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: AsRef<str>,
|
||||
{
|
||||
html_escape::encode_double_quoted_attribute(value)
|
||||
html_escape::encode_double_quoted_attribute(value)
|
||||
}
|
||||
|
||||
354
leptos_dom/src/ssr_in_order.rs
Normal file
354
leptos_dom/src/ssr_in_order.rs
Normal file
@@ -0,0 +1,354 @@
|
||||
#![cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
|
||||
//! Server-side HTML rendering utilities for in-order streaming and async rendering.
|
||||
|
||||
use crate::{ssr::render_serializers, CoreComponent, HydrationCtx, View};
|
||||
use async_recursion::async_recursion;
|
||||
use cfg_if::cfg_if;
|
||||
use futures::{channel::mpsc::Sender, Stream, StreamExt};
|
||||
use itertools::Itertools;
|
||||
use leptos_reactive::{
|
||||
create_runtime, run_scope_undisposed, suspense::StreamChunk, RuntimeId,
|
||||
Scope, ScopeId,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Renders a view to HTML, waiting to return until all `async` [Resource](leptos_reactive::Resource)s
|
||||
/// loaded in `<Suspense/>` elements have finished loading.
|
||||
pub async fn render_to_string_async(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
) -> String {
|
||||
let mut buf = String::new();
|
||||
let mut stream = Box::pin(render_to_stream_in_order(view));
|
||||
while let Some(chunk) = stream.next().await {
|
||||
buf.push_str(&chunk);
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
|
||||
/// in order:
|
||||
/// 1. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
|
||||
/// 2. any serialized [Resource](leptos_reactive::Resource)s
|
||||
pub fn render_to_stream_in_order(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
render_to_stream_in_order_with_prefix(view, |_| "".into())
|
||||
}
|
||||
|
||||
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
|
||||
/// in order:
|
||||
/// 1. `prefix`
|
||||
/// 2. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
|
||||
/// 3. any serialized [Resource](leptos_reactive::Resource)s
|
||||
///
|
||||
/// `additional_context` is injected before the `view` is rendered. The `prefix` is generated
|
||||
/// after the `view` is rendered, but before `<Suspense/>` nodes have resolved.
|
||||
pub fn render_to_stream_in_order_with_prefix(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
let (stream, runtime, _) =
|
||||
render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
view,
|
||||
prefix,
|
||||
|_| {},
|
||||
);
|
||||
runtime.dispose();
|
||||
stream
|
||||
}
|
||||
|
||||
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
|
||||
/// in order:
|
||||
/// 1. `prefix`
|
||||
/// 2. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
|
||||
/// 3. any serialized [Resource](leptos_reactive::Resource)s
|
||||
///
|
||||
/// `additional_context` is injected before the `view` is rendered. The `prefix` is generated
|
||||
/// after the `view` is rendered, but before `<Suspense/>` nodes have resolved.
|
||||
pub fn render_to_stream_in_order_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();
|
||||
|
||||
// create the runtime
|
||||
let runtime = create_runtime();
|
||||
|
||||
let ((chunks, prefix, pending_resources, serializers), scope_id, _) =
|
||||
run_scope_undisposed(runtime, |cx| {
|
||||
// add additional context
|
||||
additional_context(cx);
|
||||
|
||||
// render view and return chunks
|
||||
let view = view(cx);
|
||||
|
||||
let prefix = prefix(cx);
|
||||
(
|
||||
view.into_stream_chunks(cx),
|
||||
prefix,
|
||||
serde_json::to_string(&cx.pending_resources()).unwrap(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
});
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::channel(1);
|
||||
leptos_reactive::spawn_local(async move {
|
||||
handle_chunks(tx, chunks).await;
|
||||
});
|
||||
|
||||
let stream = futures::stream::once(async move {
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
<script>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
})
|
||||
.chain(rx)
|
||||
.chain(render_serializers(serializers));
|
||||
|
||||
(stream, runtime, scope_id)
|
||||
}
|
||||
|
||||
#[async_recursion(?Send)]
|
||||
async fn handle_chunks(mut tx: Sender<String>, chunks: Vec<StreamChunk>) {
|
||||
let mut buffer = String::new();
|
||||
for chunk in chunks {
|
||||
match chunk {
|
||||
StreamChunk::Sync(sync) => buffer.push_str(&sync),
|
||||
StreamChunk::Async(suspended) => {
|
||||
// add static HTML before the Suspense and stream it down
|
||||
_ = tx.try_send(std::mem::take(&mut buffer));
|
||||
|
||||
// send the inner stream
|
||||
let suspended = suspended.await;
|
||||
handle_chunks(tx.clone(), suspended).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
// send final sync chunk
|
||||
_ = tx.try_send(std::mem::take(&mut buffer));
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// Renders the view into a set of HTML chunks that can be streamed.
|
||||
pub fn into_stream_chunks(self, cx: Scope) -> Vec<StreamChunk> {
|
||||
let mut chunks = Vec::new();
|
||||
self.into_stream_chunks_helper(cx, &mut chunks);
|
||||
chunks
|
||||
}
|
||||
|
||||
fn into_stream_chunks_helper(
|
||||
self,
|
||||
cx: Scope,
|
||||
chunks: &mut Vec<StreamChunk>,
|
||||
) {
|
||||
match self {
|
||||
View::Suspense(id, _) => {
|
||||
let id = id.to_string();
|
||||
if let Some((_, fragment)) = cx.take_pending_fragment(&id) {
|
||||
chunks.push(StreamChunk::Async(fragment));
|
||||
}
|
||||
}
|
||||
View::Text(node) => chunks.push(StreamChunk::Sync(node.content)),
|
||||
View::Component(node) => {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let name = crate::ssr::to_kebab_case(&node.name);
|
||||
chunks.push(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-start-->"#, HydrationCtx::to_string(&node.id, false)).into()));
|
||||
for child in node.children {
|
||||
child.into_stream_chunks_helper(cx, chunks);
|
||||
}
|
||||
chunks.push(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-end-->"#, HydrationCtx::to_string(&node.id, true)).into()));
|
||||
} else {
|
||||
for child in node.children {
|
||||
child.into_stream_chunks_helper(cx, chunks);
|
||||
}
|
||||
chunks.push(StreamChunk::Sync(format!(r#"<!--hk={}-->"#, HydrationCtx::to_string(&node.id, true)).into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Element(el) => {
|
||||
if let Some(prerendered) = el.prerendered {
|
||||
chunks.push(StreamChunk::Sync(prerendered))
|
||||
} else {
|
||||
let tag_name = el.name;
|
||||
|
||||
let mut inner_html = None;
|
||||
|
||||
let attrs = el
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(name, value)| -> Option<Cow<'static, str>> {
|
||||
if value.is_empty() {
|
||||
Some(format!(" {name}").into())
|
||||
} else if name == "inner_html" {
|
||||
inner_html = Some(value);
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
format!(
|
||||
" {name}=\"{}\"",
|
||||
html_escape::encode_double_quoted_attribute(&value)
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.join("");
|
||||
|
||||
if el.is_void {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!("<{tag_name}{attrs}/>").into(),
|
||||
));
|
||||
} else if let Some(inner_html) = inner_html {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!(
|
||||
"<{tag_name}{attrs}>{inner_html}</{tag_name}>"
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
} else {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!("<{tag_name}{attrs}>").into(),
|
||||
));
|
||||
for child in el.children {
|
||||
child.into_stream_chunks_helper(cx, chunks);
|
||||
}
|
||||
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!("</{tag_name}>").into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Transparent(_) => {}
|
||||
View::CoreComponent(node) => {
|
||||
let (id, name, wrap, content) = match node {
|
||||
CoreComponent::Unit(u) => (
|
||||
u.id.clone(),
|
||||
"",
|
||||
false,
|
||||
Box::new(move |chunks: &mut Vec<StreamChunk>| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}|leptos-unit-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
})
|
||||
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
|
||||
),
|
||||
CoreComponent::DynChild(node) => {
|
||||
let child = node.child.take();
|
||||
(
|
||||
node.id,
|
||||
"dyn-child",
|
||||
true,
|
||||
Box::new(move |chunks: &mut Vec<StreamChunk>| {
|
||||
if let Some(child) = *child {
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
chunks.push(
|
||||
if !cfg!(debug_assertions) {
|
||||
StreamChunk::Sync(
|
||||
format!("<!>{}", t.content)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
StreamChunk::Sync(t.content)
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
|
||||
)
|
||||
}
|
||||
CoreComponent::Each(node) => {
|
||||
let children = node.children.take();
|
||||
(
|
||||
node.id,
|
||||
"each",
|
||||
true,
|
||||
Box::new(move |chunks: &mut Vec<StreamChunk>| {
|
||||
for node in children.into_iter().flatten() {
|
||||
let id = node.id;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}|leptos-each-item-start-->",
|
||||
HydrationCtx::to_string(&id, false)
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
node.child.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
);
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}|leptos-each-item-end-->",
|
||||
HydrationCtx::to_string(&id, true)
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
})
|
||||
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if wrap {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
chunks.push(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-start-->", HydrationCtx::to_string(&id, false)).into()));
|
||||
content(chunks);
|
||||
chunks.push(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-end-->", HydrationCtx::to_string(&id, true)).into()));
|
||||
} else {
|
||||
let _ = name;
|
||||
content(chunks);
|
||||
chunks.push(StreamChunk::Sync(format!("<!--hk={}-->", HydrationCtx::to_string(&id, true)).into()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content(chunks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
//! SVG elements.
|
||||
//! Exports types for working with SVG elements.
|
||||
|
||||
use super::{ElementDescriptor, HtmlElement};
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
use super::{HydrationKey, HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
|
||||
use super::{html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG, HydrationKey};
|
||||
use super::{ElementDescriptor, HtmlElement};
|
||||
use crate::HydrationCtx;
|
||||
use leptos_reactive::Scope;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -7,39 +7,39 @@ use std::{any::Any, fmt, rc::Rc};
|
||||
pub struct Transparent(Rc<dyn Any>);
|
||||
|
||||
impl Transparent {
|
||||
/// Creates a new wrapper for this data.
|
||||
pub fn new<T>(value: T) -> Self
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Self(Rc::new(value))
|
||||
}
|
||||
/// Creates a new wrapper for this data.
|
||||
pub fn new<T>(value: T) -> Self
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Self(Rc::new(value))
|
||||
}
|
||||
|
||||
/// Returns some reference to the inner value if it is of type `T`, or `None` if it isn't.
|
||||
pub fn downcast_ref<T>(&self) -> Option<&T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.0.downcast_ref()
|
||||
}
|
||||
/// Returns some reference to the inner value if it is of type `T`, or `None` if it isn't.
|
||||
pub fn downcast_ref<T>(&self) -> Option<&T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.0.downcast_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Transparent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("Transparent").finish()
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("Transparent").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Transparent {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
std::ptr::eq(&self.0, &other.0)
|
||||
}
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
std::ptr::eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Transparent {}
|
||||
|
||||
impl IntoView for Transparent {
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
View::Transparent(self)
|
||||
}
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
View::Transparent(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ leptos_dom = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4"
|
||||
|
||||
@@ -8,9 +8,10 @@ use proc_macro_error::ResultExt;
|
||||
use quote::{format_ident, ToTokens, TokenStreamExt};
|
||||
use std::collections::HashSet;
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument,
|
||||
ItemFn, LitStr, Meta, MetaList, MetaNameValue, NestedMeta, Pat, PatIdent, Path, PathArguments,
|
||||
ReturnType, Type, TypePath, Visibility,
|
||||
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute,
|
||||
FnArg, GenericArgument, ItemFn, LitStr, Meta, MetaList, MetaNameValue,
|
||||
NestedMeta, Pat, PatIdent, Path, PathArguments, ReturnType, Type, TypePath,
|
||||
Visibility,
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
@@ -62,7 +63,8 @@ impl Parse for Model {
|
||||
item.sig.inputs.iter_mut().for_each(|arg| {
|
||||
if let FnArg::Typed(ty) = arg {
|
||||
drain_filter(&mut ty.attrs, |attr| {
|
||||
attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop)
|
||||
attr.path == parse_quote!(doc)
|
||||
|| attr.path == parse_quote!(prop)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -91,7 +93,10 @@ impl Parse for Model {
|
||||
|
||||
// implemented manually because Vec::drain_filter is nightly only
|
||||
// follows std recommended parallel
|
||||
fn drain_filter<T>(vec: &mut Vec<T>, mut some_predicate: impl FnMut(&mut T) -> bool) {
|
||||
fn drain_filter<T>(
|
||||
vec: &mut Vec<T>,
|
||||
mut some_predicate: impl FnMut(&mut T) -> bool,
|
||||
) {
|
||||
let mut i = 0;
|
||||
while i < vec.len() {
|
||||
if some_predicate(&mut vec[i]) {
|
||||
@@ -139,32 +144,33 @@ impl ToTokens for Model {
|
||||
|
||||
let prop_names = prop_names(props);
|
||||
|
||||
let builder_name_doc =
|
||||
LitStr::new(&format!("Props for the [`{name}`] component."), name.span());
|
||||
let builder_name_doc = LitStr::new(
|
||||
&format!("Props for the [`{name}`] component."),
|
||||
name.span(),
|
||||
);
|
||||
|
||||
let component_fn_prop_docs = generate_component_fn_prop_docs(props);
|
||||
|
||||
let (tracing_instrument_attr, tracing_span_expr, tracing_guard_expr) = if cfg!(
|
||||
feature = "tracing"
|
||||
) {
|
||||
(
|
||||
quote! {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
|
||||
)]
|
||||
},
|
||||
quote! {
|
||||
let span = ::leptos::leptos_dom::tracing::Span::current();
|
||||
},
|
||||
quote! {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.entered();
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(quote! {}, quote! {}, quote! {})
|
||||
};
|
||||
let (tracing_instrument_attr, tracing_span_expr, tracing_guard_expr) =
|
||||
if cfg!(feature = "tracing") {
|
||||
(
|
||||
quote! {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
|
||||
)]
|
||||
},
|
||||
quote! {
|
||||
let span = ::leptos::leptos_dom::tracing::Span::current();
|
||||
},
|
||||
quote! {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.entered();
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(quote! {}, quote! {}, quote! {})
|
||||
};
|
||||
|
||||
let component = if *is_transparent {
|
||||
quote! {
|
||||
@@ -172,7 +178,7 @@ impl ToTokens for Model {
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
::leptos::Component::new(
|
||||
::leptos::leptos_dom::Component::new(
|
||||
stringify!(#name),
|
||||
move |cx| {
|
||||
#tracing_guard_expr
|
||||
@@ -249,11 +255,16 @@ impl Prop {
|
||||
.attrs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, attr)| PropOpt::from_attribute(attr).map(|opt| (i, opt)))
|
||||
.filter_map(|(i, attr)| {
|
||||
PropOpt::from_attribute(attr).map(|opt| (i, opt))
|
||||
})
|
||||
.fold(HashSet::new(), |mut acc, cur| {
|
||||
// Make sure opts aren't repeated
|
||||
if acc.intersection(&cur.1).next().is_some() {
|
||||
abort!(typed.attrs[cur.0], "`#[prop]` options are repeated");
|
||||
abort!(
|
||||
typed.attrs[cur.0],
|
||||
"`#[prop]` options are repeated"
|
||||
);
|
||||
}
|
||||
|
||||
acc.extend(cur.1);
|
||||
@@ -262,10 +273,13 @@ impl Prop {
|
||||
});
|
||||
|
||||
// Make sure conflicting options are not present
|
||||
if prop_opts.contains(&PropOpt::Optional) && prop_opts.contains(&PropOpt::OptionalNoStrip) {
|
||||
if prop_opts.contains(&PropOpt::Optional)
|
||||
&& prop_opts.contains(&PropOpt::OptionalNoStrip)
|
||||
{
|
||||
abort!(
|
||||
typed,
|
||||
"`optional` and `optional_no_strip` options are mutually exclusive"
|
||||
"`optional` and `optional_no_strip` options are mutually \
|
||||
exclusive"
|
||||
);
|
||||
} else if prop_opts.contains(&PropOpt::Optional)
|
||||
&& prop_opts.contains(&PropOpt::StripOption)
|
||||
@@ -279,7 +293,8 @@ impl Prop {
|
||||
{
|
||||
abort!(
|
||||
typed,
|
||||
"`optional_no_strip` and `strip_option` options are mutually exclusive"
|
||||
"`optional_no_strip` and `strip_option` options are mutually \
|
||||
exclusive"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -288,8 +303,8 @@ impl Prop {
|
||||
} else {
|
||||
abort!(
|
||||
typed.pat,
|
||||
"only `prop: bool` style types are allowed within the `#[component]` \
|
||||
macro"
|
||||
"only `prop: bool` style types are allowed within the \
|
||||
`#[component]` macro"
|
||||
);
|
||||
};
|
||||
|
||||
@@ -333,7 +348,8 @@ impl Docs {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, attr)| {
|
||||
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) = attr.parse_meta().unwrap()
|
||||
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) =
|
||||
attr.parse_meta().unwrap()
|
||||
{
|
||||
let doc_str = quote!(#doc);
|
||||
|
||||
@@ -368,7 +384,8 @@ impl Docs {
|
||||
.0
|
||||
.iter()
|
||||
.map(|attr| {
|
||||
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) = attr.parse_meta().unwrap()
|
||||
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) =
|
||||
attr.parse_meta().unwrap()
|
||||
{
|
||||
let mut doc_str = quote!(#doc).to_string();
|
||||
|
||||
@@ -403,15 +420,17 @@ enum PropOpt {
|
||||
|
||||
impl PropOpt {
|
||||
fn from_attribute(attr: &Attribute) -> Option<HashSet<Self>> {
|
||||
const ABORT_OPT_MESSAGE: &str = "only `optional`, `optional_no_strip`, \
|
||||
`strip_option`, `default` and `into` are \
|
||||
allowed as arguments to `#[prop()]`";
|
||||
const ABORT_OPT_MESSAGE: &str =
|
||||
"only `optional`, `optional_no_strip`, `strip_option`, `default` \
|
||||
and `into` are allowed as arguments to `#[prop()]`";
|
||||
|
||||
if attr.path != parse_quote!(prop) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Meta::List(MetaList { nested, .. }) = attr.parse_meta().unwrap_or_abort() {
|
||||
if let Meta::List(MetaList { nested, .. }) =
|
||||
attr.parse_meta().unwrap_or_abort()
|
||||
{
|
||||
Some(
|
||||
nested
|
||||
.iter()
|
||||
@@ -473,7 +492,8 @@ struct TypedBuilderOpts {
|
||||
impl TypedBuilderOpts {
|
||||
fn from_opts(opts: &HashSet<PropOpt>, is_ty_option: bool) -> Self {
|
||||
Self {
|
||||
default: opts.contains(&PropOpt::Optional) || opts.contains(&PropOpt::OptionalNoStrip),
|
||||
default: opts.contains(&PropOpt::Optional)
|
||||
|| opts.contains(&PropOpt::OptionalNoStrip),
|
||||
default_with_value: opts.iter().find_map(|p| match p {
|
||||
PropOpt::OptionalWithDefault(v) => Some(v.to_owned()),
|
||||
_ => None,
|
||||
@@ -531,7 +551,8 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
ty,
|
||||
} = prop;
|
||||
|
||||
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
|
||||
let builder_attrs =
|
||||
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
|
||||
|
||||
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);
|
||||
|
||||
@@ -566,7 +587,8 @@ fn generate_component_fn_prop_docs(props: &[Prop]) -> TokenStream {
|
||||
let optional_prop_docs = props
|
||||
.iter()
|
||||
.filter(|Prop { prop_opts, .. }| {
|
||||
prop_opts.contains(&PropOpt::Optional) || prop_opts.contains(&PropOpt::OptionalNoStrip)
|
||||
prop_opts.contains(&PropOpt::Optional)
|
||||
|| prop_opts.contains(&PropOpt::OptionalNoStrip)
|
||||
})
|
||||
.map(|p| prop_to_doc(p, PropDocStyle::List))
|
||||
.collect::<TokenStream>();
|
||||
@@ -613,8 +635,8 @@ fn is_option(ty: &Type) -> bool {
|
||||
|
||||
fn unwrap_option(ty: &Type) -> Option<Type> {
|
||||
const STD_OPTION_MSG: &str =
|
||||
"make sure you're not shadowing the `std::option::Option` type that is \
|
||||
automatically imported from the standard prelude";
|
||||
"make sure you're not shadowing the `std::option::Option` type that \
|
||||
is automatically imported from the standard prelude";
|
||||
|
||||
if let Type::Path(TypePath {
|
||||
path: Path { segments, .. },
|
||||
@@ -623,9 +645,9 @@ fn unwrap_option(ty: &Type) -> Option<Type> {
|
||||
{
|
||||
if let [first] = &segments.iter().collect::<Vec<_>>()[..] {
|
||||
if first.ident == "Option" {
|
||||
if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
|
||||
args, ..
|
||||
}) = &first.arguments
|
||||
if let PathArguments::AngleBracketed(
|
||||
AngleBracketedGenericArguments { args, .. },
|
||||
) = &first.arguments
|
||||
{
|
||||
if let [first] = &args.iter().collect::<Vec<_>>()[..] {
|
||||
if let GenericArgument::Type(ty) = first {
|
||||
@@ -706,7 +728,11 @@ fn prop_to_doc(
|
||||
&if !prop_opts.contains(&PropOpt::Into) {
|
||||
format!("- **{}**: [`{}`]", quote!(#name), pretty_ty)
|
||||
} else {
|
||||
format!("- **{}**: `impl`[`Into<{}>`]", quote!(#name), pretty_ty)
|
||||
format!(
|
||||
"- **{}**: `impl`[`Into<{}>`]",
|
||||
quote!(#name),
|
||||
pretty_ty
|
||||
)
|
||||
},
|
||||
name.ident.span(),
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenTree;
|
||||
use quote::ToTokens;
|
||||
use server::server_macro_impl;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
use syn::parse_macro_input;
|
||||
use syn_rsx::{parse, NodeElement};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -19,7 +19,10 @@ pub(crate) enum Mode {
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
if cfg!(feature = "hydrate") || cfg!(feature = "csr") || cfg!(feature = "web") {
|
||||
if cfg!(feature = "hydrate")
|
||||
|| cfg!(feature = "csr")
|
||||
|| cfg!(feature = "web")
|
||||
{
|
||||
Mode::Client
|
||||
} else {
|
||||
Mode::Ssr
|
||||
@@ -29,10 +32,11 @@ impl Default for Mode {
|
||||
|
||||
mod params;
|
||||
mod view;
|
||||
use template::render_template;
|
||||
use view::render_view;
|
||||
mod component;
|
||||
mod props;
|
||||
mod server;
|
||||
mod template;
|
||||
|
||||
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
|
||||
/// same rules as HTML, with the following differences:
|
||||
@@ -118,7 +122,7 @@ mod server;
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <button on:click=|ev: web_sys::MouseEvent| {
|
||||
/// <button on:click=|ev| {
|
||||
/// log::debug!("click event: {ev:#?}");
|
||||
/// }>
|
||||
/// "Click me"
|
||||
@@ -213,8 +217,8 @@ 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.
|
||||
/// `class = {/* ... */},` argument after `cx, `. This is useful for injecting a class
|
||||
/// provided by a scoped styling library.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
@@ -257,9 +261,9 @@ mod server;
|
||||
///
|
||||
/// // create event handlers for our buttons
|
||||
/// // note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
|
||||
/// let clear = move |_ev: web_sys::MouseEvent| set_value(0);
|
||||
/// let decrement = move |_ev: web_sys::MouseEvent| set_value.update(|value| *value -= 1);
|
||||
/// let increment = move |_ev: web_sys::MouseEvent| set_value.update(|value| *value += 1);
|
||||
/// let clear = move |_ev| set_value(0);
|
||||
/// let decrement = move |_ev| set_value.update(|value| *value -= 1);
|
||||
/// let increment = move |_ev| set_value.update(|value| *value += 1);
|
||||
///
|
||||
/// // this JSX is compiled to an HTML template string for performance
|
||||
/// view! {
|
||||
@@ -282,22 +286,33 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
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() == ',' => {
|
||||
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct)))
|
||||
if punct.as_char() == ',' =>
|
||||
{
|
||||
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() =>
|
||||
let global_class = match (&first, &second) {
|
||||
(Some(TokenTree::Ident(first)), Some(TokenTree::Punct(eq)))
|
||||
if *first == "class" && eq.as_char() == '=' =>
|
||||
{
|
||||
Some(val.clone())
|
||||
match &fourth {
|
||||
Some(TokenTree::Punct(comma))
|
||||
if comma.as_char() == ',' =>
|
||||
{
|
||||
third.clone()
|
||||
}
|
||||
_ => {
|
||||
let error_msg = concat!(
|
||||
"To create a scope class with the view! macro \
|
||||
you must put a comma `,` after the value.\n",
|
||||
"e.g., view!{cx, class=\"my-class\", \
|
||||
<div>...</div>}"
|
||||
);
|
||||
panic!("{error_msg}")
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
@@ -315,7 +330,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
Ok(nodes) => render_view(
|
||||
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
|
||||
&nodes,
|
||||
Mode::default(),
|
||||
Mode::Client, //Mode::default(),
|
||||
global_class.as_ref(),
|
||||
),
|
||||
Err(error) => error.to_compile_error(),
|
||||
@@ -323,11 +338,51 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
.into()
|
||||
}
|
||||
_ => {
|
||||
panic!("view! macro needs a context and RSX: e.g., view! {{ cx, <div>...</div> }}")
|
||||
panic!(
|
||||
"view! macro needs a context and RSX: e.g., view! {{ cx, \
|
||||
<div>...</div> }}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An optimized, cached template for client-side rendering. Follows the same
|
||||
/// syntax as the [view!] macro. In hydration or server-side rendering mode,
|
||||
/// behaves exactly as the `view` macro. In client-side rendering mode, uses a `<template>`
|
||||
/// node to efficiently render the element. Should only be used with a single root element.
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn template(tokens: TokenStream) -> TokenStream {
|
||||
if cfg!(feature = "csr") {
|
||||
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::<proc_macro2::TokenStream>().into())
|
||||
{
|
||||
Ok(nodes) => render_template(
|
||||
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
|
||||
&nodes,
|
||||
),
|
||||
Err(error) => error.to_compile_error(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
_ => {
|
||||
panic!(
|
||||
"view! macro needs a context and RSX: e.g., view! {{ cx, \
|
||||
<div>...</div> }}"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
view(tokens)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -348,33 +403,34 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// #[component]
|
||||
/// fn HelloComponent(
|
||||
/// cx: Scope,
|
||||
/// /// The user's name.
|
||||
/// name: String,
|
||||
/// /// The user's age.
|
||||
/// age: u8
|
||||
/// cx: Scope,
|
||||
/// /// The user's name.
|
||||
/// name: String,
|
||||
/// /// The user's age.
|
||||
/// age: u8,
|
||||
/// ) -> impl IntoView {
|
||||
/// // create the signals (reactive values) that will update the UI
|
||||
/// let (age, set_age) = create_signal(cx, age);
|
||||
/// // increase `age` by 1 every second
|
||||
/// set_interval(move || {
|
||||
/// set_age.update(|age| *age += 1)
|
||||
/// }, Duration::from_secs(1));
|
||||
///
|
||||
/// // return the user interface, which will be automatically updated
|
||||
/// // when signal values change
|
||||
/// view! { cx,
|
||||
/// <p>"Your name is " {name} " and you are " {age} " years old."</p>
|
||||
/// }
|
||||
/// // create the signals (reactive values) that will update the UI
|
||||
/// let (age, set_age) = create_signal(cx, age);
|
||||
/// // increase `age` by 1 every second
|
||||
/// set_interval(
|
||||
/// move || set_age.update(|age| *age += 1),
|
||||
/// Duration::from_secs(1),
|
||||
/// );
|
||||
///
|
||||
/// // return the user interface, which will be automatically updated
|
||||
/// // when signal values change
|
||||
/// view! { cx,
|
||||
/// <p>"Your name is " {name} " and you are " {age} " years old."</p>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// fn App(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <HelloComponent name="Greg".to_string() age=32/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <HelloComponent name="Greg".to_string() age=32/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
@@ -399,11 +455,15 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// // PascalCase: Generated component will be called MyComponent
|
||||
/// #[component]
|
||||
/// fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
|
||||
/// 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!() }
|
||||
/// fn my_snake_case_component(cx: Scope) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 3. The macro generates a type `ComponentProps` for every `Component` (so, `HomePage` generates `HomePageProps`,
|
||||
@@ -416,22 +476,28 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// use component::{MyComponent, MyComponentProps};
|
||||
///
|
||||
/// mod component {
|
||||
/// use leptos::*;
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
|
||||
/// #[component]
|
||||
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
///
|
||||
/// use snake_case_component::{MySnakeCaseComponent, MySnakeCaseComponentProps};
|
||||
/// use snake_case_component::{
|
||||
/// MySnakeCaseComponent, MySnakeCaseComponentProps,
|
||||
/// };
|
||||
///
|
||||
/// mod snake_case_component {
|
||||
/// use leptos::*;
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
|
||||
/// #[component]
|
||||
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
@@ -440,6 +506,8 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// ```compile_error
|
||||
/// // ❌ This won't work.
|
||||
/// # use leptos::*;
|
||||
/// use leptos::html::Div;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyComponent<T: Fn() -> HtmlElement<Div>>(cx: Scope, render_prop: T) -> impl IntoView {
|
||||
/// todo!()
|
||||
@@ -449,10 +517,14 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// ```
|
||||
/// // ✅ Do this instead
|
||||
/// # use leptos::*;
|
||||
/// use leptos::html::Div;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyComponent<T>(cx: Scope, render_prop: T) -> impl IntoView
|
||||
/// where T: Fn() -> HtmlElement<Div> {
|
||||
/// todo!()
|
||||
/// where
|
||||
/// T: Fn() -> HtmlElement<Div>,
|
||||
/// {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
@@ -465,26 +537,26 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// # use leptos::*;
|
||||
/// #[component]
|
||||
/// fn ComponentWithChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <ul>
|
||||
/// {children(cx)
|
||||
/// .nodes
|
||||
/// .into_iter()
|
||||
/// .map(|child| view! { cx, <li>{child}</li> })
|
||||
/// .collect::<Vec<_>>()}
|
||||
/// </ul>
|
||||
/// }
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <ul>
|
||||
/// {children(cx)
|
||||
/// .nodes
|
||||
/// .into_iter()
|
||||
/// .map(|child| view! { cx, <li>{child}</li> })
|
||||
/// .collect::<Vec<_>>()}
|
||||
/// </ul>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// fn WrapSomeChildren(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx,
|
||||
/// <ComponentWithChildren>
|
||||
/// "Ooh, look at us!"
|
||||
/// <span>"We're being projected!"</span>
|
||||
/// </ComponentWithChildren>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <ComponentWithChildren>
|
||||
/// "Ooh, look at us!"
|
||||
/// <span>"We're being projected!"</span>
|
||||
/// </ComponentWithChildren>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
@@ -506,30 +578,27 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent(
|
||||
/// cx: Scope,
|
||||
/// #[prop(into)]
|
||||
/// name: String,
|
||||
/// #[prop(optional)]
|
||||
/// optional_value: Option<i32>,
|
||||
/// #[prop(optional_no_strip)]
|
||||
/// optional_no_strip: Option<i32>
|
||||
/// cx: Scope,
|
||||
/// #[prop(into)] name: String,
|
||||
/// #[prop(optional)] optional_value: Option<i32>,
|
||||
/// #[prop(optional_no_strip)] optional_no_strip: Option<i32>,
|
||||
/// ) -> impl IntoView {
|
||||
/// // whatever UI you need
|
||||
/// // whatever UI you need
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// #[component]
|
||||
/// pub fn App(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx,
|
||||
/// <MyComponent
|
||||
/// name="Greg" // automatically converted to String with `.into()`
|
||||
/// optional_value=42 // received as `Some(42)`
|
||||
/// optional_no_strip=Some(42) // received as `Some(42)`
|
||||
/// />
|
||||
/// <MyComponent
|
||||
/// name="Bob" // automatically converted to String with `.into()`
|
||||
/// // optional values can both be omitted, and received as `None`
|
||||
/// />
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <MyComponent
|
||||
/// name="Greg" // automatically converted to String with `.into()`
|
||||
/// optional_value=42 // received as `Some(42)`
|
||||
/// optional_no_strip=Some(42) // received as `Some(42)`
|
||||
/// />
|
||||
/// <MyComponent
|
||||
/// name="Bob" // automatically converted to String with `.into()`
|
||||
/// // optional values can both be omitted, and received as `None`
|
||||
/// />
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
@@ -602,7 +671,9 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// - **Return types must be [Serializable](leptos_reactive::Serializable).**
|
||||
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
/// need to deserialize the result to return it to the client.
|
||||
/// - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
|
||||
/// They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
/// - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
|
||||
@@ -616,26 +687,18 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Props, attributes(builder))]
|
||||
pub fn derive_prop(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
props::impl_derive_prop(&input)
|
||||
.unwrap_or_else(|err| err.to_compile_error())
|
||||
.into()
|
||||
}
|
||||
|
||||
// Derive Params trait for routing
|
||||
/// Derives a trait that parses a map of string keys and values into a typed
|
||||
/// data structure, e.g., for route params.
|
||||
#[proc_macro_derive(Params, attributes(params))]
|
||||
pub fn params_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
pub fn params_derive(
|
||||
input: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
let ast = syn::parse(input).unwrap();
|
||||
params::impl_params(&ast)
|
||||
}
|
||||
|
||||
pub(crate) fn is_component_node(node: &NodeElement) -> bool {
|
||||
let name = node.name.to_string();
|
||||
let first_char = name.chars().next();
|
||||
first_char
|
||||
.map(|first_char| first_char.is_ascii_uppercase())
|
||||
.unwrap_or(false)
|
||||
node.name
|
||||
.to_string()
|
||||
.starts_with(|c: char| c.is_ascii_uppercase())
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn impl_params(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
|
||||
|
||||
let gen = quote! {
|
||||
impl Params for #name {
|
||||
fn from_map(map: &::leptos_router::ParamsMap) -> Result<Self, ::leptos_router::RouterError> {
|
||||
fn from_map(map: &::leptos_router::ParamsMap) -> Result<Self, ::leptos_router::ParamsError> {
|
||||
Ok(Self {
|
||||
#(#fields,)*
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user