Compare commits

..

18 Commits

Author SHA1 Message Date
Greg Johnston
ed405c1f27 Fixes hydration errors for elements following a \<Suspense/\> 2023-02-16 19:22:05 -05:00
Greg Johnston
3e47a4b566 create_many_signals 2023-02-16 07:32:42 -05:00
Greg Johnston
0071a48b8a feature: reintroduce limited template-node cloning w/ template macro (#526) 2023-02-16 07:02:01 -05:00
Greg Johnston
8d42e91eb8 fix: top-level SVG in view macro with new exports (#525) 2023-02-15 15:38:06 -05:00
Greg Johnston
00a796d204 change: tweak API of Errors and implement IntoIter (#522) 2023-02-15 14:03:16 -05:00
henrik
bde585dc3e feature: enable cargo-leptos to reload multiple CSS files (#524) 2023-02-14 18:51:47 -05:00
Greg Johnston
0a534bd7fd Reexport web-sys event types in leptos::ev to make it easier to type handlers (#521) 2023-02-13 20:45:46 -05:00
Greg Johnston
50d8eae694 fix: correct namespace for Unit in empty views (closes #518) (#520) 2023-02-13 20:25:26 -05:00
martin frances
e732a4952b leptos_dom erros.rs remove<E>() does not need to be generic. (#516)
* leptos_dom erros.rs remove<E>() does not need to be generic.

* fixed up errors.remove().
2023-02-13 20:25:11 -05:00
Greg Johnston
8a99623fd6 0.2.0-alpha (#515) 2023-02-13 07:49:29 -05:00
Greg Johnston
7d6c4930e4 remove .unwrap() from redirect in Actix integration (#514) 2023-02-13 06:02:43 -05:00
IcosaHedron
81d6689cc0 do not unwrap use_context in integrations axum redirect (#513) 2023-02-12 21:59:12 -05:00
Greg Johnston
989b5b93c3 CI: fix Wasm testing (#511) 2023-02-12 19:39:32 -05:00
Greg Johnston
ca510f72c1 fix: SSR export in Wasm mode (#512) 2023-02-12 19:12:15 -05:00
Greg Johnston
6dd3be75d1 fix: import in leptos_dom and add Wasm build to CI for regressions (#510) 2023-02-12 18:58:57 -05:00
g-re-g
51e11e756a Typos and a small cleanup (#509) 2023-02-12 18:11:31 -05:00
Greg Johnston
1dbcfe2861 change: reorganize module exports and reexports (#503) 2023-02-12 17:04:36 -05:00
Greg Johnston
db3f46c501 Add docs on testing (closes #489) (#508) 2023-02-12 17:03:12 -05:00
74 changed files with 1497 additions and 1760 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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, theres no particular logic to test, but
for many its 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, heres 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);
```
Im 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/> },
);
```
Well 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 elements
`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` thats 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: Ill just test
the wrappers `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 its 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).

View File

@@ -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())
);
}

View File

@@ -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 = []

View File

@@ -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")
}),
);

View File

@@ -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")),
))
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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();

View File

@@ -9,6 +9,12 @@
max-width: 250px;
height: auto;
}
.error {
border: 1px solid red;
color: red;
background-color: lightpink;
}
</style>
<body></body>
</html>

View File

@@ -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>
}
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -29,6 +29,7 @@ sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
wasm-bindgen = "0.2"
[features]
default = ["ssr"]

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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.');

View File

@@ -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
View 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",
]

View File

@@ -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),
}
}
}

View File

@@ -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::*;
///

View File

@@ -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
}
}

View File

@@ -1,4 +0,0 @@
[tasks.build-wasm]
command = "cargo"
args = ["+nightly", "build-all-features", "--target=wasm32-unknown-unknown"]
install_crate = "cargo-all-features"

View File

@@ -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())
}
}

View File

@@ -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,

View File

@@ -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,
};

View File

@@ -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};

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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,

View File

@@ -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)
/// macros use. You usually won't need to interact with it directly.
/// macros 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,

View File

@@ -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)
/// macros use. You usually won't need to interact with it directly.
/// macros 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,

View File

@@ -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)
/// macros use. You usually won't need to interact with it directly.
/// macros 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),

View File

@@ -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};
}
}

View File

@@ -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))
}

View File

@@ -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};

View File

@@ -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"))]

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
})
}

View 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)
}

View File

@@ -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)
}

View File

@@ -1,4 +0,0 @@
[tasks.build-wasm]
command = "cargo"
args = ["+nightly", "build-all-features", "--target=wasm32-unknown-unknown"]
install_crate = "cargo-all-features"

View File

@@ -67,7 +67,7 @@
//! ```
#[cfg_attr(debug_assertions, macro_use)]
pub extern crate tracing;
extern crate tracing;
mod context;
mod effect;

View File

@@ -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,
{
}

View File

@@ -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,

View File

@@ -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 signals value in place,
/// and notify all subscribers that the signals 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

View File

@@ -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};

View File

@@ -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"

View File

@@ -1,4 +0,0 @@
[tasks.build-wasm]
command = "cargo"
args = ["+nightly", "build-all-features", "--target=wasm32-unknown-unknown"]
install_crate = "cargo-all-features"

View File

@@ -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)]

View File

@@ -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"

View File

@@ -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();

View File

@@ -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)

View File

@@ -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()))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)]

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.1.3"
version = "0.2.0-alpha"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -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() {

View File

@@ -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,

View File

@@ -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())
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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)]

View File

@@ -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()

View File

@@ -1,4 +1,4 @@
use leptos::wasm_bindgen::JsValue;
use wasm_bindgen::JsValue;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct State(pub Option<JsValue>);