mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 06:42:35 -05:00
Compare commits
18 Commits
fix-export
...
fix-hydrat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed405c1f27 | ||
|
|
3e47a4b566 | ||
|
|
0071a48b8a | ||
|
|
8d42e91eb8 | ||
|
|
00a796d204 | ||
|
|
bde585dc3e | ||
|
|
0a534bd7fd | ||
|
|
50d8eae694 | ||
|
|
e732a4952b | ||
|
|
8a99623fd6 | ||
|
|
7d6c4930e4 | ||
|
|
81d6689cc0 | ||
|
|
989b5b93c3 | ||
|
|
ca510f72c1 | ||
|
|
6dd3be75d1 | ||
|
|
51e11e756a | ||
|
|
1dbcfe2861 | ||
|
|
db3f46c501 |
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
|
||||
|
||||
|
||||
18
Cargo.toml
18
Cargo.toml
@@ -19,17 +19,17 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3"
|
||||
version = "0.2.0-alpha"
|
||||
|
||||
[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-alpha" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.0-alpha" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.0-alpha" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.0-alpha" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.0-alpha" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.0-alpha" }
|
||||
leptos_router = { path = "./router", version = "0.2.0-alpha" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.0-alpha" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -21,11 +21,7 @@ install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.build-wasm]
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "build-wasm", path = "leptos_reactive" },
|
||||
{ name = "build-wasm", path = "leptos_dom" },
|
||||
{ name = "build-wasm", path = "leptos_server" },
|
||||
]
|
||||
dependencies = [{ name = "build-wasm", path = "leptos" }]
|
||||
|
||||
[tasks.check-examples]
|
||||
clear = true
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
- [Error Handling](./view/07_errors.md)
|
||||
- [Parent-Child Communication](./view/08_parent_child.md)
|
||||
- [Passing Children to Components](./view/09_component_children.md)
|
||||
- [Interlude: Reactivity and Functions](interlude_functions.md)
|
||||
- [Testing]()
|
||||
- [Interlude: Reactivity and Functions](./interlude_functions.md)
|
||||
- [Testing](./testing.md)
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Async]()
|
||||
- [Resource]()
|
||||
|
||||
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).
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -84,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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,12 +21,11 @@ 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();
|
||||
log!("Errors: {errors:#?}");
|
||||
|
||||
@@ -47,7 +44,7 @@ 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 |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use leptos::Errors;
|
||||
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
|
||||
use leptos::{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
|
||||
@@ -11,12 +10,12 @@ pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
|
||||
<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()}
|
||||
each=errors
|
||||
// a unique key for each item as a reference
|
||||
key=|error| error.0.clone()
|
||||
key=|(key, _)| key.clone()
|
||||
// renders each item to a view
|
||||
view= move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
view= move |cx, (_, error)| {
|
||||
let error_string = error.to_string();
|
||||
view! {
|
||||
cx,
|
||||
<p>"Error: " {error_string}</p>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -29,6 +29,7 @@ sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{web_sys::HtmlInputElement, *};
|
||||
use leptos::{html::Input, leptos_dom::helpers::location_hash, *};
|
||||
use storage::TodoSerialized;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -143,17 +143,18 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
|
||||
});
|
||||
|
||||
// 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("");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -211,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
|
||||
|
||||
@@ -15,7 +15,11 @@ use actix_web::{
|
||||
};
|
||||
use futures::{Future, 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_meta::*;
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
@@ -93,13 +97,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
|
||||
@@ -584,14 +589,15 @@ fn html_parts(
|
||||
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");
|
||||
}}
|
||||
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.');
|
||||
|
||||
@@ -19,7 +19,10 @@ use axum::{
|
||||
use futures::{Future, SinkExt, Stream, StreamExt};
|
||||
use http::{header, method::Method, uri::Uri, version::Version, Response};
|
||||
use hyper::body;
|
||||
use leptos::*;
|
||||
use leptos::{
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
*,
|
||||
};
|
||||
use leptos_meta::MetaContext;
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
@@ -91,13 +94,14 @@ impl ResponseOptions {
|
||||
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
|
||||
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
|
||||
pub 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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Decomposes an HTTP request into its parts, allowing you to read its headers
|
||||
@@ -491,7 +495,7 @@ where
|
||||
};
|
||||
|
||||
let (bundle, runtime, scope) =
|
||||
render_to_stream_with_prefix_undisposed_with_context(
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| {
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
@@ -590,14 +594,15 @@ fn html_parts(
|
||||
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");
|
||||
}}
|
||||
let found = false;
|
||||
document.querySelectorAll("link").forEach((link) => {{
|
||||
if (link.getAttribute('href').includes(msg.css)) {{
|
||||
let newHref = '/' + href + '?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.');
|
||||
|
||||
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",
|
||||
]
|
||||
@@ -46,15 +46,15 @@ where
|
||||
let children = children(cx);
|
||||
|
||||
move || {
|
||||
match errors.get().0.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>
|
||||
</>
|
||||
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),
|
||||
}
|
||||
.into_view(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,16 +141,29 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub use leptos_config::*;
|
||||
pub use leptos_config::{self, get_configuration, LeptosOptions};
|
||||
#[cfg(not(all(
|
||||
target_arch = "wasm32",
|
||||
any(feature = "csr", feature = "hydrate")
|
||||
)))]
|
||||
pub use leptos_dom::ssr::{self, render_to_string};
|
||||
pub use leptos_dom::{
|
||||
self,
|
||||
wasm_bindgen::{JsCast, UnwrapThrowExt},
|
||||
*,
|
||||
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::{self, *};
|
||||
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::*;
|
||||
@@ -161,7 +174,9 @@ pub use show::*;
|
||||
mod suspense;
|
||||
pub use suspense::*;
|
||||
mod transition;
|
||||
pub use leptos_dom::debug_warn;
|
||||
#[cfg(debug_assertions)]
|
||||
#[doc(hidden)]
|
||||
pub use tracing;
|
||||
pub use transition::*;
|
||||
|
||||
extern crate self as leptos;
|
||||
@@ -179,9 +194,7 @@ pub type ChildrenFn = Box<dyn Fn(Scope) -> Fragment>;
|
||||
pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
|
||||
|
||||
/// A type for taking anything that implements [`IntoAttribute`].
|
||||
/// Very usefull inside components.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```rust
|
||||
/// use leptos::*;
|
||||
///
|
||||
|
||||
@@ -87,6 +87,7 @@ where
|
||||
} else {
|
||||
// run the child; we'll probably throw this away, but it will register resource reads
|
||||
let child = orig_child(cx).into_view(cx);
|
||||
let after_original_child = HydrationCtx::id();
|
||||
|
||||
let initial = {
|
||||
// no resources were read under this, so just return the child
|
||||
@@ -118,8 +119,7 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
|
||||
HydrationCtx::continue_from(after_original_child);
|
||||
initial
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[tasks.build-wasm]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features", "--target=wasm32-unknown-unknown"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -5,7 +5,66 @@ 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
|
||||
@@ -13,7 +72,7 @@ where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||
let id = HydrationCtx::peek().previous;
|
||||
let id = ErrorKey(HydrationCtx::peek().previous);
|
||||
let errors = use_context::<RwSignal<Errors>>(cx);
|
||||
match self {
|
||||
Ok(stuff) => {
|
||||
@@ -45,7 +104,7 @@ where
|
||||
on_cleanup(cx, move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
errors.remove::<E>(&id);
|
||||
errors.remove(&id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -67,25 +126,37 @@ where
|
||||
}
|
||||
}
|
||||
impl Errors {
|
||||
/// 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: String, error: E)
|
||||
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(String::new(), Arc::new(error));
|
||||
self.0.insert(Default::default(), 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);
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,29 @@ use wasm_bindgen::{
|
||||
};
|
||||
|
||||
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,
|
||||
@@ -39,7 +58,7 @@ pub fn add_event_listener<E>(
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn add_event_listener_undelegated<E>(
|
||||
pub(crate) fn add_event_listener_undelegated<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: &str,
|
||||
mut cb: impl FnMut(E) + 'static,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Collection of typed events.
|
||||
//! Types for all DOM events.
|
||||
|
||||
use std::{borrow::Cow, marker::PhantomData};
|
||||
use wasm_bindgen::convert::FromWasmAbi;
|
||||
@@ -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,3 +1,5 @@
|
||||
//! A variety of DOM utility functions.
|
||||
|
||||
use crate::{is_server, window};
|
||||
use std::time::Duration;
|
||||
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod math;
|
||||
pub mod svg;
|
||||
//! Exports types for working with HTML elements.
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
@@ -25,7 +24,7 @@ cfg_if! {
|
||||
use crate::hydration::HydrationKey;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
const HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG: &str =
|
||||
pub(crate) const HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG: &str =
|
||||
"`Deref<Target = web_sys::HtmlElement>` and `AsRef<web_sys::HtmlElement>` \
|
||||
can only be used on web targets. \
|
||||
This is for the same reason that normal `wasm_bindgen` methods can be used \
|
||||
@@ -162,6 +161,7 @@ pub struct Custom {
|
||||
}
|
||||
|
||||
impl Custom {
|
||||
/// Creates a new custom element with the given tag name.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
let name = name.into();
|
||||
let id = HydrationCtx::id();
|
||||
@@ -295,7 +295,7 @@ where
|
||||
}
|
||||
|
||||
impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
fn new(cx: Scope, element: El) -> Self {
|
||||
pub(crate) fn new(cx: Scope, element: El) -> Self {
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
Self {
|
||||
|
||||
@@ -110,7 +110,7 @@ impl HydrationCtx {
|
||||
ID.with(|id| *id.borrow_mut() = Default::default());
|
||||
}
|
||||
|
||||
/// Resums hydration from the provided `id`. Usefull for
|
||||
/// 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);
|
||||
|
||||
@@ -5,30 +5,32 @@
|
||||
|
||||
//! The DOM implementation for `leptos`.
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg_attr(debug_assertions, macro_use)]
|
||||
pub extern crate tracing;
|
||||
|
||||
mod components;
|
||||
mod events;
|
||||
mod helpers;
|
||||
#[doc(hidden)]
|
||||
pub mod helpers;
|
||||
pub mod html;
|
||||
mod hydration;
|
||||
mod logging;
|
||||
mod macro_helpers;
|
||||
pub mod math;
|
||||
mod node_ref;
|
||||
mod ssr;
|
||||
pub mod ssr;
|
||||
pub mod svg;
|
||||
mod transparent;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
pub use components::*;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub use events::add_event_helper;
|
||||
pub use events::typed as ev;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use events::{add_event_listener, add_event_listener_undelegated};
|
||||
pub use helpers::*;
|
||||
pub use html::*;
|
||||
pub use html::HtmlElement;
|
||||
use html::{AnyElement, ElementDescriptor};
|
||||
pub use hydration::{HydrationCtx, HydrationKey};
|
||||
pub use js_sys;
|
||||
use leptos_reactive::Scope;
|
||||
pub use logging::*;
|
||||
pub use macro_helpers::*;
|
||||
@@ -37,17 +39,13 @@ pub use node_ref::*;
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
use smallvec::SmallVec;
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub use ssr::*;
|
||||
use std::{borrow::Cow, fmt};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
pub use transparent::*;
|
||||
pub use wasm_bindgen;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
pub use web_sys;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
thread_local! {
|
||||
@@ -64,7 +62,8 @@ pub trait IntoView {
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
trait Mountable {
|
||||
#[doc(hidden)]
|
||||
pub trait Mountable {
|
||||
/// Gets the [`web_sys::Node`] that can be directly inserted as
|
||||
/// a child of another node. Typically, this is a [`web_sys::DocumentFragment`]
|
||||
/// for components, and [`web_sys::HtmlElement`] for elements.
|
||||
@@ -142,9 +141,11 @@ cfg_if! {
|
||||
/// HTML element.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Element {
|
||||
#[doc(hidden)]
|
||||
#[cfg(debug_assertions)]
|
||||
name: Cow<'static, str>,
|
||||
element: web_sys::HtmlElement,
|
||||
pub name: Cow<'static, str>,
|
||||
#[doc(hidden)]
|
||||
pub element: web_sys::HtmlElement,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Element {
|
||||
@@ -619,7 +620,11 @@ impl View {
|
||||
#[cfg_attr(debug_assertions, instrument)]
|
||||
#[track_caller]
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
fn mount_child<GWSN: Mountable + fmt::Debug>(kind: MountKind, child: &GWSN) {
|
||||
#[doc(hidden)]
|
||||
pub fn mount_child<GWSN: Mountable + fmt::Debug>(
|
||||
kind: MountKind,
|
||||
child: &GWSN,
|
||||
) {
|
||||
let child = child.get_mountable_node();
|
||||
|
||||
match kind {
|
||||
@@ -682,7 +687,8 @@ fn prepare_to_move(
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[derive(Debug)]
|
||||
enum MountKind<'a> {
|
||||
#[doc(hidden)]
|
||||
pub enum MountKind<'a> {
|
||||
Before(
|
||||
// The closing node
|
||||
&'a web_sys::Node,
|
||||
@@ -690,7 +696,7 @@ enum MountKind<'a> {
|
||||
Append(&'a web_sys::Node),
|
||||
}
|
||||
|
||||
/// Runs the provided closure and mounts the result to eht `<body>`.
|
||||
/// Runs the provided closure and mounts the result to the `<body>`.
|
||||
pub fn mount_to_body<F, N>(f: F)
|
||||
where
|
||||
F: FnOnce(Scope) -> N + 'static,
|
||||
|
||||
@@ -6,7 +6,8 @@ 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.
|
||||
@@ -103,7 +104,7 @@ impl std::fmt::Debug for Attribute {
|
||||
pub trait IntoAttribute {
|
||||
/// Converts the object into an [Attribute].
|
||||
fn into_attribute(self, cx: Scope) -> Attribute;
|
||||
/// Helper function for dealing with [Box<dyn IntoAttribute>]
|
||||
/// Helper function for dealing with `Box<dyn IntoAttribute>`.
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute;
|
||||
}
|
||||
|
||||
@@ -253,7 +254,8 @@ 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(
|
||||
#[doc(hidden)]
|
||||
pub fn attribute_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Attribute,
|
||||
|
||||
@@ -7,7 +7,8 @@ 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),
|
||||
@@ -70,7 +71,8 @@ impl<T: IntoClass> IntoClass for (Scope, T) {
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn class_helper(
|
||||
#[doc(hidden)]
|
||||
pub fn class_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Class,
|
||||
|
||||
@@ -7,7 +7,8 @@ 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),
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
use crate::{ElementDescriptor, HtmlElement};
|
||||
use crate::{html::ElementDescriptor, HtmlElement};
|
||||
use leptos_reactive::{create_effect, create_rw_signal, 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::<Input>::new(cx);
|
||||
/// let input_ref = create_node_ref::<Input>(cx);
|
||||
///
|
||||
/// let on_click = move |_| {
|
||||
/// let node =
|
||||
@@ -34,8 +37,46 @@ pub struct NodeRef<T: ElementDescriptor + 'static>(
|
||||
RwSignal<Option<HtmlElement<T>>>,
|
||||
);
|
||||
|
||||
/// 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: 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))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#![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};
|
||||
|
||||
@@ -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"))]
|
||||
@@ -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"
|
||||
|
||||
@@ -178,7 +178,7 @@ impl ToTokens for Model {
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
::leptos::Component::new(
|
||||
::leptos::leptos_dom::Component::new(
|
||||
stringify!(#name),
|
||||
move |cx| {
|
||||
#tracing_guard_expr
|
||||
|
||||
@@ -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)]
|
||||
@@ -32,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:
|
||||
@@ -121,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"
|
||||
@@ -260,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! {
|
||||
@@ -345,6 +346,43 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
/// An optimized, cached template for client-side rendering. Follows the same
|
||||
/// syntax as the [view](crate::macro) 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
|
||||
@@ -468,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!()
|
||||
@@ -477,6 +517,8 @@ 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
|
||||
@@ -629,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
|
||||
@@ -643,16 +687,8 @@ 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,
|
||||
@@ -662,9 +698,7 @@ pub fn params_derive(
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,8 +35,8 @@ pub fn server_macro_impl(
|
||||
} = syn::parse::<ServerFnName>(args)?;
|
||||
let prefix = prefix.unwrap_or_else(|| Literal::string(""));
|
||||
let encoding = match encoding {
|
||||
Encoding::Cbor => quote! { ::leptos::Encoding::Cbor },
|
||||
Encoding::Url => quote! { ::leptos::Encoding::Url },
|
||||
Encoding::Cbor => quote! { ::leptos::leptos_server::Encoding::Cbor },
|
||||
Encoding::Url => quote! { ::leptos::leptos_server::Encoding::Url },
|
||||
};
|
||||
|
||||
let body = syn::parse::<ServerFnBody>(s.into())?;
|
||||
@@ -166,7 +166,7 @@ pub fn server_macro_impl(
|
||||
#url
|
||||
}
|
||||
|
||||
fn encoding() -> ::leptos::Encoding {
|
||||
fn encoding() -> ::leptos::leptos_server::Encoding {
|
||||
#encoding
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ pub fn server_macro_impl(
|
||||
#vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty {
|
||||
let prefix = #struct_name::prefix().to_string();
|
||||
let url = prefix + "/" + #struct_name::url();
|
||||
::leptos::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
|
||||
::leptos::leptos_server::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
522
leptos_macro/src/template.rs
Normal file
522
leptos_macro/src/template.rs
Normal file
@@ -0,0 +1,522 @@
|
||||
use crate::is_component_node;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::spanned::Spanned;
|
||||
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeValueExpr};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) fn render_template(cx: &Ident, nodes: &[Node]) -> TokenStream {
|
||||
let template_uid = Ident::new(
|
||||
&format!("TEMPLATE_{}", Uuid::new_v4().simple()),
|
||||
Span::call_site(),
|
||||
);
|
||||
|
||||
if nodes.len() == 1 {
|
||||
first_node_to_tokens(cx, &template_uid, &nodes[0])
|
||||
} else {
|
||||
panic!("template! takes a single root element.")
|
||||
}
|
||||
}
|
||||
|
||||
fn first_node_to_tokens(
|
||||
cx: &Ident,
|
||||
template_uid: &Ident,
|
||||
node: &Node,
|
||||
) -> TokenStream {
|
||||
match node {
|
||||
Node::Element(node) => root_element_to_tokens(cx, template_uid, node),
|
||||
_ => panic!("template! takes a single root element."),
|
||||
}
|
||||
}
|
||||
|
||||
fn root_element_to_tokens(
|
||||
cx: &Ident,
|
||||
template_uid: &Ident,
|
||||
node: &NodeElement,
|
||||
) -> TokenStream {
|
||||
let mut template = String::new();
|
||||
let mut navigations = Vec::new();
|
||||
let mut expressions = Vec::new();
|
||||
|
||||
if is_component_node(node) {
|
||||
crate::view::component_to_tokens(cx, node, None)
|
||||
} else {
|
||||
element_to_tokens(
|
||||
cx,
|
||||
node,
|
||||
&Ident::new("root", Span::call_site()),
|
||||
None,
|
||||
&mut 0,
|
||||
&mut 0,
|
||||
&mut template,
|
||||
&mut navigations,
|
||||
&mut expressions,
|
||||
true,
|
||||
);
|
||||
|
||||
// create the root element from which navigations and expressions will begin
|
||||
let generate_root = quote! {
|
||||
let root = #template_uid.with(|tpl| tpl.content().clone_node_with_deep(true))
|
||||
.unwrap()
|
||||
.first_child()
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
let span = node.name.span();
|
||||
|
||||
let navigations = if navigations.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! { #(#navigations);* }
|
||||
};
|
||||
|
||||
let expressions = if expressions.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! { #(#expressions;);* }
|
||||
};
|
||||
|
||||
let tag_name = node.name.to_string();
|
||||
|
||||
quote_spanned! {
|
||||
span => {
|
||||
thread_local! {
|
||||
static #template_uid: web_sys::HtmlTemplateElement = {
|
||||
let document = leptos::document();
|
||||
let el = document.create_element("template").unwrap();
|
||||
el.set_inner_html(#template);
|
||||
el.unchecked_into()
|
||||
};
|
||||
}
|
||||
|
||||
#generate_root
|
||||
|
||||
#navigations
|
||||
#expressions
|
||||
|
||||
leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
|
||||
#[cfg(debug_assertions)]
|
||||
name: #tag_name.into(),
|
||||
element: root.unchecked_into()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum PrevSibChange {
|
||||
Sib(Ident),
|
||||
Parent,
|
||||
Skip,
|
||||
}
|
||||
|
||||
fn attributes(node: &NodeElement) -> impl Iterator<Item = &NodeAttribute> {
|
||||
node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(attribute) = node {
|
||||
Some(attribute)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn element_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
parent: &Ident,
|
||||
prev_sib: Option<Ident>,
|
||||
next_el_id: &mut usize,
|
||||
next_co_id: &mut usize,
|
||||
template: &mut String,
|
||||
navigations: &mut Vec<TokenStream>,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
is_root_el: bool,
|
||||
) -> Ident {
|
||||
// create this element
|
||||
*next_el_id += 1;
|
||||
let this_el_ident = child_ident(*next_el_id, node.name.span());
|
||||
|
||||
// Open tag
|
||||
let name_str = node.name.to_string();
|
||||
let span = node.name.span();
|
||||
|
||||
// CSR/hydrate, push to template
|
||||
template.push('<');
|
||||
template.push_str(&name_str);
|
||||
|
||||
// attributes
|
||||
for attr in attributes(node) {
|
||||
attr_to_tokens(cx, attr, &this_el_ident, template, expressions);
|
||||
}
|
||||
|
||||
// navigation for this el
|
||||
let debug_name = node.name.to_string();
|
||||
let this_nav = if is_root_el {
|
||||
quote_spanned! {
|
||||
span => let #this_el_ident = #debug_name;
|
||||
let #this_el_ident = #parent.clone().unchecked_into::<web_sys::Node>();
|
||||
//debug!("=> got {}", #this_el_ident.node_name());
|
||||
}
|
||||
} else if let Some(prev_sib) = &prev_sib {
|
||||
quote_spanned! {
|
||||
span => let #this_el_ident = #debug_name;
|
||||
//log::debug!("next_sibling ({})", #debug_name);
|
||||
let #this_el_ident = #prev_sib.next_sibling().unwrap_or_else(|| panic!("error : {} => {} ", #debug_name, "nextSibling"));
|
||||
//log::debug!("=> got {}", #this_el_ident.node_name());
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {
|
||||
span => let #this_el_ident = #debug_name;
|
||||
//log::debug!("first_child ({})", #debug_name);
|
||||
let #this_el_ident = #parent.first_child().unwrap_or_else(|| panic!("error: {} => {}", #debug_name, "firstChild"));
|
||||
//log::debug!("=> got {}", #this_el_ident.node_name());
|
||||
}
|
||||
};
|
||||
navigations.push(this_nav);
|
||||
|
||||
// self-closing tags
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
|
||||
if matches!(
|
||||
name_str.as_str(),
|
||||
"area"
|
||||
| "base"
|
||||
| "br"
|
||||
| "col"
|
||||
| "embed"
|
||||
| "hr"
|
||||
| "img"
|
||||
| "input"
|
||||
| "link"
|
||||
| "meta"
|
||||
| "param"
|
||||
| "source"
|
||||
| "track"
|
||||
| "wbr"
|
||||
) {
|
||||
template.push_str("/>");
|
||||
return this_el_ident;
|
||||
} else {
|
||||
template.push('>');
|
||||
}
|
||||
|
||||
// iterate over children
|
||||
let mut prev_sib = prev_sib;
|
||||
for (idx, child) in node.children.iter().enumerate() {
|
||||
// set next sib (for any insertions)
|
||||
let next_sib = next_sibling_node(&node.children, idx + 1, next_el_id);
|
||||
|
||||
let curr_id = child_to_tokens(
|
||||
cx,
|
||||
child,
|
||||
&this_el_ident,
|
||||
if idx == 0 { None } else { prev_sib.clone() },
|
||||
next_sib,
|
||||
next_el_id,
|
||||
next_co_id,
|
||||
template,
|
||||
navigations,
|
||||
expressions,
|
||||
);
|
||||
|
||||
prev_sib = match curr_id {
|
||||
PrevSibChange::Sib(id) => Some(id),
|
||||
PrevSibChange::Parent => None,
|
||||
PrevSibChange::Skip => prev_sib,
|
||||
};
|
||||
}
|
||||
|
||||
// close tag
|
||||
template.push_str("</");
|
||||
template.push_str(&name_str);
|
||||
template.push('>');
|
||||
|
||||
this_el_ident
|
||||
}
|
||||
|
||||
fn next_sibling_node(
|
||||
children: &[Node],
|
||||
idx: usize,
|
||||
next_el_id: &mut usize,
|
||||
) -> Option<Ident> {
|
||||
if children.len() <= idx {
|
||||
None
|
||||
} else {
|
||||
let sibling = &children[idx];
|
||||
|
||||
match sibling {
|
||||
Node::Element(sibling) => {
|
||||
if is_component_node(sibling) {
|
||||
next_sibling_node(children, idx + 1, next_el_id)
|
||||
} else {
|
||||
Some(child_ident(*next_el_id + 1, sibling.name.span()))
|
||||
}
|
||||
}
|
||||
Node::Block(sibling) => {
|
||||
Some(child_ident(*next_el_id + 1, sibling.value.span()))
|
||||
}
|
||||
Node::Text(sibling) => {
|
||||
Some(child_ident(*next_el_id + 1, sibling.value.span()))
|
||||
}
|
||||
_ => panic!("expected either an element or a block"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn attr_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeAttribute,
|
||||
el_id: &Ident,
|
||||
template: &mut String,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
) {
|
||||
let name = node.key.to_string();
|
||||
let name = if name.starts_with('_') {
|
||||
name.replacen('_', "", 1)
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let name = if name.starts_with("attr:") {
|
||||
name.replacen("attr:", "", 1)
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let value = match &node.value {
|
||||
Some(expr) => match expr.as_ref() {
|
||||
syn::Expr::Lit(expr_lit) => {
|
||||
if let syn::Lit::Str(s) = &expr_lit.lit {
|
||||
AttributeValue::Static(s.value())
|
||||
} else {
|
||||
AttributeValue::Dynamic(expr)
|
||||
}
|
||||
}
|
||||
_ => AttributeValue::Dynamic(expr),
|
||||
},
|
||||
None => AttributeValue::Empty,
|
||||
};
|
||||
|
||||
let span = node.key.span();
|
||||
|
||||
// refs
|
||||
if name == "ref" {
|
||||
panic!("node_ref not yet supported in template! macro")
|
||||
}
|
||||
// Event Handlers
|
||||
else if name.starts_with("on:") {
|
||||
let (event_type, handler) =
|
||||
crate::view::event_from_attribute_node(node, false);
|
||||
expressions.push(quote! {
|
||||
leptos::leptos_dom::add_event_helper(#el_id.unchecked_ref(), #event_type, #handler);
|
||||
})
|
||||
}
|
||||
// Properties
|
||||
else if name.starts_with("prop:") {
|
||||
let name = name.replacen("prop:", "", 1);
|
||||
let value = node
|
||||
.value
|
||||
.as_ref()
|
||||
.expect("prop: blocks need values")
|
||||
.as_ref();
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos_dom::property(#cx, #el_id.unchecked_ref(), #name, #value.into_property(#cx))
|
||||
});
|
||||
}
|
||||
// Classes
|
||||
else if name.starts_with("class:") {
|
||||
let name = name.replacen("class:", "", 1);
|
||||
let value = node
|
||||
.value
|
||||
.as_ref()
|
||||
.expect("class: attributes need values")
|
||||
.as_ref();
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos::leptos_dom::class_helper(#el_id.unchecked_ref(), #name.into(), #value.into_class(#cx))
|
||||
});
|
||||
}
|
||||
// Attributes
|
||||
else {
|
||||
match value {
|
||||
AttributeValue::Empty => {
|
||||
template.push(' ');
|
||||
template.push_str(&name);
|
||||
}
|
||||
|
||||
// Static attributes (i.e., just a literal given as value, not an expression)
|
||||
// are just set in the template — again, nothing programmatic
|
||||
AttributeValue::Static(value) => {
|
||||
template.push(' ');
|
||||
template.push_str(&name);
|
||||
template.push_str("=\"");
|
||||
template.push_str(&value);
|
||||
template.push('"');
|
||||
}
|
||||
AttributeValue::Dynamic(value) => {
|
||||
// For client-side rendering, dynamic attributes don't need to be rendered in the template
|
||||
// They'll immediately be set synchronously before the cloned template is mounted
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos::leptos_dom::attribute_helper(#el_id.unchecked_ref(), #name.into(), {#value}.into_attribute(#cx))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AttributeValue<'a> {
|
||||
Static(String),
|
||||
Dynamic(&'a syn::Expr),
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn child_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &Node,
|
||||
parent: &Ident,
|
||||
prev_sib: Option<Ident>,
|
||||
next_sib: Option<Ident>,
|
||||
next_el_id: &mut usize,
|
||||
next_co_id: &mut usize,
|
||||
template: &mut String,
|
||||
navigations: &mut Vec<TokenStream>,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
) -> PrevSibChange {
|
||||
match node {
|
||||
Node::Element(node) => {
|
||||
if is_component_node(node) {
|
||||
proc_macro_error::emit_error!(
|
||||
node.name.span(),
|
||||
"component children not allowed in template!, use view! \
|
||||
instead"
|
||||
);
|
||||
PrevSibChange::Skip
|
||||
} else {
|
||||
PrevSibChange::Sib(element_to_tokens(
|
||||
cx,
|
||||
node,
|
||||
parent,
|
||||
prev_sib,
|
||||
next_el_id,
|
||||
next_co_id,
|
||||
template,
|
||||
navigations,
|
||||
expressions,
|
||||
false,
|
||||
))
|
||||
}
|
||||
}
|
||||
Node::Text(node) => block_to_tokens(
|
||||
cx,
|
||||
&node.value,
|
||||
node.value.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
next_sib,
|
||||
next_el_id,
|
||||
template,
|
||||
expressions,
|
||||
navigations,
|
||||
),
|
||||
Node::Block(node) => block_to_tokens(
|
||||
cx,
|
||||
&node.value,
|
||||
node.value.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
next_sib,
|
||||
next_el_id,
|
||||
template,
|
||||
expressions,
|
||||
navigations,
|
||||
),
|
||||
_ => panic!("unexpected child node type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn block_to_tokens(
|
||||
_cx: &Ident,
|
||||
value: &NodeValueExpr,
|
||||
span: Span,
|
||||
parent: &Ident,
|
||||
prev_sib: Option<Ident>,
|
||||
next_sib: Option<Ident>,
|
||||
next_el_id: &mut usize,
|
||||
template: &mut String,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
navigations: &mut Vec<TokenStream>,
|
||||
) -> PrevSibChange {
|
||||
let value = value.as_ref();
|
||||
let str_value = match value {
|
||||
syn::Expr::Lit(lit) => match &lit.lit {
|
||||
syn::Lit::Str(s) => Some(s.value()),
|
||||
syn::Lit::Char(c) => Some(c.value().to_string()),
|
||||
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
|
||||
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// code to navigate to this text node
|
||||
|
||||
let (name, location) = /* if is_first_child && mode == Mode::Client {
|
||||
(None, quote! { })
|
||||
}
|
||||
else */ {
|
||||
*next_el_id += 1;
|
||||
let name = child_ident(*next_el_id, span);
|
||||
let location = if let Some(sibling) = &prev_sib {
|
||||
quote_spanned! {
|
||||
span => //log::debug!("-> next sibling");
|
||||
let #name = #sibling.next_sibling().unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "nextSibling"));
|
||||
//log::debug!("\tnext sibling = {}", #name.node_name());
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {
|
||||
span => //log::debug!("\\|/ first child on {}", #parent.node_name());
|
||||
let #name = #parent.first_child().unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "firstChild"));
|
||||
//log::debug!("\tfirst child = {}", #name.node_name());
|
||||
}
|
||||
};
|
||||
(Some(name), location)
|
||||
};
|
||||
|
||||
let mount_kind = match &next_sib {
|
||||
Some(child) => {
|
||||
quote! { leptos::leptos_dom::MountKind::Before(#child.clone()) }
|
||||
}
|
||||
None => {
|
||||
quote! { leptos::leptos_dom::MountKind::Append(&#parent) }
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(v) = str_value {
|
||||
navigations.push(location);
|
||||
template.push_str(&v);
|
||||
|
||||
if let Some(name) = name {
|
||||
PrevSibChange::Sib(name)
|
||||
} else {
|
||||
PrevSibChange::Parent
|
||||
}
|
||||
} else {
|
||||
template.push_str("<!>");
|
||||
navigations.push(location);
|
||||
|
||||
expressions.push(quote! {
|
||||
leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
|
||||
});
|
||||
|
||||
if let Some(name) = name {
|
||||
PrevSibChange::Sib(name)
|
||||
} else {
|
||||
PrevSibChange::Parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn child_ident(el_id: usize, span: Span) -> Ident {
|
||||
let id = format!("_el{el_id}");
|
||||
Ident::new(&id, span)
|
||||
}
|
||||
@@ -152,7 +152,7 @@ pub(crate) fn render_view(
|
||||
0 => {
|
||||
let span = Span::call_site();
|
||||
quote_spanned! {
|
||||
span => leptos::Unit
|
||||
span => leptos::leptos_dom::Unit
|
||||
}
|
||||
}
|
||||
1 => root_node_to_tokens_ssr(cx, &nodes[0], global_class),
|
||||
@@ -168,7 +168,7 @@ pub(crate) fn render_view(
|
||||
0 => {
|
||||
let span = Span::call_site();
|
||||
quote_spanned! {
|
||||
span => leptos::Unit
|
||||
span => leptos::leptos_dom::Unit
|
||||
}
|
||||
}
|
||||
1 => node_to_tokens(cx, &nodes[0], TagType::Unknown, global_class),
|
||||
@@ -200,7 +200,7 @@ fn root_node_to_tokens_ssr(
|
||||
Node::Text(node) => {
|
||||
let value = node.value.as_ref();
|
||||
quote! {
|
||||
leptos::text(#value)
|
||||
leptos::leptos_dom::html::text(#value)
|
||||
}
|
||||
}
|
||||
Node::Block(node) => {
|
||||
@@ -287,11 +287,11 @@ fn root_element_to_tokens_ssr(
|
||||
} else if is_math_ml_element(&tag_name) {
|
||||
quote! { math::#typed_element_name }
|
||||
} else {
|
||||
quote! { #typed_element_name }
|
||||
quote! { html::#typed_element_name }
|
||||
};
|
||||
let full_name = if is_custom_element {
|
||||
quote! {
|
||||
leptos::leptos_dom::Custom::new(#tag_name)
|
||||
leptos::leptos_dom::html::Custom::new(#tag_name)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
@@ -347,9 +347,9 @@ fn element_to_tokens_ssr(
|
||||
|
||||
// insert hydration ID
|
||||
let hydration_id = if is_root {
|
||||
quote! { leptos::HydrationCtx::peek(), }
|
||||
quote! { leptos::leptos_dom::HydrationCtx::peek(), }
|
||||
} else {
|
||||
quote! { leptos::HydrationCtx::id(), }
|
||||
quote! { leptos::leptos_dom::HydrationCtx::id(), }
|
||||
};
|
||||
match node
|
||||
.attributes
|
||||
@@ -456,7 +456,7 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
} else if name.strip_prefix("on:").is_some() {
|
||||
let (event_type, handler) = event_from_attribute_node(node, false);
|
||||
exprs_for_compiler.push(quote! {
|
||||
leptos::ssr_event_listener(#event_type, #handler);
|
||||
leptos::leptos_dom::helpers::ssr_event_listener(#event_type, #handler);
|
||||
})
|
||||
} else if name.strip_prefix("prop:").is_some()
|
||||
|| name.strip_prefix("class:").is_some()
|
||||
@@ -483,7 +483,7 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
holes.push(quote! {
|
||||
&{#value}.into_attribute(#cx)
|
||||
.as_nameless_value_string()
|
||||
.map(|a| format!("{}=\"{}\"", #name, leptos::escape_attr(&a)))
|
||||
.map(|a| format!("{}=\"{}\"", #name, leptos::leptos_dom::ssr::escape_attr(&a)))
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
@@ -607,7 +607,7 @@ fn set_class_attribute_ssr(
|
||||
let value = value.as_ref();
|
||||
holes.push(quote! {
|
||||
&(cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
.map(|a| leptos::escape_attr(&a).to_string())
|
||||
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
|
||||
.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
@@ -682,7 +682,7 @@ fn node_to_tokens(
|
||||
Node::Text(node) => {
|
||||
let value = node.value.as_ref();
|
||||
quote! {
|
||||
leptos::text(#value)
|
||||
leptos::leptos_dom::html::text(#value)
|
||||
}
|
||||
}
|
||||
Node::Block(node) => {
|
||||
@@ -708,7 +708,7 @@ fn element_to_tokens(
|
||||
let tag = node.name.to_string();
|
||||
let name = if is_custom_element(&tag) {
|
||||
let name = node.name.to_string();
|
||||
quote! { leptos::leptos_dom::custom(#cx, leptos::leptos_dom::Custom::new(#name)) }
|
||||
quote! { leptos::leptos_dom::html::custom(#cx, leptos::leptos_dom::html::Custom::new(#name)) }
|
||||
} else if is_svg_element(&tag) {
|
||||
let name = &node.name;
|
||||
parent_type = TagType::Svg;
|
||||
@@ -725,10 +725,12 @@ fn element_to_tokens(
|
||||
/* proc_macro_error::emit_warning!(name.span(), "The view macro is assuming this is an HTML element, \
|
||||
but it is ambiguous; if it is an SVG or MathML element, prefix with svg:: or math::"); */
|
||||
quote! {
|
||||
leptos::leptos_dom::#name(#cx)
|
||||
leptos::leptos_dom::html::#name(#cx)
|
||||
}
|
||||
}
|
||||
TagType::Html => quote! { leptos::leptos_dom::#name(#cx) },
|
||||
TagType::Html => {
|
||||
quote! { leptos::leptos_dom::html::#name(#cx) }
|
||||
}
|
||||
TagType::Svg => quote! { leptos::leptos_dom::svg::#name(#cx) },
|
||||
TagType::Math => {
|
||||
quote! { leptos::leptos_dom::math::#name(#cx) }
|
||||
@@ -737,7 +739,7 @@ fn element_to_tokens(
|
||||
} else {
|
||||
let name = &node.name;
|
||||
parent_type = TagType::Html;
|
||||
quote! { leptos::leptos_dom::#name(#cx) }
|
||||
quote! { leptos::leptos_dom::html::#name(#cx) }
|
||||
};
|
||||
let attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
@@ -830,7 +832,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
.expect("couldn't parse event name");
|
||||
|
||||
let event_type = if is_custom {
|
||||
quote! { Custom::new(#name) }
|
||||
quote! { leptos::ev::Custom::new(#name) }
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
@@ -967,7 +969,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn component_to_tokens(
|
||||
pub(crate) fn component_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
global_class: Option<&TokenTree>,
|
||||
@@ -1079,7 +1081,7 @@ fn component_to_tokens(
|
||||
}
|
||||
}
|
||||
|
||||
fn event_from_attribute_node(
|
||||
pub(crate) fn event_from_attribute_node(
|
||||
attr: &NodeAttribute,
|
||||
force_undelegated: bool,
|
||||
) -> (TokenStream, &Expr) {
|
||||
@@ -1105,9 +1107,9 @@ fn event_from_attribute_node(
|
||||
.expect("couldn't parse event name");
|
||||
|
||||
let event_type = if force_undelegated || name_undelegated {
|
||||
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
|
||||
quote! { ::leptos::leptos_dom::ev::undelegated(::leptos::leptos_dom::ev::#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::ev::#event_type }
|
||||
quote! { ::leptos::leptos_dom::ev::#event_type }
|
||||
};
|
||||
(event_type, handler)
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[tasks.build-wasm]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features", "--target=wasm32-unknown-unknown"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -67,7 +67,7 @@
|
||||
//! ```
|
||||
|
||||
#[cfg_attr(debug_assertions, macro_use)]
|
||||
pub extern crate tracing;
|
||||
extern crate tracing;
|
||||
|
||||
mod context;
|
||||
mod effect;
|
||||
|
||||
@@ -509,8 +509,8 @@ slotmap::new_key_type! {
|
||||
|
||||
impl<S, T> Clone for Resource<S, T>
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
S: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
@@ -526,8 +526,8 @@ where
|
||||
|
||||
impl<S, T> Copy for Resource<S, T>
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
S: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +167,57 @@ impl RuntimeId {
|
||||
)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_many_signals_with_map<T, U>(
|
||||
self,
|
||||
cx: Scope,
|
||||
values: impl IntoIterator<Item = T>,
|
||||
map_fn: impl Fn((ReadSignal<T>, WriteSignal<T>)) -> U,
|
||||
) -> Vec<U>
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
with_runtime(self, move |runtime| {
|
||||
let mut signals = runtime.signals.borrow_mut();
|
||||
let properties = runtime.scopes.borrow();
|
||||
let mut properties = properties
|
||||
.get(cx.id)
|
||||
.expect(
|
||||
"tried to add signals to a scope that has been disposed",
|
||||
)
|
||||
.borrow_mut();
|
||||
let values = values.into_iter();
|
||||
let size = values.size_hint().0;
|
||||
signals.reserve(size);
|
||||
properties.reserve(size);
|
||||
values
|
||||
.map(|value| signals.insert(Rc::new(RefCell::new(value))))
|
||||
.map(|id| {
|
||||
properties.push(ScopeProperty::Signal(id));
|
||||
(
|
||||
ReadSignal {
|
||||
runtime: self,
|
||||
id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
},
|
||||
WriteSignal {
|
||||
runtime: self,
|
||||
id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.map(map_fn)
|
||||
.collect()
|
||||
})
|
||||
.expect("tried to create a signal in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_rw_signal<T>(self, value: T) -> RwSignal<T>
|
||||
where
|
||||
T: Any + 'static,
|
||||
|
||||
@@ -69,6 +69,50 @@ pub fn create_signal<T>(
|
||||
s
|
||||
}
|
||||
|
||||
/// Works exactly as [create_signal], but creates multiple signals at once.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_many_signals<T>(
|
||||
cx: Scope,
|
||||
values: impl IntoIterator<Item = T>,
|
||||
) -> Vec<(ReadSignal<T>, WriteSignal<T>)> {
|
||||
cx.runtime.create_many_signals_with_map(cx, values, |x| x)
|
||||
}
|
||||
|
||||
/// Works exactly as [create_many_signals], but applies the map function to each signal pair.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_many_signals_mapped<T, U>(
|
||||
cx: Scope,
|
||||
values: impl IntoIterator<Item = T>,
|
||||
map_fn: impl Fn((ReadSignal<T>, WriteSignal<T>)) -> U + 'static,
|
||||
) -> Vec<U>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
cx.runtime.create_many_signals_with_map(cx, values, map_fn)
|
||||
}
|
||||
|
||||
/// Creates a signal that always contains the most recent value emitted by a
|
||||
/// [Stream](futures::stream::Stream).
|
||||
/// If the stream has not yet emitted a value since the signal was created, the signal's
|
||||
@@ -373,7 +417,7 @@ where
|
||||
/// Calling [WriteSignal::update] will mutate the signal’s value in place,
|
||||
/// and notify all subscribers that the signal’s value has changed.
|
||||
///
|
||||
/// `ReadSignal` implements [Fn], such that `set_value(new_value)` is equivalent to
|
||||
/// `WriteSignal` implements [Fn], such that `set_value(new_value)` is equivalent to
|
||||
/// `set_value.update(|value| *value = new_value)`.
|
||||
///
|
||||
/// `WriteSignal` is [Copy] and `'static`, so it can very easily moved into closures
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//#[cfg(not(feature = "stable"))]
|
||||
#[cfg(not(feature = "stable"))]
|
||||
use leptos_reactive::{
|
||||
create_isomorphic_effect, create_runtime, create_scope, create_signal,
|
||||
UntrackedGettableSignal, UntrackedSettableSignal,
|
||||
};
|
||||
|
||||
//#[cfg(not(feature = "stable"))]
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn untracked_set_doesnt_trigger_effect() {
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
@@ -36,6 +36,7 @@ fn untracked_set_doesnt_trigger_effect() {
|
||||
.dispose()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn untracked_get_doesnt_trigger_effect() {
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
@@ -13,6 +13,7 @@ leptos_dom = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
form_urlencoded = "1"
|
||||
gloo-net = "0.2"
|
||||
js-sys = "0.3"
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_urlencoded = "0.7"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[tasks.build-wasm]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features", "--target=wasm32-unknown-unknown"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -78,7 +78,6 @@
|
||||
//! can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request
|
||||
//! or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
|
||||
pub use form_urlencoded;
|
||||
use leptos_reactive::*;
|
||||
use proc_macro2::{Literal, TokenStream};
|
||||
use quote::TokenStreamExt;
|
||||
@@ -384,7 +383,7 @@ where
|
||||
T: serde::Serialize + serde::de::DeserializeOwned + Sized,
|
||||
{
|
||||
use ciborium::ser::into_writer;
|
||||
use leptos_dom::js_sys::Uint8Array;
|
||||
use js_sys::Uint8Array;
|
||||
use serde_json::Deserializer as JSONDeserializer;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.1.3"
|
||||
version = "0.2.0-alpha"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -12,6 +12,7 @@ cfg-if = "1"
|
||||
leptos = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.12"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@@ -45,13 +45,18 @@
|
||||
//! which mode your app is operating in.
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{leptos_dom::debug_warn, *};
|
||||
use leptos::{
|
||||
leptos_dom::{debug_warn, html::AnyElement},
|
||||
*,
|
||||
};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
rc::Rc,
|
||||
};
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
mod body;
|
||||
mod html;
|
||||
@@ -255,6 +260,8 @@ impl MetaContext {
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn dehydrate(&self) -> String {
|
||||
use leptos::leptos_dom::HydrationCtx;
|
||||
|
||||
let prev_key = HydrationCtx::peek();
|
||||
let mut tags = String::new();
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ pub fn Link(
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
|
||||
|
||||
let builder_el = leptos::link(cx)
|
||||
let builder_el = leptos::leptos_dom::html::link(cx)
|
||||
.attr("id", &id)
|
||||
.attr("as_", as_)
|
||||
.attr("crossorigin", crossorigin)
|
||||
|
||||
@@ -41,7 +41,7 @@ pub fn Meta(
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = format!("leptos-link-{}", next_id.0);
|
||||
|
||||
let builder_el = leptos::meta(cx)
|
||||
let builder_el = leptos::leptos_dom::html::meta(cx)
|
||||
.attr("charset", move || charset.as_ref().map(|v| v.get()))
|
||||
.attr("name", move || name.as_ref().map(|v| v.get()))
|
||||
.attr("http-equiv", move || http_equiv.as_ref().map(|v| v.get()))
|
||||
|
||||
@@ -67,7 +67,7 @@ pub fn Script(
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
|
||||
|
||||
let builder_el = leptos::script(cx)
|
||||
let builder_el = leptos::leptos_dom::html::script(cx)
|
||||
.attr("id", &id)
|
||||
.attr("async", async_)
|
||||
.attr("crossorigin", crossorigin)
|
||||
|
||||
@@ -46,7 +46,7 @@ pub fn Style(
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
|
||||
|
||||
let builder_el = leptos::style(cx)
|
||||
let builder_el = leptos::leptos_dom::html::style(cx)
|
||||
.attr("id", &id)
|
||||
.attr("media", media)
|
||||
.attr("nonce", nonce)
|
||||
|
||||
@@ -2,6 +2,8 @@ use crate::{use_head, TextProp};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
/// Contains the current state of the document's `<title>`.
|
||||
#[derive(Clone, Default)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.1.3"
|
||||
version = "0.2.0-alpha"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{use_navigate, use_resolved_path, ToHref};
|
||||
use leptos::*;
|
||||
use std::{error::Error, rc::Rc};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
type OnFormData = Rc<dyn Fn(&web_sys::FormData)>;
|
||||
@@ -58,7 +58,7 @@ where
|
||||
on_response: Option<OnResponse>,
|
||||
class: Option<Attribute>,
|
||||
children: Children,
|
||||
) -> HtmlElement<Form> {
|
||||
) -> HtmlElement<html::Form> {
|
||||
let action_version = version;
|
||||
let on_submit = move |ev: web_sys::SubmitEvent| {
|
||||
if ev.default_prevented() {
|
||||
|
||||
@@ -76,7 +76,7 @@ where
|
||||
replace: bool,
|
||||
class: Option<AttributeValue>,
|
||||
children: Children,
|
||||
) -> HtmlElement<A> {
|
||||
) -> HtmlElement<leptos::html::A> {
|
||||
let location = use_location(cx);
|
||||
let is_active = create_memo(cx, move |_| match href.get() {
|
||||
None => false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::use_route;
|
||||
use leptos::*;
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
/// Displays the child route nested in a parent route, allowing you to control exactly where
|
||||
@@ -37,5 +37,5 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
});
|
||||
|
||||
leptos::DynChild::new_with_id(id, move || outlet.get())
|
||||
leptos::leptos_dom::DynChild::new_with_id(id, move || outlet.get())
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
|
||||
ParamsMap, RouterContext,
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos::{leptos_dom::Transparent, *};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
|
||||
@@ -166,7 +166,7 @@ impl RouterContext {
|
||||
|
||||
// handle all click events on anchor tags
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
leptos_dom::window_event_listener("click", {
|
||||
leptos::window_event_listener("click", {
|
||||
let inner = Rc::clone(&inner);
|
||||
move |ev| inner.clone().handle_anchor_click(ev)
|
||||
});
|
||||
@@ -340,7 +340,8 @@ impl RouterContextInner {
|
||||
|
||||
// let browser handle this event if it leaves our domain
|
||||
// or our base path
|
||||
if url.origin != leptos_dom::location().origin().unwrap_or_default()
|
||||
if url.origin
|
||||
!= leptos_dom::helpers::location().origin().unwrap_or_default()
|
||||
|| (!self.base_path.is_empty()
|
||||
&& !path_name.is_empty()
|
||||
&& !path_name
|
||||
@@ -351,22 +352,24 @@ impl RouterContextInner {
|
||||
}
|
||||
|
||||
let to = path_name + &unescape(&url.search) + &unescape(&url.hash);
|
||||
let state = get_property(a.unchecked_ref(), "state").ok().and_then(
|
||||
|value| {
|
||||
if value == wasm_bindgen::JsValue::UNDEFINED {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
},
|
||||
);
|
||||
let state =
|
||||
leptos_dom::helpers::get_property(a.unchecked_ref(), "state")
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
if value == wasm_bindgen::JsValue::UNDEFINED {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
});
|
||||
|
||||
ev.prevent_default();
|
||||
|
||||
let replace = get_property(a.unchecked_ref(), "replace")
|
||||
.ok()
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false);
|
||||
let replace =
|
||||
leptos_dom::helpers::get_property(a.unchecked_ref(), "replace")
|
||||
.ok()
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false);
|
||||
if let Err(e) = self.navigate_from_route(
|
||||
&to,
|
||||
&NavigateOptions {
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
},
|
||||
RouteContext, RouterContext,
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
cmp::Reverse,
|
||||
@@ -203,8 +203,7 @@ pub fn Routes(
|
||||
})
|
||||
});
|
||||
|
||||
//HydrationCtx::continue_from(id_before);
|
||||
leptos::DynChild::new_with_id(id, move || root.get())
|
||||
leptos::leptos_dom::DynChild::new_with_id(id, move || root.get())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use leptos::*;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
mod location;
|
||||
mod params;
|
||||
@@ -35,7 +36,7 @@ pub struct BrowserIntegration {}
|
||||
|
||||
impl BrowserIntegration {
|
||||
fn current() -> LocationChange {
|
||||
let loc = leptos_dom::location();
|
||||
let loc = leptos_dom::helpers::location();
|
||||
LocationChange {
|
||||
value: loc.pathname().unwrap_or_default()
|
||||
+ &loc.search().unwrap_or_default()
|
||||
@@ -53,7 +54,7 @@ impl History for BrowserIntegration {
|
||||
|
||||
let (location, set_location) = create_signal(cx, Self::current());
|
||||
|
||||
leptos_dom::window_event_listener("popstate", move |_| {
|
||||
leptos::window_event_listener("popstate", move |_| {
|
||||
let router = use_context::<RouterContext>(cx);
|
||||
if let Some(router) = router {
|
||||
let change = Self::current();
|
||||
@@ -98,7 +99,7 @@ impl History for BrowserIntegration {
|
||||
.unwrap_throw();
|
||||
}
|
||||
// scroll to el
|
||||
if let Ok(hash) = leptos_dom::location().hash() {
|
||||
if let Ok(hash) = leptos_dom::helpers::location().hash() {
|
||||
if !hash.is_empty() {
|
||||
let hash = js_sys::decode_uri(&hash[1..])
|
||||
.ok()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::wasm_bindgen::JsValue;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct State(pub Option<JsValue>);
|
||||
|
||||
Reference in New Issue
Block a user