Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
fdf6ebaeaf fix: typed route params with #[derive(Params)] 2023-02-07 13:13:51 -05:00
128 changed files with 5679 additions and 5941 deletions

View File

@@ -30,9 +30,6 @@ 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
@@ -46,3 +43,4 @@ 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.2.0-alpha"
version = "0.1.3"
[workspace.dependencies]
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" }
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" }
[profile.release]
codegen-units = 1

View File

@@ -12,17 +12,13 @@ dependencies = ["build", "check-examples", "test"]
[tasks.build]
clear = true
dependencies = ["build-all", "build-wasm"]
dependencies = ["build-all"]
[tasks.build-all]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.build-wasm]
clear = true
dependencies = [{ name = "build-wasm", path = "leptos" }]
[tasks.check-examples]
clear = true
dependencies = [

View File

@@ -78,7 +78,7 @@ rustup target add wasm32-unknown-unknown
If youre on `stable`, note the following:
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0", features = ["stable"] }`
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0-alpha", features = ["stable"] }`
2. `nightly` enables the function call syntax for accessing and setting signals. If youre using `stable`,
youll just call `.get()`, `.set()`, or `.update()` manually. Check out the
[`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)

View File

@@ -1 +0,0 @@
book

View File

@@ -11,9 +11,9 @@
- [Control Flow](./view/06_control_flow.md)
- [Error Handling](./view/07_errors.md)
- [Parent-Child Communication](./view/08_parent_child.md)
- [Passing Children to Components](./view/09_component_children.md)
- [Interlude: Reactivity and Functions](./interlude_functions.md)
- [Testing](./testing.md)
- [Passing Children to Components]()
- [Interlude: Reactivity and Functions]()
- [Testing]()
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
- [Async]()
- [Resource]()

View File

@@ -1,76 +0,0 @@
# Interlude: Reactivity and Functions
One of our core contributors said to me recently: “I never used closures this often
until I started using Leptos.” And its true. Closures are at the heart of any Leptos
application. It sometimes looks a little silly:
```rust
// a signal holds a value, and can be updated
let (count, set_count) = create_signal(cx, 0);
// a derived signal is a function that accesses other signals
let double_count = move || count() * 2;
let count_is_odd = move || count() & 1 == 1;
let text = move || if count_is_odd() {
"odd"
} else {
"even"
};
// an effect automatically tracks the signals it depends on
// and re-runs when they change
create_effect(cx, move |_| {
log!("text = {}", text());
});
view! { cx,
<p>{move || text().to_uppercase()}</p>
}
```
Closures, closures everywhere!
But why?
## Functions and UI Frameworks
Functions are at the heart of every UI framework. And this makes perfect sense. Creating a user interface is basically divided into two phases:
1. initial rendering
2. updates
In a web framework, the framework does some kind of initial rendering. Then it hands control back over to the browser. When certain events fire (like a mouse click) or asynchronous tasks finish (like an HTTP request finishing), the browser wakes the framework back up to update something. The framework runs some kind of code to update your user interface, and goes back asleep until the browser wakes it up again.
The key phrase here is “runs some kind of code.” The natural way to “run some kind of code” at an arbitrary point in time—in Rust or in any other programming language—is to call a function. And in fact every UI framework is based on rerunning some kind of function over and over:
1. virtual DOM (VDOM) frameworks like React, Yew, or Dioxus rerun a component or render function over and over, to generate a virtual DOM tree that can be reconciled with the previous result to patch the DOM
2. compiled frameworks like Angular and Svelte divide your component templates into “create” and “update” functions, rerunning the update function when they detect a change to the components state
3. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, _you_ define the functions that re-run
Thats what all our components are doing.
Take our typical `<SimpleCounter/>` example in its simplest form:
```rust
#[component]
pub fn SimpleCounter(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, 0);
let increment = move |_| set_value.update(|value| *value += 1);
view! { cx,
<button on:click=increment>
{value}
</button>
}
}
```
The `SimpleCounter` function itself runs once. The `value` signal is created once. The framework hands off the `increment` function to the browser as an event listener. When you click the button, the browser calls `increment`, which updates `value` via `set_value`. And that updates the single text node represented in our view by `{value}`.
Closures are key to reactivity. They provide the framework with the ability to re-run the smallest possible unit of your application in responsive to a change.
So remember two things:
1. Your component function is a setup function, not a render function: it only runs once.
2. For values in your view template to be reactive, they must be functions: either signals (which implement the `Fn` traits) or closures.

View File

@@ -1,180 +0,0 @@
# 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

@@ -187,8 +187,6 @@ This rerenders `<Small/>` five times, then `<Big/>` infinitely. If theyre
loading resources, creating signals, or even just creating DOM nodes, this is
unnecessary work.
### `<Show/>`
The [`<Show/>`](https://docs.rs/leptos/latest/leptos/fn.Show.html) component is
the answer. You pass it a `when` condition function, a `fallback` to be shown if
the `when` function returns `false`, and children to be rendered if `when` is `true`.

View File

@@ -1,124 +0,0 @@
# Component Children
Its pretty common to want to pass children into a component, just as you can pass
children into an HTML element. For example, imagine I have a `<FancyForm/>` component
that enhances an HTML `<form>`. I need some way to pass all its inputs.
```rust
view! { cx,
<Form>
<fieldset>
<label>
"Some Input"
<input type="text" name="something"/>
</label>
</fieldset>
<button>"Submit"</button>
</Form>
}
```
How can you do this in Leptos? There are basically two ways to pass components to
other components:
1. **render props**: properties that are functions that return a view
2. the **`children`** prop: a special component property that includes anything
you pass as a child to the component.
In fact, youve already seen these both in action in the [`<Show/>`](/view/06_control_flow.html#show) component:
```rust
view! { cx,
<Show
// `when` is a normal prop
when=move || value() > 5
// `fallback` is a "render prop": a function that returns a view
fallback=|cx| view! { cx, <Small/> }
>
// `<Big/>` (and anything else here)
// will be given to the `children` prop
<Big/>
</Show>
}
```
Lets define a component that takes some children and a render prop.
```rust
#[component]
pub fn TakesChildren<F, IV>(
cx: Scope,
/// Takes a function (type F) that returns anything that can be
/// converted into a View (type IV)
render_prop: F,
/// `children` takes the `Children` type
children: Children,
) -> impl IntoView
where
F: Fn() -> IV,
IV: IntoView,
{
view! { cx,
<h2>"Render Prop"</h2>
{render_prop()}
<h2>"Children"</h2>
{children(cx)}
}
}
```
`render_prop` and `children` are both functions, so we can call them to generate
the appropriate views. `children`, in particular, is an alias for
`Box<dyn FnOnce(Scope) -> Fragment>`. (Aren't you glad we named it `Children` instead?)
> If you need a `Fn` or `FnMut` here because you need to call `children` more than once,
> we also provide `ChildrenFn` and `ChildrenMut` aliases.
We can use the component like this:
```rust
view! { cx,
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
// these get passed to `children`
"Some text"
<span>"A span"</span>
</TakesChildren>
}
```
## Manipulating Children
The [`Fragment`](https://docs.rs/leptos/latest/leptos/struct.Fragment.html) type is
basically a way of wrapping a `Vec<View>`. You can insert it anywhere into your view.
But you can also access those inner views directly to manipulate them. For example, heres
a component that takes its children and turns them into an unordered list.
```rust
#[component]
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
// Fragment has `nodes` field that contains a Vec<View>
let children = children(cx)
.nodes
.into_iter()
.map(|child| view! { cx, <li>{child}</li> })
.collect::<Vec<_>>();
view! { cx,
<ul>{children}</ul>
}
}
```
Calling it like this will create a list:
```rust
view! { cx,
<WrappedChildren>
"A"
"B"
"C"
</WrappedChildren>
}
```

View File

@@ -1,86 +1,16 @@
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counter::*;
use leptos::*;
use web_sys::HtmlElement;
#[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()
});
}
use counter::*;
#[wasm_bindgen_test]
fn inc() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
document.body().unwrap().append_child(&test_wrapper);
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> });
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 div = document.query_selector("div").unwrap().unwrap();
let clear = div
.first_child()
.unwrap()
@@ -117,40 +47,4 @@ 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,7 +25,6 @@ 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("couldn't connect to SSE stream");
.expect_throw("couldn't connect to SSE stream");
let s = create_signal_from_stream(
cx,
source.subscribe("message").unwrap().map(|value| {
value
.expect("no message event")
.expect_throw("no message event")
.1
.data()
.as_string()
.expect("expected string value")
.expect_throw("expected string value")
}),
);

View File

@@ -1,4 +1,4 @@
use leptos::{ev, html::*, *};
use leptos::{ev, *};
pub struct Props {
/// The starting value for the counter
@@ -25,9 +25,7 @@ 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((
@@ -40,9 +38,7 @@ 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,4 +1,5 @@
use leptos::{For, ForProps, *};
use leptos::*;
use leptos::{For, ForProps};
const MANY_COUNTERS: usize = 1000;
@@ -65,8 +66,9 @@ pub fn Counters(cx: Scope) -> impl IntoView {
<For
each=counters
key=|counter| counter.0
view=move |cx, (id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! { cx,
view=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! {
cx,
<Counter id value set_value/>
}
}
@@ -83,11 +85,9 @@ fn Counter(
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context(cx).unwrap();
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
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

@@ -72,7 +72,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |cx, (id, (value, set_value))| {
view=move |(id, (value, set_value))| {
view! {
cx,
<Counter id value set_value/>
@@ -91,12 +91,9 @@ fn Counter(
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context(cx).unwrap();
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
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

@@ -1,10 +0,0 @@
[package]
name = "error_boundary"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -1,9 +0,0 @@
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -1,7 +0,0 @@
# Leptos `<ErrorBoundary/>` Example
This example shows how to handle basic errors using Leptos.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,48 +0,0 @@
use leptos::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
// when input changes, try to parse a number from the input
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input type="number" on:input=on_input/>
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
// the fallback will be displayed. Otherwise, the children of the
// <ErrorBoundary/> will be displayed.
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors
// as strings, if we'd like
<ul>
{move || errors.get()
.0
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
}
</ul>
</div>
}
>
<p>
"You entered "
// because `value` is `Result<i32, _>`,
// it will render the `i32` if it is `Ok`,
// and render nothing and trigger the error boundary
// if it is `Err`. It's a signal, so this will dynamically
// update when `value` changes
<strong>{value}</strong>
</p>
</ErrorBoundary>
</label>
}
}

View File

@@ -1,12 +0,0 @@
use error_boundary::*;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx,
<App/>
}
})
}

View File

@@ -32,49 +32,45 @@ 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

@@ -49,7 +49,7 @@ pub fn ErrorTemplate(
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
view=move |cx, error| {
view= move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! { cx,

View File

@@ -91,7 +91,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
<For
each=move || stories.clone()
key=|story| story.id
view=move |cx, story: api::Story| {
view=move |story: api::Story| {
view! { cx,
<Story story/>
}

View File

@@ -53,7 +53,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move |cx, comment| view! { cx, <Comment comment /> }
view=move |comment| view! { cx, <Comment comment /> }
/>
</ul>
</div>
@@ -98,7 +98,7 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
<For
each=move || comments.clone()
key=|comment| comment.id
view=move |cx, comment: api::Comment| view! { cx, <Comment comment /> }
view=move |comment: api::Comment| view! { cx, <Comment comment /> }
/>
</ul>
}

View File

@@ -15,7 +15,7 @@ pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
// a unique key for each item as a reference
key=|error| error.0.clone()
// renders each item to a view
view= move |cx, error| {
view= move |error| {
let error_string = error.1.to_string();
view! {
cx,

View File

@@ -91,7 +91,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
<For
each=move || stories.clone()
key=|story| story.id
view=move |cx, story: api::Story| {
view=move |story: api::Story| {
view! { cx,
<Story story/>
}

View File

@@ -53,7 +53,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move |cx, comment| view! { cx, <Comment comment /> }
view=move |comment| view! { cx, <Comment comment /> }
/>
</ul>
</div>
@@ -98,7 +98,7 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
<For
each=move || comments.clone()
key=|comment| comment.id
view=move |cx, comment: api::Comment| view! { cx, <Comment comment /> }
view=move |comment: api::Comment| view! { cx, <Comment comment /> }
/>
</ul>
}

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

@@ -1,18 +1,14 @@
mod api;
use crate::api::*;
use leptos::*;
use leptos_router::*;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct ExampleContext(i32);
use crate::api::{get_contact, get_contacts};
#[component]
pub fn RouterExample(cx: Scope) -> impl IntoView {
log::debug!("rendering <RouterExample/>");
// contexts are passed down through the route tree
provide_context(cx, ExampleContext(0));
view! { cx,
<Router>
<nav>
@@ -63,13 +59,6 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
pub fn ContactList(cx: Scope) -> impl IntoView {
log::debug!("rendering <ContactList/>");
// contexts are passed down through the route tree
provide_context(cx, ExampleContext(42));
on_cleanup(cx, || {
log!("cleaning up <ContactList/>");
});
let location = use_location(cx);
let contacts = create_resource(cx, move || location.search.get(), get_contacts);
let contacts = move || {
@@ -106,15 +95,6 @@ pub struct ContactParams {
pub fn Contact(cx: Scope) -> impl IntoView {
log::debug!("rendering <Contact/>");
log::debug!(
"ExampleContext should be Some(42). It is {:?}",
use_context::<ExampleContext>(cx)
);
on_cleanup(cx, || {
log!("cleaning up <Contact/>");
});
let params = use_params::<ContactParams>(cx);
let contact = create_resource(
cx,
@@ -156,16 +136,6 @@ pub fn Contact(cx: Scope) -> impl IntoView {
#[component]
pub fn About(cx: Scope) -> impl IntoView {
log::debug!("rendering <About/>");
on_cleanup(cx, || {
log!("cleaning up <About/>");
});
log::debug!(
"ExampleContext should be Some(0). It is {:?}",
use_context::<ExampleContext>(cx)
);
// use_navigate allows you to navigate programmatically by calling a function
let navigate = use_navigate(cx);
@@ -187,11 +157,6 @@ pub fn About(cx: Scope) -> impl IntoView {
#[component]
pub fn Settings(cx: Scope) -> impl IntoView {
log::debug!("rendering <Settings/>");
on_cleanup(cx, || {
log!("cleaning up <Settings/>");
});
view! { cx,
<>
<h1>"Settings"</h1>

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Cargo Leptos</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- INJECT HEAD -->
</head>
<body>
<!-- INJECT BODY -->
</body>
</html>

View File

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

View File

@@ -36,26 +36,22 @@ 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

@@ -51,7 +51,7 @@ pub fn ErrorTemplate(
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
view= move |cx, error| {
view= move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {

View File

@@ -1,4 +1,4 @@
use leptos::{html::Input, leptos_dom::helpers::location_hash, *};
use leptos::{web_sys::HtmlInputElement, *};
use storage::TodoSerialized;
use uuid::Uuid;
@@ -12,15 +12,12 @@ const STORAGE_KEY: &str = "todos-leptos";
// Basic operations to manipulate the todo list: nothing really interesting here
impl Todos {
pub fn new(cx: Scope) -> Self {
let starting_todos = if let Ok(Some(storage)) = window().local_storage()
{
let starting_todos = if let Ok(Some(storage)) = window().local_storage() {
storage
.get_item(STORAGE_KEY)
.ok()
.flatten()
.and_then(|value| {
serde_json::from_str::<Vec<TodoSerialized>>(&value).ok()
})
.and_then(|value| serde_json::from_str::<Vec<TodoSerialized>>(&value).ok())
.map(|values| {
values
.into_iter()
@@ -92,12 +89,7 @@ impl Todo {
Self::new_with_completed(cx, id, title, false)
}
pub fn new_with_completed(
cx: Scope,
id: Uuid,
title: String,
completed: bool,
) -> Self {
pub fn new_with_completed(cx: Scope, id: Uuid, title: String, completed: bool) -> Self {
// RwSignal combines the getter and setter in one struct, rather than separating
// the getter from the setter. This makes it more convenient in some cases, such
// as when we're putting the signals into a struct and passing it around. There's
@@ -137,24 +129,22 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
// Handle the three filter modes: All, Active, and Completed
let (mode, set_mode) = create_signal(cx, Mode::All);
window_event_listener("hashchange", move |_| {
let new_mode =
location_hash().map(|hash| route(&hash)).unwrap_or_default();
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);
});
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
let input_ref = NodeRef::<Input>::new(cx);
let add_todo = move |ev: web_sys::KeyboardEvent| {
let input = input_ref.get().unwrap();
let target = event_target::<HtmlInputElement>(&ev);
ev.stop_propagation();
let key_code = ev.key_code();
if key_code == ENTER_KEY {
let title = input.value();
let title = event_target_value(&ev);
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));
input.set_value("");
target.set_value("");
}
}
};
@@ -194,8 +184,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
.iter()
.map(TodoSerialized::from)
.collect::<Vec<_>>();
let json =
serde_json::to_string(&objs).expect("couldn't serialize Todos");
let json = serde_json::to_string(&objs).expect("couldn't serialize Todos");
if storage.set_item(STORAGE_KEY, &json).is_err() {
log::error!("error while trying to set item in localStorage");
}
@@ -212,7 +201,6 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
placeholder="What needs to be done?"
autofocus
on:keydown=add_todo
node_ref=input_ref
/>
</header>
<section
@@ -228,7 +216,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
<For
each=filtered_todos
key=|todo| todo.id
view=move |cx, todo: Todo| view! { cx, <Todo todo /> }
view=move |todo: Todo| view! { cx, <Todo todo /> }
/>
</ul>
</section>
@@ -274,7 +262,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
// this will be filled by _ref=input below
let todo_input = NodeRef::<Input>::new(cx);
let todo_input = NodeRef::<HtmlElement<Input>>::new(cx);
let save = move |value: &str| {
let value = value.trim();

View File

@@ -15,11 +15,7 @@ use actix_web::{
};
use futures::{Future, StreamExt};
use http::StatusCode;
use leptos::{
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
leptos_server::{server_fn_by_path, Payload},
*,
};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
@@ -36,19 +32,11 @@ pub struct ResponseParts {
impl ResponseParts {
/// Insert a header, overwriting any previous value with the same key
pub fn insert_header(
&mut self,
key: header::HeaderName,
value: header::HeaderValue,
) {
pub fn insert_header(&mut self, key: header::HeaderName, value: header::HeaderValue) {
self.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub fn append_header(
&mut self,
key: header::HeaderName,
value: header::HeaderValue,
) {
pub fn append_header(&mut self, key: header::HeaderName, value: header::HeaderValue) {
self.headers.append(key, value);
}
}
@@ -72,21 +60,13 @@ impl ResponseOptions {
res_parts.status = Some(status);
}
/// Insert a header, overwriting any previous value with the same key
pub fn insert_header(
&self,
key: header::HeaderName,
value: header::HeaderValue,
) {
pub fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub fn append_header(
&self,
key: header::HeaderName,
value: header::HeaderValue,
) {
pub fn append_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
@@ -97,14 +77,12 @@ 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) {
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"),
);
}
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"),
);
}
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
@@ -195,8 +173,7 @@ pub fn handle_server_fns_with_context(
match server_fn(cx, body).await {
Ok(serialized) => {
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
let res_options = use_context::<ResponseOptions>(cx).unwrap();
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
@@ -206,8 +183,7 @@ pub fn handle_server_fns_with_context(
let mut res_parts = res_options.0.write();
if accept_header == Some("application/json")
|| accept_header
== Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = HttpResponse::Ok();
@@ -245,9 +221,7 @@ pub fn handle_server_fns_with_context(
res.body(Bytes::from(data))
}
Payload::Url(data) => {
res.content_type(
"application/x-www-form-urlencoded",
);
res.content_type("application/x-www-form-urlencoded");
res.body(data)
}
Payload::Json(data) => {
@@ -256,15 +230,13 @@ pub fn handle_server_fns_with_context(
}
}
}
Err(e) => HttpResponse::InternalServerError()
.body(e.to_string()),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
}
} else {
HttpResponse::BadRequest().body(format!(
"Could not find a server function at the route {:?}. \
\n\nIt's likely that you need to call \
ServerFn::register() on the server function type, \
somewhere in your `main` function.",
\n\nIt's likely that you need to call ServerFn::register() on the \
server function type, somewhere in your `main` function.",
req.path()
))
}
@@ -284,13 +256,13 @@ pub fn handle_server_fns_with_context(
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
/// use actix_web::{HttpServer, App};
/// use leptos::*;
/// use std::{env, net::SocketAddr};
/// use std::{env,net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
@@ -300,17 +272,11 @@ pub fn handle_server_fns_with_context(
/// let addr = conf.leptos_options.site_addr.clone();
/// HttpServer::new(move || {
/// let leptos_options = &conf.leptos_options;
///
///
/// App::new()
/// // {tail:.*} passes the remainder of the URL as the route
/// // the actual routing will be handled by `leptos_router`
/// .route(
/// "/{tail:.*}",
/// leptos_actix::render_app_to_stream(
/// leptos_options.to_owned(),
/// |cx| view! { cx, <MyApp/> },
/// ),
/// )
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream(leptos_options.to_owned(), |cx| view! { cx, <MyApp/> }))
/// })
/// .bind(&addr)?
/// .run()
@@ -387,14 +353,14 @@ where
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
/// use actix_web::{HttpServer, App};
/// use leptos::*;
/// use std::{env,net::SocketAddr};
/// use leptos_actix::DataResponse;
/// use std::{env, net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope, data: &'static str) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
@@ -404,21 +370,14 @@ where
/// let addr = conf.leptos_options.site_addr.clone();
/// HttpServer::new(move || {
/// let leptos_options = &conf.leptos_options;
///
///
/// App::new()
/// // {tail:.*} passes the remainder of the URL as the route
/// // the actual routing will be handled by `leptos_router`
/// .route(
/// "/{tail:.*}",
/// leptos_actix::render_preloaded_data_app(
/// leptos_options.to_owned(),
/// |req| async move {
/// Ok(DataResponse::Data(
/// "async func that can preload data",
/// ))
/// },
/// |cx, data| view! { cx, <MyApp data/> },
/// ),
/// .route("/{tail:.*}", leptos_actix::render_preloaded_data_app(
/// leptos_options.to_owned(),
/// |req| async move { Ok(DataResponse::Data("async func that can preload data")) },
/// |cx, data| view! { cx, <MyApp data/> })
/// )
/// })
/// .bind(&addr)?
@@ -471,11 +430,7 @@ where
})
}
fn provide_contexts(
cx: leptos::Scope,
req: &HttpRequest,
res_options: ResponseOptions,
) {
fn provide_contexts(cx: leptos::Scope, req: &HttpRequest, res_options: ResponseOptions) {
let path = leptos_corrected_path(req);
let integration = ServerIntegration { path };
@@ -502,27 +457,25 @@ async fn stream_app(
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) =
render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| {
let meta = use_context::<MetaContext>(cx);
let head = meta
.as_ref()
.map(|meta| meta.dehydrate())
.unwrap_or_default();
let body_meta = meta
.as_ref()
.and_then(|meta| meta.body.as_string())
.unwrap_or_default();
format!("{head}</head><body{body_meta}>").into()
},
additional_context,
);
let (stream, runtime, scope) = render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| {
let meta = use_context::<MetaContext>(cx);
let head = meta
.as_ref()
.map(|meta| meta.dehydrate())
.unwrap_or_default();
let body_meta = meta
.as_ref()
.and_then(|meta| meta.body.as_string())
.unwrap_or_default();
format!("{head}</head><body{body_meta}>").into()
},
additional_context,
);
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) =
html_parts(options, use_context::<MetaContext>(cx).as_ref());
let (head, tail) = html_parts(options, use_context::<MetaContext>(cx).as_ref());
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })
@@ -540,13 +493,11 @@ async fn stream_app(
let res_options = res_options.0.read();
let (status, mut headers) =
(res_options.status, res_options.headers.clone());
let (status, mut headers) = (res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
let complete_stream =
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
.chain(stream);
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()]).chain(stream);
let mut res = HttpResponse::Ok()
.content_type("text/html")
.streaming(complete_stream);
@@ -563,10 +514,7 @@ async fn stream_app(
res
}
fn html_parts(
options: &LeptosOptions,
meta_context: Option<&MetaContext>,
) -> (String, String) {
fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (String, String) {
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
@@ -630,9 +578,7 @@ fn html_parts(
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list<IV>(
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
) -> Vec<String>
pub fn generate_route_list<IV>(app_fn: impl FnOnce(leptos::Scope) -> IV + 'static) -> Vec<String>
where
IV: IntoView + 'static,
{
@@ -712,12 +658,7 @@ pub trait LeptosRoutes {
/// to those paths to Leptos's renderer.
impl<T> LeptosRoutes for actix_web::App<T>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Error = Error,
InitError = (),
>,
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
{
fn leptos_routes<IV>(
self,
@@ -730,10 +671,7 @@ where
{
let mut router = self;
for path in paths.iter() {
router = router.route(
path,
render_app_to_stream(options.clone(), app_fn.clone()),
);
router = router.route(path, render_app_to_stream(options.clone(), app_fn.clone()));
}
router
}
@@ -755,11 +693,7 @@ where
for path in paths.iter() {
router = router.route(
path,
render_preloaded_data_app(
options.clone(),
data_fn.clone(),
app_fn.clone(),
),
render_preloaded_data_app(options.clone(), data_fn.clone(), app_fn.clone()),
);
}
router

View File

@@ -9,25 +9,19 @@
use axum::{
body::{Body, Bytes, Full, StreamBody},
extract::Path,
http::{
header::{HeaderName, HeaderValue},
HeaderMap, Request, StatusCode,
},
http::{header::HeaderName, header::HeaderValue, HeaderMap, Request, StatusCode},
response::IntoResponse,
routing::get,
};
use futures::{Future, SinkExt, Stream, StreamExt};
use http::{header, method::Method, uri::Uri, version::Version, Response};
use hyper::body;
use leptos::{
leptos_server::{server_fn_by_path, Payload},
*,
};
use leptos::*;
use leptos_meta::MetaContext;
use leptos_router::*;
use parking_lot::RwLock;
use std::{io, pin::Pin, sync::Arc};
use tokio::task::{spawn_blocking, LocalSet};
use tokio::{task::spawn_blocking, task::LocalSet};
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
/// to construct this for Leptos to use in Axum
@@ -94,14 +88,12 @@ 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) {
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"),
);
}
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"),
);
}
/// Decomposes an HTTP request into its parts, allowing you to read its headers
@@ -126,8 +118,8 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
///
/// ```
/// use axum::{handler::Handler, routing::post, Router};
/// use leptos::*;
/// use std::net::SocketAddr;
/// use leptos::*;
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[tokio::main]
@@ -136,7 +128,7 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
///
/// // build our application with a route
/// let app = Router::new()
/// .route("/api/*fn_name", post(leptos_axum::handle_server_fns));
/// .route("/api/*fn_name", post(leptos_axum::handle_server_fns));
///
/// // run our app with hyper
/// // `axum::Server` is a re-export of `hyper::Server`
@@ -204,12 +196,9 @@ async fn handle_server_fns_inner(
.expect("couldn't spawn runtime")
.block_on({
async move {
let res = if let Some(server_fn) =
server_fn_by_path(fn_name.as_str())
{
let res = if let Some(server_fn) = server_fn_by_path(fn_name.as_str()) {
let runtime = create_runtime();
let (cx, disposer) =
raw_scope_and_disposer(runtime);
let (cx, disposer) = raw_scope_and_disposer(runtime);
additional_context(cx);
@@ -222,43 +211,34 @@ async fn handle_server_fns_inner(
match server_fn(cx, &req_parts.body).await {
Ok(serialized) => {
// If ResponseOptions are set, add the headers and status to the request
let res_options =
use_context::<ResponseOptions>(cx);
let res_options = use_context::<ResponseOptions>(cx);
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
// if this is Accept: application/json then send a serialized JSON response
let accept_header = headers
.get("Accept")
.and_then(|value| value.to_str().ok());
let accept_header =
headers.get("Accept").and_then(|value| value.to_str().ok());
let mut res = Response::builder();
// Add headers from ResponseParts if they exist. These should be added as long
// as the server function returns an OK response
let res_options_outer =
res_options.unwrap().0;
let res_options_inner =
res_options_outer.read();
let res_options_outer = res_options.unwrap().0;
let res_options_inner = res_options_outer.read();
let (status, mut res_headers) = (
res_options_inner.status,
res_options_inner.headers.clone(),
);
if let Some(header_ref) = res.headers_mut()
{
header_ref.extend(res_headers.drain());
if let Some(header_ref) = res.headers_mut() {
header_ref.extend(res_headers.drain());
};
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header
== Some("application/cbor")
== Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
@@ -266,9 +246,7 @@ async fn handle_server_fns_inner(
else {
let referer = headers
.get("Referer")
.and_then(|value| {
value.to_str().ok()
})
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
res = res
@@ -282,23 +260,16 @@ async fn handle_server_fns_inner(
};
match serialized {
Payload::Binary(data) => res
.header(
"Content-Type",
"application/cbor",
)
.header("Content-Type", "application/cbor")
.body(Full::from(data)),
Payload::Url(data) => res
.header(
"Content-Type",
"application/\
x-www-form-urlencoded",
"application/x-www-form-urlencoded",
)
.body(Full::from(data)),
Payload::Json(data) => res
.header(
"Content-Type",
"application/json",
)
.header("Content-Type", "application/json")
.body(Full::from(data)),
}
}
@@ -309,13 +280,11 @@ async fn handle_server_fns_inner(
} else {
Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Full::from(format!(
"Could not find a server function at the \
route {fn_name}. \n\nIt's likely that \
you need to call ServerFn::register() on \
the server function type, somewhere in \
your `main` function."
)))
.body(Full::from(
format!("Could not find a server function at the route {fn_name}. \
\n\nIt's likely that you need to call ServerFn::register() on the \
server function type, somewhere in your `main` function." )
))
}
.expect("could not build Response");
@@ -328,8 +297,7 @@ async fn handle_server_fns_inner(
rx.await.unwrap()
}
pub type PinnedHtmlStream =
Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
@@ -342,28 +310,28 @@ pub type PinnedHtmlStream =
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::{handler::Handler, Router};
/// use axum::handler::Handler;
/// use axum::Router;
/// use std::{net::SocketAddr, env};
/// use leptos::*;
/// use leptos_config::get_configuration;
/// use std::{env, net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[tokio::main]
/// async fn main() {
///
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
/// let leptos_options = conf.leptos_options;
/// let addr = leptos_options.site_addr.clone();
///
///
/// // build our application with a route
/// let app = Router::new().fallback(leptos_axum::render_app_to_stream(
/// leptos_options,
/// |cx| view! { cx, <MyApp/> },
/// ));
/// let app = Router::new()
/// .fallback(leptos_axum::render_app_to_stream(leptos_options, |cx| view! { cx, <MyApp/> }));
///
/// // run our app with hyper
/// // `axum::Server` is a re-export of `hyper::Server`
@@ -386,13 +354,8 @@ pub fn render_app_to_stream<IV>(
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<
Box<
dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>>
+ Send
+ 'static,
>,
> + Clone
) -> Pin<Box<dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
@@ -432,13 +395,8 @@ pub fn render_app_to_stream_with_context<IV>(
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<
Box<
dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>>
+ Send
+ 'static,
>,
> + Clone
) -> Pin<Box<dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
@@ -495,7 +453,7 @@ where
};
let (bundle, runtime, scope) =
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
render_to_stream_with_prefix_undisposed_with_context(
app,
|cx| {
let head = use_context::<MetaContext>(cx)
@@ -544,16 +502,13 @@ where
// Extract the resources now that they've been rendered
let res_options = res_options3.0.read();
let complete_stream = futures::stream::iter([
first_chunk.unwrap(),
second_chunk.unwrap(),
])
.chain(stream);
let complete_stream =
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
.chain(stream);
let mut res = Response::new(StreamBody::new(Box::pin(
complete_stream,
)
as PinnedHtmlStream));
let mut res = Response::new(StreamBody::new(
Box::pin(complete_stream) as PinnedHtmlStream
));
if let Some(status) = res_options.status {
*res.status_mut() = status
@@ -567,10 +522,7 @@ where
}
}
fn html_parts(
options: &LeptosOptions,
meta: Option<&MetaContext>,
) -> (String, &'static str) {
fn html_parts(options: &LeptosOptions, meta: Option<&MetaContext>) -> (String, &'static str) {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
@@ -612,8 +564,7 @@ fn html_parts(
false => "".to_string(),
};
let html_metadata =
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
let html_metadata = meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
let head = format!(
r#"<!DOCTYPE html>
<html{html_metadata}>
@@ -633,9 +584,7 @@ fn html_parts(
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> Vec<String>
pub async fn generate_route_list<IV>(app_fn: impl FnOnce(Scope) -> IV + 'static) -> Vec<String>
where
IV: IntoView + 'static,
{
@@ -696,11 +645,7 @@ pub trait LeptosRoutes {
where
IV: IntoView + 'static;
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<String>,
handler: H,
) -> Self
fn leptos_routes_with_handler<H, T>(self, paths: Vec<String>, handler: H) -> Self
where
H: axum::handler::Handler<T, (), axum::body::Body>,
T: 'static;
@@ -751,11 +696,7 @@ impl LeptosRoutes for axum::Router {
router
}
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<String>,
handler: H,
) -> Self
fn leptos_routes_with_handler<H, T>(self, paths: Vec<String>, handler: H) -> Self
where
H: axum::handler::Handler<T, (), axum::body::Body>,
T: 'static,

View File

@@ -1,21 +0,0 @@
[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

@@ -1,6 +1,6 @@
use crate::Children;
use leptos_dom::{Errors, IntoView};
use leptos_macro::{component, view};
use leptos_macro::component;
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
/// When you render a `Result<_, _>` in your view, in the `Err` case it will
@@ -45,16 +45,8 @@ where
// Run children so that they render and execute resources
let children = children(cx);
move || {
match errors.get().0.is_empty() {
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>
</>
}
.into_view(cx),
}
false => fallback(cx, errors).into_view(cx),
}
}

View File

@@ -30,7 +30,7 @@ use std::hash::Hash;
/// // a unique key for each item
/// key=|counter| counter.id
/// // renders each item to a view
/// view=move |cx, counter: Counter| {
/// view=move |counter: Counter| {
/// view! {
/// cx,
/// <button>"Value: " {move || counter.count.get()}</button>
@@ -54,7 +54,7 @@ pub fn For<IF, I, T, EF, N, KF, K>(
where
IF: Fn() -> I + 'static,
I: IntoIterator<Item = T>,
EF: Fn(Scope, T) -> N + 'static,
EF: Fn(T) -> N + 'static,
N: IntoView,
KF: Fn(&T) -> K + 'static,
K: Eq + Hash + 'static,

View File

@@ -34,8 +34,6 @@
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
//! - [`counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable) adapts the `counters` example
//! to show how to use Leptos with `stable` Rust.
//! - [`error_boundary`](https://github.com/leptos-rs/leptos/tree/main/examples/error_boundary) shows how to use
//! `Result` types to handle errors.
//! - [`parent_child`](https://github.com/leptos-rs/leptos/tree/main/examples/parent_child) shows four different
//! ways a parent component can communicate with a child, including passing a closure, context, and more
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) implements the classic to-do
@@ -132,7 +130,7 @@
//!
//! #[component]
//! fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
//! todo!()
//! todo!()
//! }
//!
//! pub fn main() {
@@ -141,29 +139,16 @@
//! # }
//! ```
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, 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_config::*;
pub use leptos_dom;
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};
pub use leptos_dom::*;
pub use leptos_macro::*;
pub use leptos_reactive::*;
pub use leptos_server::{
self, create_action, create_multi_action, create_server_action,
create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError,
};
pub use leptos_server;
pub use leptos_server::*;
pub use tracing;
pub use typed_builder;
mod error_boundary;
pub use error_boundary::*;
@@ -174,11 +159,10 @@ pub use show::*;
mod suspense;
pub use suspense::*;
mod transition;
#[cfg(debug_assertions)]
#[doc(hidden)]
pub use tracing;
pub use transition::*;
pub use leptos_reactive::debug_warn;
extern crate self as leptos;
/// The most common type for the `children` property on components,
@@ -194,20 +178,23 @@ 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::*;
///
/// #[component]
/// pub fn MyHeading(
/// cx: Scope,
/// text: String,
/// #[prop(optional, into)] class: Option<AttributeValue>,
/// cx: Scope,
/// text: String,
/// #[prop(optional, into)]
/// class: Option<AttributeValue>
/// ) -> impl IntoView {
/// view! {
/// cx,
/// <h1 class=class>{text}</h1>
/// }
/// view!{
/// cx,
/// <h1 class=class>{text}</h1>
/// }
/// }
/// ```
pub type AttributeValue = Box<dyn IntoAttribute>;

View File

@@ -1,5 +1,6 @@
use cfg_if::cfg_if;
use leptos_dom::{DynChild, Fragment, HydrationCtx, IntoView};
use leptos_dom::HydrationCtx;
use leptos_dom::{DynChild, Fragment, IntoView};
use leptos_macro::component;
use leptos_reactive::{provide_context, Scope, SuspenseContext};
use std::rc::Rc;
@@ -91,7 +92,7 @@ where
let initial = {
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
child
child.clone()
}
// show the fallback, but also prepare to stream HTML
else {

View File

@@ -19,9 +19,7 @@ use std::{cell::RefCell, rc::Rc};
/// # use leptos::*;
/// # if false {
/// # run_scope(create_runtime(), |cx| {
/// async fn fetch_cats(how_many: u32) -> Option<Vec<String>> {
/// Some(vec![])
/// }
/// async fn fetch_cats(how_many: u32) -> Option<Vec<String>> { Some(vec![]) }
///
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
/// let (pending, set_pending) = create_signal(cx, false);

View File

@@ -16,11 +16,7 @@ fn simple_ssr_test() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span \
id=\"_0-3\">Value: \
<!--hk=_0-4o|leptos-dyn-child-start-->0<!\
--hk=_0-4c|leptos-dyn-child-end-->!</span><button \
id=\"_0-5\">+1</button></div>"
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span id=\"_0-3\">Value: <!--hk=_0-4o|leptos-dyn-child-start-->0<!--hk=_0-4c|leptos-dyn-child-end-->!</span><button id=\"_0-5\">+1</button></div>"
);
});
}
@@ -54,21 +50,7 @@ fn ssr_test_with_components() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\" \
class=\"counters\"><!--hk=_0-1-0o|leptos-counter-start--><div \
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
id=\"_0-1-3\">Value: \
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
id=\"_0-1-5\">+1</button></div><!\
--hk=_0-1-0c|leptos-counter-end--><!\
--hk=_0-1-5-0o|leptos-counter-start--><div \
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
id=\"_0-1-5-3\">Value: \
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
id=\"_0-1-5-5\">+1</button></div><!\
--hk=_0-1-5-0c|leptos-counter-end--></div>"
"<div id=\"_0-1\" class=\"counters\"><!--hk=_0-1-0o|leptos-counter-start--><div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span id=\"_0-1-3\">Value: <!--hk=_0-1-4o|leptos-dyn-child-start-->1<!--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5\">+1</button></div><!--hk=_0-1-0c|leptos-counter-end--><!--hk=_0-1-5-0o|leptos-counter-start--><div id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span id=\"_0-1-5-3\">Value: <!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5-5\">+1</button></div><!--hk=_0-1-5-0c|leptos-counter-end--></div>"
);
});
}
@@ -102,22 +84,7 @@ fn ssr_test_with_snake_case_components() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\" \
class=\"counters\"><!\
--hk=_0-1-0o|leptos-snake-case-counter-start--><div \
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
id=\"_0-1-3\">Value: \
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
id=\"_0-1-5\">+1</button></div><!\
--hk=_0-1-0c|leptos-snake-case-counter-end--><!\
--hk=_0-1-5-0o|leptos-snake-case-counter-start--><div \
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
id=\"_0-1-5-3\">Value: \
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
id=\"_0-1-5-5\">+1</button></div><!\
--hk=_0-1-5-0c|leptos-snake-case-counter-end--></div>"
"<div id=\"_0-1\" class=\"counters\"><!--hk=_0-1-0o|leptos-snake-case-counter-start--><div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span id=\"_0-1-3\">Value: <!--hk=_0-1-4o|leptos-dyn-child-start-->1<!--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5\">+1</button></div><!--hk=_0-1-0c|leptos-snake-case-counter-end--><!--hk=_0-1-5-0o|leptos-snake-case-counter-start--><div id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span id=\"_0-1-5-3\">Value: <!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5-5\">+1</button></div><!--hk=_0-1-5-0c|leptos-snake-case-counter-end--></div>"
);
});
}
@@ -158,8 +125,7 @@ fn ssr_with_styles() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" \
class=\"btn myclass\">-1</button></div>"
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" class=\"btn myclass\">-1</button></div>"
);
});
}

View File

@@ -1,4 +1,5 @@
use std::{net::AddrParseError, num::ParseIntError};
use thiserror::Error;
#[derive(Debug, Error, Clone)]

View File

@@ -5,7 +5,9 @@ pub mod errors;
use crate::errors::LeptosConfigError;
use config::{Config, File, FileFormat};
use regex::Regex;
use std::{convert::TryFrom, env::VarError, fs, net::SocketAddr, str::FromStr};
use std::convert::TryFrom;
use std::fs;
use std::{env::VarError, net::SocketAddr, str::FromStr};
use typed_builder::TypedBuilder;
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
@@ -51,26 +53,18 @@ pub struct LeptosOptions {
impl LeptosOptions {
fn try_from_env() -> Result<Self, LeptosConfigError> {
Ok(LeptosOptions {
output_name: std::env::var("LEPTOS_OUTPUT_NAME").map_err(|e| {
LeptosConfigError::EnvVarError(format!(
"LEPTOS_OUTPUT_NAME: {e}"
))
})?,
output_name: std::env::var("LEPTOS_OUTPUT_NAME")
.map_err(|e| LeptosConfigError::EnvVarError(format!("LEPTOS_OUTPUT_NAME: {e}")))?,
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
env: Env::default(),
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
.parse()?,
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?
.parse()?,
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?.parse()?,
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?.parse()?,
})
}
}
fn env_w_default(
key: &str,
default: &str,
) -> Result<String, LeptosConfigError> {
fn env_w_default(key: &str, default: &str) -> Result<String, LeptosConfigError> {
match std::env::var(key) {
Ok(val) => Ok(val),
Err(VarError::NotPresent) => Ok(default.to_string()),
@@ -99,8 +93,7 @@ fn from_str(input: &str) -> Result<Env, String> {
"dev" | "development" => Ok(Env::DEV),
"prod" | "production" => Ok(Env::PROD),
_ => Err(format!(
"{input} is not a supported environment. Use either `dev` or \
`production`.",
"{input} is not a supported environment. Use either `dev` or `production`.",
)),
}
}
@@ -139,15 +132,11 @@ impl TryFrom<String> for Env {
/// you'll need to set the options as environment variables or rely on the defaults. This is the preferred
/// approach for cargo-leptos. If Some("./Cargo.toml") is provided, Leptos will read in the settings itself. This
/// option currently does not allow dashes in file or foldernames, as all dashes become underscores
pub async fn get_configuration(
path: Option<&str>,
) -> Result<ConfFile, LeptosConfigError> {
pub async fn get_configuration(path: Option<&str>) -> Result<ConfFile, LeptosConfigError> {
if let Some(path) = path {
let text = fs::read_to_string(path)
.map_err(|_| LeptosConfigError::ConfigNotFound)?;
let text = fs::read_to_string(path).map_err(|_| LeptosConfigError::ConfigNotFound)?;
let re: Regex =
Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
let re: Regex = Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
let start = match re.find(&text) {
Some(found) => found.start(),
None => return Err(LeptosConfigError::ConfigSectionNotFound),
@@ -165,9 +154,7 @@ pub async fn get_configuration(
// Layer on the environment-specific values.
// Add in settings from environment variables (with a prefix of LEPTOS and '_' as separator)
// E.g. `LEPTOS_RELOAD_PORT=5001 would set `LeptosOptions.reload_port`
.add_source(
config::Environment::with_prefix("LEPTOS").separator("_"),
)
.add_source(config::Environment::with_prefix("LEPTOS").separator("_"))
.build()?;
settings

View File

@@ -32,7 +32,7 @@ fn view_fn(cx: Scope) -> impl IntoView {
<For
each=|| vec![0, 1, 2, 3, 4, 5, 6, 7]
key=|i| *i
view=|cx, i| view! { cx, {i} }
view=|i| view! { cx, {i} }
/>
}
.into_view(cx);

4
leptos_dom/rustfmt.toml Normal file
View File

@@ -0,0 +1,4 @@
max_width = 80
imports_granularity = "Crate"
tab_spaces = 2
format_strings = true

View File

@@ -5,8 +5,8 @@ mod fragment;
mod unit;
use crate::{
hydration::{HydrationCtx, HydrationKey},
Comment, IntoView, View,
hydration::{HydrationCtx, HydrationKey},
Comment, IntoView, View,
};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use crate::{mount_child, prepare_to_move, MountKind, Mountable};
@@ -28,234 +28,231 @@ use wasm_bindgen::JsCast;
#[derive(educe::Educe)]
#[educe(Default, Clone, PartialEq, Eq)]
pub enum CoreComponent {
/// The [Unit] component.
#[educe(Default)]
Unit(UnitRepr),
/// The [DynChild] component.
DynChild(DynChildRepr),
/// The [Each] component.
Each(EachRepr),
/// The [Unit] component.
#[educe(Default)]
Unit(UnitRepr),
/// The [DynChild] component.
DynChild(DynChildRepr),
/// The [Each] component.
Each(EachRepr),
}
impl fmt::Debug for CoreComponent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unit(u) => u.fmt(f),
Self::DynChild(dc) => dc.fmt(f),
Self::Each(e) => e.fmt(f),
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unit(u) => u.fmt(f),
Self::DynChild(dc) => dc.fmt(f),
Self::Each(e) => e.fmt(f),
}
}
}
/// Custom leptos component.
#[derive(Clone, PartialEq, Eq)]
pub struct ComponentRepr {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) document_fragment: web_sys::DocumentFragment,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
mounted: Rc<OnceCell<()>>,
#[cfg(debug_assertions)]
pub(crate) name: Cow<'static, str>,
#[cfg(debug_assertions)]
_opening: Comment,
/// The children of the component.
pub children: Vec<View>,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: HydrationKey,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) document_fragment: web_sys::DocumentFragment,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
mounted: Rc<OnceCell<()>>,
#[cfg(debug_assertions)]
pub(crate) name: Cow<'static, str>,
#[cfg(debug_assertions)]
_opening: Comment,
/// The children of the component.
pub children: Vec<View>,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: HydrationKey,
}
impl fmt::Debug for ComponentRepr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write;
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write;
if self.children.is_empty() {
#[cfg(debug_assertions)]
return write!(f, "<{} />", self.name);
if self.children.is_empty() {
#[cfg(debug_assertions)]
return write!(f, "<{} />", self.name);
#[cfg(not(debug_assertions))]
return f.write_str("<Component />");
} else {
#[cfg(debug_assertions)]
writeln!(f, "<{}>", self.name)?;
#[cfg(not(debug_assertions))]
f.write_str("<Component>")?;
#[cfg(not(debug_assertions))]
return f.write_str("<Component />");
} else {
#[cfg(debug_assertions)]
writeln!(f, "<{}>", self.name)?;
#[cfg(not(debug_assertions))]
f.write_str("<Component>")?;
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
for child in &self.children {
writeln!(pad_adapter, "{child:#?}")?;
}
for child in &self.children {
writeln!(pad_adapter, "{child:#?}")?;
}
#[cfg(debug_assertions)]
write!(f, "</{}>", self.name)?;
#[cfg(not(debug_assertions))]
f.write_str("</Component>")?;
#[cfg(debug_assertions)]
write!(f, "</{}>", self.name)?;
#[cfg(not(debug_assertions))]
f.write_str("</Component>")?;
Ok(())
}
Ok(())
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
impl Mountable for ComponentRepr {
fn get_mountable_node(&self) -> web_sys::Node {
if self.mounted.get().is_none() {
self.mounted.set(()).unwrap();
fn get_mountable_node(&self) -> web_sys::Node {
if self.mounted.get().is_none() {
self.mounted.set(()).unwrap();
self.document_fragment
.unchecked_ref::<web_sys::Node>()
.to_owned()
}
// We need to prepare all children to move
else {
let opening = self.get_opening_node();
prepare_to_move(
&self.document_fragment,
&opening,
&self.closing.node,
);
self.document_fragment.clone().unchecked_into()
}
self
.document_fragment
.unchecked_ref::<web_sys::Node>()
.to_owned()
}
// We need to prepare all children to move
else {
let opening = self.get_opening_node();
fn get_opening_node(&self) -> web_sys::Node {
#[cfg(debug_assertions)]
return self._opening.node.clone();
prepare_to_move(&self.document_fragment, &opening, &self.closing.node);
#[cfg(not(debug_assertions))]
return if let Some(child) = self.children.get(0) {
child.get_opening_node()
} else {
self.closing.node.clone()
};
self.document_fragment.clone().unchecked_into()
}
}
fn get_closing_node(&self) -> web_sys::Node {
self.closing.node.clone()
}
fn get_opening_node(&self) -> web_sys::Node {
#[cfg(debug_assertions)]
return self._opening.node.clone();
#[cfg(not(debug_assertions))]
return if let Some(child) = self.children.get(0) {
child.get_opening_node()
} else {
self.closing.node.clone()
};
}
fn get_closing_node(&self) -> web_sys::Node {
self.closing.node.clone()
}
}
impl IntoView for ComponentRepr {
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<Component />", skip_all, fields(name = %self.name)))]
fn into_view(self, _: Scope) -> View {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
if !HydrationCtx::is_hydrating() {
for child in &self.children {
mount_child(MountKind::Before(&self.closing.node), child);
}
}
View::Component(self)
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<Component />", skip_all, fields(name = %self.name)))]
fn into_view(self, _: Scope) -> View {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
if !HydrationCtx::is_hydrating() {
for child in &self.children {
mount_child(MountKind::Before(&self.closing.node), child);
}
}
View::Component(self)
}
}
impl ComponentRepr {
/// Creates a new [`Component`].
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self::new_with_id(name, HydrationCtx::id())
}
/// Creates a new [`Component`] with the given hydration ID.
pub fn new_with_id(
name: impl Into<Cow<'static, str>>,
id: HydrationKey,
) -> Self {
let name = name.into();
let markers = (
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
#[cfg(debug_assertions)]
Comment::new(Cow::Owned(format!("<{name}>")), &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let document_fragment = {
let fragment = crate::document().create_document_fragment();
// Insert the comments into the document fragment
// so they can serve as our references when inserting
// future nodes
if !HydrationCtx::is_hydrating() {
#[cfg(debug_assertions)]
fragment
.append_with_node_2(&markers.1.node, &markers.0.node)
.expect("append to not err");
#[cfg(not(debug_assertions))]
fragment
.append_with_node_1(&markers.0.node)
.expect("append to not err");
}
fragment
};
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
document_fragment,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
mounted: Default::default(),
#[cfg(debug_assertions)]
_opening: markers.1,
closing: markers.0,
#[cfg(debug_assertions)]
name,
children: Vec::with_capacity(1),
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
}
/// Creates a new [`Component`].
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self::new_with_id(name, HydrationCtx::id())
}
/// Creates a new [`Component`] with the given hydration ID.
pub fn new_with_id(
name: impl Into<Cow<'static, str>>,
id: HydrationKey,
) -> Self {
let name = name.into();
let markers = (
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
#[cfg(debug_assertions)]
Comment::new(Cow::Owned(format!("<{name}>")), &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let document_fragment = {
let fragment = crate::document().create_document_fragment();
// Insert the comments into the document fragment
// so they can serve as our references when inserting
// future nodes
if !HydrationCtx::is_hydrating() {
#[cfg(debug_assertions)]
fragment
.append_with_node_2(&markers.1.node, &markers.0.node)
.expect("append to not err");
#[cfg(not(debug_assertions))]
fragment
.append_with_node_1(&markers.0.node)
.expect("append to not err");
}
fragment
};
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
document_fragment,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
mounted: Default::default(),
#[cfg(debug_assertions)]
_opening: markers.1,
closing: markers.0,
#[cfg(debug_assertions)]
name,
children: Vec::with_capacity(1),
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
}
}
}
/// A user-defined `leptos` component.
pub struct Component<F, V>
where
F: FnOnce(Scope) -> V,
V: IntoView,
F: FnOnce(Scope) -> V,
V: IntoView,
{
id: HydrationKey,
name: Cow<'static, str>,
children_fn: F,
id: HydrationKey,
name: Cow<'static, str>,
children_fn: F,
}
impl<F, V> Component<F, V>
where
F: FnOnce(Scope) -> V,
V: IntoView,
F: FnOnce(Scope) -> V,
V: IntoView,
{
/// Creates a new component.
pub fn new(name: impl Into<Cow<'static, str>>, f: F) -> Self {
Self {
id: HydrationCtx::next_component(),
name: name.into(),
children_fn: f,
}
/// Creates a new component.
pub fn new(name: impl Into<Cow<'static, str>>, f: F) -> Self {
Self {
id: HydrationCtx::next_component(),
name: name.into(),
children_fn: f,
}
}
}
impl<F, V> IntoView for Component<F, V>
where
F: FnOnce(Scope) -> V,
V: IntoView,
F: FnOnce(Scope) -> V,
V: IntoView,
{
#[track_caller]
fn into_view(self, cx: Scope) -> View {
let Self {
id,
name,
children_fn,
} = self;
#[track_caller]
fn into_view(self, cx: Scope) -> View {
let Self {
id,
name,
children_fn,
} = self;
let mut repr = ComponentRepr::new_with_id(name, id);
let mut repr = ComponentRepr::new_with_id(name, id);
// disposed automatically when the parent scope is disposed
let (child, _) = cx
.run_child_scope(|cx| cx.untrack(|| children_fn(cx).into_view(cx)));
// disposed automatically when the parent scope is disposed
let (child, _) =
cx.run_child_scope(|cx| cx.untrack(|| children_fn(cx).into_view(cx)));
repr.children.push(child);
repr.children.push(child);
repr.into_view(cx)
}
repr.into_view(cx)
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
hydration::{HydrationCtx, HydrationKey},
Comment, IntoView, View,
hydration::{HydrationCtx, HydrationKey},
Comment, IntoView, View,
};
use cfg_if::cfg_if;
use leptos_reactive::Scope;
@@ -16,349 +16,320 @@ cfg_if! {
/// The internal representation of the [`DynChild`] core-component.
#[derive(Clone, PartialEq, Eq)]
pub struct DynChildRepr {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
document_fragment: web_sys::DocumentFragment,
#[cfg(debug_assertions)]
opening: Comment,
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: HydrationKey,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
document_fragment: web_sys::DocumentFragment,
#[cfg(debug_assertions)]
opening: Comment,
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: HydrationKey,
}
impl fmt::Debug for DynChildRepr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write;
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write;
f.write_str("<DynChild>\n")?;
f.write_str("<DynChild>\n")?;
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
writeln!(
pad_adapter,
"{:#?}",
self.child.borrow().deref().deref().as_ref().unwrap()
)?;
writeln!(
pad_adapter,
"{:#?}",
self.child.borrow().deref().deref().as_ref().unwrap()
)?;
f.write_str("</DynChild>")
}
f.write_str("</DynChild>")
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
impl Mountable for DynChildRepr {
fn get_mountable_node(&self) -> web_sys::Node {
if self.document_fragment.child_nodes().length() != 0 {
self.document_fragment.clone().unchecked_into()
} else {
let opening = self.get_opening_node();
fn get_mountable_node(&self) -> web_sys::Node {
if self.document_fragment.child_nodes().length() != 0 {
self.document_fragment.clone().unchecked_into()
} else {
let opening = self.get_opening_node();
prepare_to_move(
&self.document_fragment,
&opening,
&self.closing.node,
);
prepare_to_move(&self.document_fragment, &opening, &self.closing.node);
self.document_fragment.clone().unchecked_into()
}
self.document_fragment.clone().unchecked_into()
}
}
fn get_opening_node(&self) -> web_sys::Node {
#[cfg(debug_assertions)]
return self.opening.node.clone();
fn get_opening_node(&self) -> web_sys::Node {
#[cfg(debug_assertions)]
return self.opening.node.clone();
#[cfg(not(debug_assertions))]
return self
.child
.borrow()
.as_ref()
.as_ref()
.unwrap()
.get_opening_node();
}
#[cfg(not(debug_assertions))]
return self
.child
.borrow()
.as_ref()
.as_ref()
.unwrap()
.get_opening_node();
}
fn get_closing_node(&self) -> web_sys::Node {
self.closing.node.clone()
}
fn get_closing_node(&self) -> web_sys::Node {
self.closing.node.clone()
}
}
impl DynChildRepr {
fn new_with_id(id: HydrationKey) -> Self {
let markers = (
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
#[cfg(debug_assertions)]
Comment::new(Cow::Borrowed("<DynChild>"), &id, false),
);
fn new_with_id(id: HydrationKey) -> Self {
let markers = (
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
#[cfg(debug_assertions)]
Comment::new(Cow::Borrowed("<DynChild>"), &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let document_fragment = {
let fragment = crate::document().create_document_fragment();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let document_fragment = {
let fragment = crate::document().create_document_fragment();
// Insert the comments into the document fragment
// so they can serve as our references when inserting
// future nodes
if !HydrationCtx::is_hydrating() {
#[cfg(debug_assertions)]
fragment
.append_with_node_2(&markers.1.node, &markers.0.node)
.unwrap();
#[cfg(not(debug_assertions))]
fragment.append_with_node_1(&markers.0.node).unwrap();
}
// Insert the comments into the document fragment
// so they can serve as our references when inserting
// future nodes
if !HydrationCtx::is_hydrating() {
#[cfg(debug_assertions)]
fragment
.append_with_node_2(&markers.1.node, &markers.0.node)
.unwrap();
#[cfg(not(debug_assertions))]
fragment.append_with_node_1(&markers.0.node).unwrap();
}
fragment
};
fragment
};
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
document_fragment,
#[cfg(debug_assertions)]
opening: markers.1,
child: Default::default(),
closing: markers.0,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
}
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
document_fragment,
#[cfg(debug_assertions)]
opening: markers.1,
child: Default::default(),
closing: markers.0,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
}
}
}
/// Represents any [`View`] that can change over time.
pub struct DynChild<CF, N>
where
CF: Fn() -> N + 'static,
N: IntoView,
CF: Fn() -> N + 'static,
N: IntoView,
{
id: crate::HydrationKey,
child_fn: CF,
id: crate::HydrationKey,
child_fn: CF,
}
impl<CF, N> DynChild<CF, N>
where
CF: Fn() -> N + 'static,
N: IntoView,
CF: Fn() -> N + 'static,
N: IntoView,
{
/// Creates a new dynamic child which will re-render whenever it's
/// signal dependencies change.
pub fn new(child_fn: CF) -> Self {
Self::new_with_id(HydrationCtx::id(), child_fn)
}
/// Creates a new dynamic child which will re-render whenever it's
/// signal dependencies change.
pub fn new(child_fn: CF) -> Self {
Self::new_with_id(HydrationCtx::id(), child_fn)
}
#[doc(hidden)]
pub fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
Self { id, child_fn }
}
#[doc(hidden)]
pub fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
Self { id, child_fn }
}
}
impl<CF, N> IntoView for DynChild<CF, N>
where
CF: Fn() -> N + 'static,
N: IntoView,
CF: Fn() -> N + 'static,
N: IntoView,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "<DynChild />", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
// concrete inner function
fn create_dyn_view(
cx: Scope,
component: DynChildRepr,
child_fn: Box<dyn Fn() -> View>,
) -> DynChildRepr {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let closing = component.closing.node.clone();
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "<DynChild />", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
// concrete inner function
fn create_dyn_view(
cx: Scope,
component: DynChildRepr,
child_fn: Box<dyn Fn() -> View>,
) -> DynChildRepr {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let closing = component.closing.node.clone();
let child = component.child.clone();
let child = component.child.clone();
#[cfg(all(
debug_assertions,
target_arch = "wasm32",
feature = "web"
))]
let span = tracing::Span::current();
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
let span = tracing::Span::current();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
create_effect(
cx,
move |prev_run: Option<(
Option<web_sys::Node>,
ScopeDisposer,
)>| {
#[cfg(debug_assertions)]
let _guard = span.enter();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
create_effect(
cx,
move |prev_run: Option<(Option<web_sys::Node>, ScopeDisposer)>| {
#[cfg(debug_assertions)]
let _guard = span.enter();
let (new_child, disposer) =
cx.run_child_scope(|cx| child_fn().into_view(cx));
let (new_child, disposer) =
cx.run_child_scope(|cx| child_fn().into_view(cx));
let mut child_borrow = child.borrow_mut();
let mut child_borrow = child.borrow_mut();
// Is this at least the second time we are loading a child?
if let Some((prev_t, prev_disposer)) = prev_run {
let child = child_borrow.take().unwrap();
// Is this at least the second time we are loading a child?
if let Some((prev_t, prev_disposer)) = prev_run {
let child = child_borrow.take().unwrap();
// Dispose of the scope
prev_disposer.dispose();
// Dispose of the scope
prev_disposer.dispose();
// We need to know if our child wasn't moved elsewhere.
// If it was, `DynChild` no longer "owns" that child, and
// is therefore no longer sound to unmount it from the DOM
// or to reuse it in the case of a text node
// We need to know if our child wasn't moved elsewhere.
// If it was, `DynChild` no longer "owns" that child, and
// is therefore no longer sound to unmount it from the DOM
// or to reuse it in the case of a text node
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none()
&& child.get_closing_node().next_sibling().as_ref()
!= Some(&closing);
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none()
&& child.get_closing_node().next_sibling().as_ref()
!= Some(&closing);
// If the previous child was a text node, we would like to
// make use of it again if our current child is also a text
// node
let ret = if let Some(prev_t) = prev_t {
// Here, our child is also a text node
if let Some(new_t) = new_child.get_text() {
if !was_child_moved && child != new_child {
prev_t
.unchecked_ref::<web_sys::Text>()
.set_data(&new_t.content);
// If the previous child was a text node, we would like to
// make use of it again if our current child is also a text
// node
let ret = if let Some(prev_t) = prev_t {
// Here, our child is also a text node
if let Some(new_t) = new_child.get_text() {
if !was_child_moved && child != new_child {
prev_t
.unchecked_ref::<web_sys::Text>()
.set_data(&new_t.content);
**child_borrow = Some(new_child);
**child_borrow = Some(new_child);
(Some(prev_t), disposer)
} else {
mount_child(
MountKind::Before(&closing),
&new_child,
);
(Some(prev_t), disposer)
} else {
mount_child(MountKind::Before(&closing), &new_child);
**child_borrow = Some(new_child.clone());
**child_borrow = Some(new_child.clone());
(Some(new_t.node.clone()), disposer)
}
}
// Child is not a text node, so we can remove the previous
// text node
else {
if !was_child_moved && child != new_child {
// Remove the text
closing
.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
(Some(new_t.node.clone()), disposer)
}
}
// Child is not a text node, so we can remove the previous
// text node
else {
if !was_child_moved && child != new_child {
// Remove the text
closing
.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
// Mount the new child, and we're done
mount_child(
MountKind::Before(&closing),
&new_child,
);
// Mount the new child, and we're done
mount_child(MountKind::Before(&closing), &new_child);
**child_borrow = Some(new_child);
**child_borrow = Some(new_child);
(None, disposer)
}
}
// Otherwise, the new child can still be a text node,
// but we know the previous child was not, so no special
// treatment here
else {
// Technically, I think this check shouldn't be necessary, but
// I can imagine some edge case that the child changes while
// hydration is ongoing
if !HydrationCtx::is_hydrating() {
if !was_child_moved && child != new_child {
// Remove the child
let start = child.get_opening_node();
let end = &closing;
(None, disposer)
}
}
// Otherwise, the new child can still be a text node,
// but we know the previous child was not, so no special
// treatment here
else {
// Technically, I think this check shouldn't be necessary, but
// I can imagine some edge case that the child changes while
// hydration is ongoing
if !HydrationCtx::is_hydrating() {
if !was_child_moved && child != new_child {
// Remove the child
let start = child.get_opening_node();
let end = &closing;
unmount_child(&start, end);
}
unmount_child(&start, end);
}
// Mount the new child
mount_child(
MountKind::Before(&closing),
&new_child,
);
}
// Mount the new child
mount_child(MountKind::Before(&closing), &new_child);
}
// We want to reuse text nodes, so hold onto it if
// our child is one
let t =
new_child.get_text().map(|t| t.node.clone());
// We want to reuse text nodes, so hold onto it if
// our child is one
let t = new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
**child_borrow = Some(new_child);
(t, disposer)
};
(t, disposer)
};
ret
}
// Otherwise, we know for sure this is our first time
else {
// We need to remove the text created from SSR
if HydrationCtx::is_hydrating()
&& new_child.get_text().is_some()
{
let t = closing
.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>();
ret
}
// Otherwise, we know for sure this is our first time
else {
// We need to remove the text created from SSR
if HydrationCtx::is_hydrating() && new_child.get_text().is_some() {
let t = closing
.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>();
// See note on ssr.rs when matching on `DynChild`
// for more details on why we need to do this for
// release
if !cfg!(debug_assertions) {
t.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
// See note on ssr.rs when matching on `DynChild`
// for more details on why we need to do this for
// release
if !cfg!(debug_assertions) {
t.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
t.remove();
t.remove();
mount_child(
MountKind::Before(&closing),
&new_child,
);
}
// If we are not hydrating, we simply mount the child
if !HydrationCtx::is_hydrating() {
mount_child(
MountKind::Before(&closing),
&new_child,
);
}
// We want to update text nodes, rather than replace them, so
// make sure to hold onto the text node
let t = new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
(t, disposer)
}
},
);
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let new_child = child_fn().into_view(cx);
**child.borrow_mut() = Some(new_child);
mount_child(MountKind::Before(&closing), &new_child);
}
component
}
// If we are not hydrating, we simply mount the child
if !HydrationCtx::is_hydrating() {
mount_child(MountKind::Before(&closing), &new_child);
}
// monomorphized outer function
let Self { id, child_fn } = self;
// We want to update text nodes, rather than replace them, so
// make sure to hold onto the text node
let t = new_child.get_text().map(|t| t.node.clone());
let component = DynChildRepr::new_with_id(id);
let component = create_dyn_view(
cx,
component,
Box::new(move || child_fn().into_view(cx)),
);
**child_borrow = Some(new_child);
View::CoreComponent(crate::CoreComponent::DynChild(component))
(t, disposer)
}
},
);
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let new_child = child_fn().into_view(cx);
**child.borrow_mut() = Some(new_child);
}
component
}
// monomorphized outer function
let Self { id, child_fn } = self;
let component = DynChildRepr::new_with_id(id);
let component = create_dyn_view(
cx,
component,
Box::new(move || child_fn().into_view(cx)),
);
View::CoreComponent(crate::CoreComponent::DynChild(component))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,83 +9,79 @@ pub struct Errors(pub HashMap<String, Arc<dyn Error + Send + Sync>>);
impl<T, E> IntoView for Result<T, E>
where
T: IntoView + 'static,
E: Error + Send + Sync + 'static,
T: IntoView + 'static,
E: Error + Send + Sync + 'static,
{
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
let id = HydrationCtx::peek().previous;
let errors = use_context::<RwSignal<Errors>>(cx);
match self {
Ok(stuff) => {
if let Some(errors) = errors {
errors.update(|errors| {
errors.0.remove(&id);
});
}
stuff.into_view(cx)
}
Err(error) => {
match errors {
Some(errors) => {
errors.update({
#[cfg(all(
target_arch = "wasm32",
feature = "web"
))]
let id = id.clone();
move |errors: &mut Errors| errors.insert(id, error)
});
// remove the error from the list if this drops,
// i.e., if it's in a DynChild that switches from Err to Ok
// Only can run on the client, will panic on the server
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use leptos_reactive::{on_cleanup, queue_microtask};
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
errors.remove::<E>(&id);
});
});
});
}
}
}
None => {
#[cfg(debug_assertions)]
warn!(
"No ErrorBoundary components found! Returning \
errors will not be handled and will silently \
disappear"
);
}
}
().into_view(cx)
}
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
let id = HydrationCtx::peek().previous;
let errors = use_context::<RwSignal<Errors>>(cx);
match self {
Ok(stuff) => {
if let Some(errors) = errors {
errors.update(|errors| {
errors.0.remove(&id);
});
}
stuff.into_view(cx)
}
Err(error) => {
match errors {
Some(errors) => {
errors.update({
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let id = id.clone();
move |errors: &mut Errors| errors.insert(id, error)
});
// remove the error from the list if this drops,
// i.e., if it's in a DynChild that switches from Err to Ok
// Only can run on the client, will panic on the server
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use leptos_reactive::{on_cleanup, queue_microtask};
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
errors.remove::<E>(&id);
});
});
});
}
}
}
None => {
#[cfg(debug_assertions)]
warn!(
"No ErrorBoundary components found! Returning errors will not \
be handled and will silently disappear"
);
}
}
().into_view(cx)
}
}
}
}
impl Errors {
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn insert<E>(&mut self, key: String, error: E)
where
E: Error + Send + Sync + 'static,
{
self.0.insert(key, Arc::new(error));
}
/// Add an error with the default key for errors outside the reactive system
pub fn insert_with_default_key<E>(&mut self, error: E)
where
E: Error + Send + Sync + 'static,
{
self.0.insert(String::new(), Arc::new(error));
}
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn remove<E>(&mut self, key: &str)
where
E: Error + Send + Sync + 'static,
{
self.0.remove(key);
}
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn insert<E>(&mut self, key: String, error: E)
where
E: Error + Send + Sync + 'static,
{
self.0.insert(key, Arc::new(error));
}
/// Add an error with the default key for errors outside the reactive system
pub fn insert_with_default_key<E>(&mut self, error: E)
where
E: Error + Send + Sync + 'static,
{
self.0.insert(String::new(), Arc::new(error));
}
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn remove<E>(&mut self, key: &str)
where
E: Error + Send + Sync + 'static,
{
self.0.remove(key);
}
}

View File

@@ -1,78 +1,79 @@
use crate::{
hydration::HydrationKey, ComponentRepr, HydrationCtx, IntoView, View,
};
use leptos_reactive::Scope;
use crate::{
hydration::HydrationKey, ComponentRepr, HydrationCtx, IntoView, View,
};
/// Trait for converting any iterable into a [`Fragment`].
pub trait IntoFragment {
/// Consumes this type, returning [`Fragment`].
fn into_fragment(self, cx: Scope) -> Fragment;
/// Consumes this type, returning [`Fragment`].
fn into_fragment(self, cx: Scope) -> Fragment;
}
impl<I, V> IntoFragment for I
where
I: IntoIterator<Item = V>,
V: IntoView,
I: IntoIterator<Item = V>,
V: IntoView,
{
fn into_fragment(self, cx: Scope) -> Fragment {
self.into_iter().map(|v| v.into_view(cx)).collect()
}
fn into_fragment(self, cx: Scope) -> Fragment {
self.into_iter().map(|v| v.into_view(cx)).collect()
}
}
/// Represents a group of [`views`](View).
#[derive(Debug, Clone)]
pub struct Fragment {
id: HydrationKey,
/// The nodes contained in the fragment.
pub nodes: Vec<View>,
id: HydrationKey,
/// The nodes contained in the fragment.
pub nodes: Vec<View>,
}
impl FromIterator<View> for Fragment {
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
Fragment::new(iter.into_iter().collect())
}
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
Fragment::new(iter.into_iter().collect())
}
}
impl From<View> for Fragment {
fn from(view: View) -> Self {
Fragment::new(vec![view])
}
fn from(view: View) -> Self {
Fragment::new(vec![view])
}
}
impl Fragment {
/// Creates a new [`Fragment`] from a [`Vec<Node>`].
pub fn new(nodes: Vec<View>) -> Self {
Self::new_with_id(HydrationCtx::id(), nodes)
}
/// Creates a new [`Fragment`] from a [`Vec<Node>`].
pub fn new(nodes: Vec<View>) -> Self {
Self::new_with_id(HydrationCtx::id(), nodes)
}
/// Creates a new [`Fragment`] from a function that returns [`Vec<Node>`].
pub fn lazy(nodes: impl FnOnce() -> Vec<View>) -> Self {
Self::new_with_id(HydrationCtx::id(), nodes())
}
/// Creates a new [`Fragment`] from a function that returns [`Vec<Node>`].
pub fn lazy(nodes: impl FnOnce() -> Vec<View>) -> Self {
Self::new_with_id(HydrationCtx::id(), nodes())
}
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
Self { id, nodes }
}
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
Self { id, nodes }
}
/// Gives access to the [View] children contained within the fragment.
pub fn as_children(&self) -> &[View] {
&self.nodes
}
/// Gives access to the [View] children contained within the fragment.
pub fn as_children(&self) -> &[View] {
&self.nodes
}
/// Returns the fragment's hydration ID.
pub fn id(&self) -> &HydrationKey {
&self.id
}
/// Returns the fragment's hydration ID.
pub fn id(&self) -> &HydrationKey {
&self.id
}
}
impl IntoView for Fragment {
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "</>", skip_all, fields(children = self.nodes.len())))]
fn into_view(self, cx: leptos_reactive::Scope) -> View {
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "</>", skip_all, fields(children = self.nodes.len())))]
fn into_view(self, cx: leptos_reactive::Scope) -> View {
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
frag.children = self.nodes;
frag.children = self.nodes;
frag.into_view(cx)
}
frag.into_view(cx)
}
}

View File

@@ -15,42 +15,42 @@ use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View};
/// The internal representation of the [`Unit`] core-component.
#[derive(Clone, PartialEq, Eq)]
pub struct UnitRepr {
comment: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: HydrationKey,
comment: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: HydrationKey,
}
impl fmt::Debug for UnitRepr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<() />")
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<() />")
}
}
impl Default for UnitRepr {
fn default() -> Self {
let id = HydrationCtx::id();
fn default() -> Self {
let id = HydrationCtx::id();
Self {
comment: Comment::new("<() />", &id, true),
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
}
Self {
comment: Comment::new("<() />", &id, true),
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
impl Mountable for UnitRepr {
fn get_mountable_node(&self) -> web_sys::Node {
self.comment.node.clone().unchecked_into()
}
fn get_mountable_node(&self) -> web_sys::Node {
self.comment.node.clone().unchecked_into()
}
fn get_opening_node(&self) -> web_sys::Node {
self.comment.node.clone().unchecked_into()
}
fn get_opening_node(&self) -> web_sys::Node {
self.comment.node.clone().unchecked_into()
}
fn get_closing_node(&self) -> web_sys::Node {
self.comment.node.clone().unchecked_into()
}
fn get_closing_node(&self) -> web_sys::Node {
self.comment.node.clone().unchecked_into()
}
}
/// The unit `()` leptos counterpart.
@@ -58,13 +58,13 @@ impl Mountable for UnitRepr {
pub struct Unit;
impl IntoView for Unit {
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "<() />", skip_all)
)]
fn into_view(self, _: leptos_reactive::Scope) -> crate::View {
let component = UnitRepr::default();
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "<() />", skip_all)
)]
fn into_view(self, _: leptos_reactive::Scope) -> crate::View {
let component = UnitRepr::default();
View::CoreComponent(CoreComponent::Unit(component))
}
View::CoreComponent(CoreComponent::Unit(component))
}
}

View File

@@ -3,8 +3,8 @@ pub mod typed;
use std::{borrow::Cow, cell::RefCell, collections::HashSet};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::{
convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue,
UnwrapThrowExt,
convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue,
UnwrapThrowExt,
};
thread_local! {
@@ -14,141 +14,135 @@ thread_local! {
/// Adds an event listener to the target DOM element using implicit event delegation.
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub fn add_event_listener<E>(
target: &web_sys::Element,
event_name: Cow<'static, str>,
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
target: &web_sys::Element,
event_name: Cow<'static, str>,
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
E: FromWasmAbi + 'static,
{
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
}
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
}
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
let key = event_delegation_key(&event_name);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
add_delegated_event_listener(event_name);
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
let key = event_delegation_key(&event_name);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
add_delegated_event_listener(event_name);
}
#[doc(hidden)]
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub fn add_event_listener_undelegated<E>(
target: &web_sys::Element,
event_name: &str,
mut cb: impl FnMut(E) + 'static,
target: &web_sys::Element,
event_name: &str,
mut cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
E: FromWasmAbi + 'static,
{
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
}
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
}
}
let event_name = intern(event_name);
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
let event_name = intern(event_name);
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
// cf eventHandler in ryansolid/dom-expressions
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn add_delegated_event_listener(event_name: Cow<'static, str>) {
GLOBAL_EVENTS.with(|global_events| {
let mut events = global_events.borrow_mut();
if !events.contains(&event_name) {
// create global handler
let key = JsValue::from_str(&event_delegation_key(&event_name));
let handler = move |ev: web_sys::Event| {
let target = ev.target();
let node = ev.composed_path().get(0);
let mut node = if node.is_undefined() || node.is_null() {
JsValue::from(target)
} else {
node
};
GLOBAL_EVENTS.with(|global_events| {
let mut events = global_events.borrow_mut();
if !events.contains(&event_name) {
// create global handler
let key = JsValue::from_str(&event_delegation_key(&event_name));
let handler = move |ev: web_sys::Event| {
let target = ev.target();
let node = ev.composed_path().get(0);
let mut node = if node.is_undefined() || node.is_null() {
JsValue::from(target)
} else {
node
};
// TODO reverse Shadow DOM retargetting
// TODO reverse Shadow DOM retargetting
// TODO simulate currentTarget
// TODO simulate currentTarget
while !node.is_null() {
let node_is_disabled = js_sys::Reflect::get(
&node,
&JsValue::from_str("disabled"),
)
.unwrap_throw()
.is_truthy();
if !node_is_disabled {
let maybe_handler =
js_sys::Reflect::get(&node, &key).unwrap_throw();
if !maybe_handler.is_undefined() {
let f = maybe_handler
.unchecked_ref::<js_sys::Function>();
let _ = f.call1(&node, &ev);
while !node.is_null() {
let node_is_disabled =
js_sys::Reflect::get(&node, &JsValue::from_str("disabled"))
.unwrap_throw()
.is_truthy();
if !node_is_disabled {
let maybe_handler =
js_sys::Reflect::get(&node, &key).unwrap_throw();
if !maybe_handler.is_undefined() {
let f = maybe_handler.unchecked_ref::<js_sys::Function>();
let _ = f.call1(&node, &ev);
if ev.cancel_bubble() {
return;
}
}
}
// navigate up tree
let host =
js_sys::Reflect::get(&node, &JsValue::from_str("host"))
.unwrap_throw();
if host.is_truthy()
&& host != node
&& host.dyn_ref::<web_sys::Node>().is_some()
{
node = host;
} else if let Some(parent) =
node.unchecked_into::<web_sys::Node>().parent_node()
{
node = parent.into()
} else {
node = JsValue::null()
}
}
};
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let handler = move |e| {
let _guard = span.enter();
handler(e);
};
if ev.cancel_bubble() {
return;
}
}
}
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
let handler = Closure::wrap(handler).into_js_value();
_ = crate::window().add_event_listener_with_callback(
&event_name,
handler.unchecked_ref(),
);
// register that we've created handler
events.insert(event_name);
// navigate up tree
let host = js_sys::Reflect::get(&node, &JsValue::from_str("host"))
.unwrap_throw();
if host.is_truthy()
&& host != node
&& host.dyn_ref::<web_sys::Node>().is_some()
{
node = host;
} else if let Some(parent) =
node.unchecked_into::<web_sys::Node>().parent_node()
{
node = parent.into()
} else {
node = JsValue::null()
}
}
})
};
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let handler = move |e| {
let _guard = span.enter();
handler(e);
};
}
}
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
let handler = Closure::wrap(handler).into_js_value();
_ = crate::window()
.add_event_listener_with_callback(&event_name, handler.unchecked_ref());
// register that we've created handler
events.insert(event_name);
}
})
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn event_delegation_key(event_name: &str) -> String {
let event_name = intern(event_name);
let mut n = String::from("$$$");
n.push_str(event_name);
n
let event_name = intern(event_name);
let mut n = String::from("$$$");
n.push_str(event_name);
n
}

View File

@@ -5,20 +5,20 @@ use wasm_bindgen::convert::FromWasmAbi;
/// A trait for converting types into [web_sys events](web_sys).
pub trait EventDescriptor: Clone {
/// The [`web_sys`] event type, such as [`web_sys::MouseEvent`].
type EventType: FromWasmAbi;
/// The [`web_sys`] event type, such as [`web_sys::MouseEvent`].
type EventType: FromWasmAbi;
/// The name of the event, such as `click` or `mouseover`.
fn name(&self) -> Cow<'static, str>;
/// The name of the event, such as `click` or `mouseover`.
fn name(&self) -> Cow<'static, str>;
/// Indicates if this event bubbles. For example, `click` bubbles,
/// but `focus` does not.
///
/// If this method returns true, then the event will be delegated globally,
/// otherwise, event listeners will be directly attached to the element.
fn bubbles(&self) -> bool {
true
}
/// Indicates if this event bubbles. For example, `click` bubbles,
/// but `focus` does not.
///
/// If this method returns true, then the event will be delegated globally,
/// otherwise, event listeners will be directly attached to the element.
fn bubbles(&self) -> bool {
true
}
}
/// Overrides the [`EventDescriptor::bubbles`] method to always return
@@ -28,54 +28,54 @@ pub trait EventDescriptor: Clone {
pub struct undelegated<Ev: EventDescriptor>(pub Ev);
impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
type EventType = Ev::EventType;
type EventType = Ev::EventType;
fn name(&self) -> Cow<'static, str> {
self.0.name()
}
fn name(&self) -> Cow<'static, str> {
self.0.name()
}
fn bubbles(&self) -> bool {
false
}
fn bubbles(&self) -> bool {
false
}
}
/// A custom event.
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
name: Cow<'static, str>,
_event_type: PhantomData<E>,
name: Cow<'static, str>,
_event_type: PhantomData<E>,
}
impl<E: FromWasmAbi> Clone for Custom<E> {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
_event_type: PhantomData,
}
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
_event_type: PhantomData,
}
}
}
impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
type EventType = E;
type EventType = E;
fn name(&self) -> Cow<'static, str> {
self.name.clone()
}
fn name(&self) -> Cow<'static, str> {
self.name.clone()
}
fn bubbles(&self) -> bool {
false
}
fn bubbles(&self) -> bool {
false
}
}
impl<E: FromWasmAbi> Custom<E> {
/// Creates a custom event type that can be used within
/// [`HtmlElement::on`](crate::HtmlElement::on), for events
/// which are not covered in the [`ev`](crate::ev) module.
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self {
name: name.into(),
_event_type: PhantomData,
}
/// Creates a custom event type that can be used within
/// [`HtmlElement::on`](crate::HtmlElement::on), for events
/// which are not covered in the [`ev`](crate::ev) module.
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self {
name: name.into(),
_event_type: PhantomData,
}
}
}
macro_rules! generate_event_types {

View File

@@ -1,58 +1,56 @@
//! A variety of DOM utility functions.
use crate::{is_server, window};
use std::time::Duration;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
/// Sets a property on a DOM element.
pub fn set_property(
el: &web_sys::Element,
prop_name: &str,
value: &Option<JsValue>,
el: &web_sys::Element,
prop_name: &str,
value: &Option<JsValue>,
) {
let key = JsValue::from_str(prop_name);
match value {
Some(value) => _ = js_sys::Reflect::set(el, &key, value),
None => _ = js_sys::Reflect::delete_property(el, &key),
};
let key = JsValue::from_str(prop_name);
match value {
Some(value) => _ = js_sys::Reflect::set(el, &key, value),
None => _ = js_sys::Reflect::delete_property(el, &key),
};
}
/// Gets the value of a property set on a DOM element.
pub fn get_property(
el: &web_sys::Element,
prop_name: &str,
el: &web_sys::Element,
prop_name: &str,
) -> Result<JsValue, JsValue> {
let key = JsValue::from_str(prop_name);
js_sys::Reflect::get(el, &key)
let key = JsValue::from_str(prop_name);
js_sys::Reflect::get(el, &key)
}
/// Returns the current [`window.location`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
pub fn location() -> web_sys::Location {
window().location()
window().location()
}
/// Current [`window.location.hash`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location)
/// without the beginning #.
pub fn location_hash() -> Option<String> {
if is_server() {
None
} else {
location().hash().ok().map(|hash| hash.replace('#', ""))
}
if is_server() {
None
} else {
location().hash().ok().map(|hash| hash.replace('#', ""))
}
}
/// Current [`window.location.pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
pub fn location_pathname() -> Option<String> {
location().pathname().ok()
location().pathname().ok()
}
/// Helper function to extract [`Event.target`](https://developer.mozilla.org/en-US/docs/Web/API/Event/target)
/// from any event.
pub fn event_target<T>(event: &web_sys::Event) -> T
where
T: JsCast,
T: JsCast,
{
event.target().unwrap_throw().unchecked_into::<T>()
event.target().unwrap_throw().unchecked_into::<T>()
}
/// Helper function to extract `event.target.value` from an event.
@@ -60,60 +58,60 @@ where
/// This is useful in the `on:input` or `on:change` listeners for an `<input>` element.
pub fn event_target_value<T>(event: &T) -> String
where
T: JsCast,
T: JsCast,
{
event
.unchecked_ref::<web_sys::Event>()
.target()
.unwrap_throw()
.unchecked_into::<web_sys::HtmlInputElement>()
.value()
event
.unchecked_ref::<web_sys::Event>()
.target()
.unwrap_throw()
.unchecked_into::<web_sys::HtmlInputElement>()
.value()
}
/// Helper function to extract `event.target.checked` from an event.
///
/// This is useful in the `on:change` listeners for an `<input type="checkbox">` element.
pub fn event_target_checked(ev: &web_sys::Event) -> bool {
ev.target()
.unwrap()
.unchecked_into::<web_sys::HtmlInputElement>()
.checked()
ev.target()
.unwrap()
.unchecked_into::<web_sys::HtmlInputElement>()
.checked()
}
/// Runs the given function between the next repaint
/// using [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
}
let cb = Closure::once_into_js(cb);
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
let cb = Closure::once_into_js(cb);
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
}
/// Queues the given function during an idle period
/// using [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_idle_callback(cb: impl Fn() + 'static) {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
}
/// Executes the given function after the given duration of time has passed.
@@ -123,21 +121,21 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
}
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
);
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
);
}
/// Handle that is generated by [set_interval] and can be used to clear the interval.
@@ -145,11 +143,11 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
pub struct IntervalHandle(i32);
impl IntervalHandle {
/// Cancels the repeating event to which this refers.
/// See [`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)
pub fn clear(&self) {
window().clear_interval_with_handle(self.0);
}
/// Cancels the repeating event to which this refers.
/// See [`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)
pub fn clear(&self) {
window().clear_interval_with_handle(self.0);
}
}
/// Repeatedly calls the given function, with a delay of the given duration between calls.
@@ -159,26 +157,26 @@ impl IntervalHandle {
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_interval(
cb: impl Fn() + 'static,
duration: Duration,
cb: impl Fn() + 'static,
duration: Duration,
) -> Result<IntervalHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
let handle = window()
.set_interval_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)?;
Ok(IntervalHandle(handle))
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
let handle = window()
.set_interval_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)?;
Ok(IntervalHandle(handle))
}
/// Adds an event listener to the `Window`.
@@ -187,34 +185,34 @@ pub fn set_interval(
instrument(level = "trace", skip_all, fields(event_name = %event_name))
)]
pub fn window_event_listener(
event_name: &str,
cb: impl Fn(web_sys::Event) + 'static,
event_name: &str,
cb: impl Fn(web_sys::Event) + 'static,
) {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
}
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
}
}
if !is_server() {
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
if !is_server() {
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
let cb = Closure::wrap(handler).into_js_value();
_ = window()
.add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
let cb = Closure::wrap(handler).into_js_value();
_ =
window().add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
}
#[doc(hidden)]
/// This exists only to enable type inference on event listeners when in SSR mode.
pub fn ssr_event_listener<E: crate::ev::EventDescriptor + 'static>(
event: E,
event_handler: impl FnMut(E::EventType) + 'static,
event: E,
event_handler: impl FnMut(E::EventType) + 'static,
) {
_ = event;
_ = event_handler;
_ = event;
_ = event_handler;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
//! Exports types for working with MathML elements.
//! 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::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
use super::{HydrationKey, HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
}
}
@@ -178,69 +178,69 @@ macro_rules! generate_math_tags {
}
generate_math_tags![
/// MathML element.
math,
/// MathML element.
mi,
/// MathML element.
mn,
/// MathML element.
mo,
/// MathML element.
ms,
/// MathML element.
mspace,
/// MathML element.
mtext,
/// MathML element.
menclose,
/// MathML element.
merror,
/// MathML element.
mfenced,
/// MathML element.
mfrac,
/// MathML element.
mpadded,
/// MathML element.
mphantom,
/// MathML element.
mroot,
/// MathML element.
mrow,
/// MathML element.
msqrt,
/// MathML element.
mstyle,
/// MathML element.
mmultiscripts,
/// MathML element.
mover,
/// MathML element.
mprescripts,
/// MathML element.
msub,
/// MathML element.
msubsup,
/// MathML element.
msup,
/// MathML element.
munder,
/// MathML element.
munderover,
/// MathML element.
mtable,
/// MathML element.
mtd,
/// MathML element.
mtr,
/// MathML element.
maction,
/// MathML element.
annotation,
/// MathML element.
annotation
- xml,
/// MathML element.
semantics,
/// MathML element.
math,
/// MathML element.
mi,
/// MathML element.
mn,
/// MathML element.
mo,
/// MathML element.
ms,
/// MathML element.
mspace,
/// MathML element.
mtext,
/// MathML element.
menclose,
/// MathML element.
merror,
/// MathML element.
mfenced,
/// MathML element.
mfrac,
/// MathML element.
mpadded,
/// MathML element.
mphantom,
/// MathML element.
mroot,
/// MathML element.
mrow,
/// MathML element.
msqrt,
/// MathML element.
mstyle,
/// MathML element.
mmultiscripts,
/// MathML element.
mover,
/// MathML element.
mprescripts,
/// MathML element.
msub,
/// MathML element.
msubsup,
/// MathML element.
msup,
/// MathML element.
munder,
/// MathML element.
munderover,
/// MathML element.
mtable,
/// MathML element.
mtd,
/// MathML element.
mtr,
/// MathML element.
maction,
/// MathML element.
annotation,
/// MathML element.
annotation
- xml,
/// MathML element.
semantics,
];

View File

@@ -1,8 +1,8 @@
//! Exports types for working with SVG elements.
//! SVG elements.
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
use super::{html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG, HydrationKey};
use super::{ElementDescriptor, HtmlElement};
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
use super::{HydrationKey, HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
use crate::HydrationCtx;
use leptos_reactive::Scope;
#[cfg(all(target_arch = "wasm32", feature = "web"))]

View File

@@ -51,25 +51,25 @@ cfg_if! {
/// A stable identifer within the server-rendering or hydration process.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct HydrationKey {
/// The key of the previous component.
pub previous: String,
/// The element offset within the current component.
pub offset: usize,
/// The key of the previous component.
pub previous: String,
/// The element offset within the current component.
pub offset: usize,
}
impl Display for HydrationKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.previous, self.offset)
}
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.previous, self.offset)
}
}
impl Default for HydrationKey {
fn default() -> Self {
Self {
previous: "0-".to_string(),
offset: 0,
}
fn default() -> Self {
Self {
previous: "0-".to_string(),
offset: 0,
}
}
}
thread_local!(static ID: RefCell<HydrationKey> = Default::default());
@@ -78,65 +78,65 @@ thread_local!(static ID: RefCell<HydrationKey> = Default::default());
pub struct HydrationCtx;
impl HydrationCtx {
/// Get the next `id` without incrementing it.
pub fn peek() -> HydrationKey {
ID.with(|id| id.borrow().clone())
}
/// Increments the current hydration `id` and returns it
pub fn id() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
id.offset = id.offset.wrapping_add(1);
id.clone()
})
}
/// Resets the hydration `id` for the next component, and returns it
pub fn next_component() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
let offset = id.offset;
id.previous.push_str(&offset.to_string());
id.previous.push('-');
id.offset = 0;
id.clone()
})
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub fn reset_id() {
ID.with(|id| *id.borrow_mut() = Default::default());
}
/// Resumes hydration from the provided `id`. Useful for
/// `Suspense` and other fancy things.
pub fn continue_from(id: HydrationKey) {
ID.with(|i| *i.borrow_mut() = id);
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn stop_hydrating() {
IS_HYDRATING.with(|is_hydrating| {
std::mem::take(&mut *is_hydrating.borrow_mut());
})
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn is_hydrating() -> bool {
IS_HYDRATING.with(|is_hydrating| **is_hydrating.borrow())
}
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
#[cfg(debug_assertions)]
return format!("_{id}{}", if closing { 'c' } else { 'o' });
#[cfg(not(debug_assertions))]
{
let _ = closing;
format!("_{id}")
}
/// Get the next `id` without incrementing it.
pub fn peek() -> HydrationKey {
ID.with(|id| id.borrow().clone())
}
/// Increments the current hydration `id` and returns it
pub fn id() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
id.offset = id.offset.wrapping_add(1);
id.clone()
})
}
/// Resets the hydration `id` for the next component, and returns it
pub fn next_component() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
let offset = id.offset;
id.previous.push_str(&offset.to_string());
id.previous.push('-');
id.offset = 0;
id.clone()
})
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub fn reset_id() {
ID.with(|id| *id.borrow_mut() = Default::default());
}
/// Resums hydration from the provided `id`. Usefull for
/// `Suspense` and other fancy things.
pub fn continue_from(id: HydrationKey) {
ID.with(|i| *i.borrow_mut() = id);
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn stop_hydrating() {
IS_HYDRATING.with(|is_hydrating| {
std::mem::take(&mut *is_hydrating.borrow_mut());
})
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn is_hydrating() -> bool {
IS_HYDRATING.with(|is_hydrating| **is_hydrating.borrow())
}
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
#[cfg(debug_assertions)]
return format!("_{id}{}", if closing { 'c' } else { 'o' });
#[cfg(not(debug_assertions))]
{
let _ = closing;
format!("_{id}")
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -44,45 +44,45 @@ macro_rules! debug_warn {
/// Log a string to the console (in the browser)
/// or via `println!()` (if not in the browser).
pub fn console_log(s: &str) {
if is_server() {
println!("{s}");
} else {
web_sys::console::log_1(&JsValue::from_str(s));
}
if is_server() {
println!("{s}");
} else {
web_sys::console::log_1(&JsValue::from_str(s));
}
}
/// Log a warning to the console (in the browser)
/// or via `println!()` (if not in the browser).
pub fn console_warn(s: &str) {
if is_server() {
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
if is_server() {
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
}
/// Log an error to the console (in the browser)
/// or via `println!()` (if not in the browser).
pub fn console_error(s: &str) {
if is_server() {
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
if is_server() {
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
}
/// Log an error to the console (in the browser)
/// or via `println!()` (if not in the browser), but only in a debug build.
pub fn console_debug_warn(s: &str) {
cfg_if! {
if #[cfg(debug_assertions)] {
if is_server() {
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
} else {
let _ = s;
}
}
cfg_if! {
if #[cfg(debug_assertions)] {
if is_server() {
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
} else {
let _ = s;
}
}
}

View File

@@ -1,236 +1,234 @@
use leptos_reactive::Scope;
use std::rc::Rc;
use leptos_reactive::Scope;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::UnwrapThrowExt;
/// Represents the different possible values an attribute node could have.
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly, but it can be useful for defining
/// permissive APIs for certain components.
/// macros use. You usually won't need to interact with it directly.
#[derive(Clone)]
pub enum Attribute {
/// A plain string value.
String(String),
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
Fn(Scope, Rc<dyn Fn() -> Attribute>),
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
Option(Scope, Option<String>),
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
Bool(bool),
/// A plain string value.
String(String),
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
Fn(Scope, Rc<dyn Fn() -> Attribute>),
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
Option(Scope, Option<String>),
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
Bool(bool),
}
impl Attribute {
/// Converts the attribute to its HTML value at that moment, including the attribute name,
/// so it can be rendered on the server.
pub fn as_value_string(&self, attr_name: &'static str) -> String {
match self {
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
value = f();
}
value.as_value_string(attr_name)
}
Attribute::Option(_, value) => value
.as_ref()
.map(|value| format!("{attr_name}=\"{value}\""))
.unwrap_or_default(),
Attribute::Bool(include) => {
if *include {
attr_name.to_string()
} else {
String::new()
}
}
/// Converts the attribute to its HTML value at that moment, including the attribute name,
/// so it can be rendered on the server.
pub fn as_value_string(&self, attr_name: &'static str) -> String {
match self {
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
value = f();
}
value.as_value_string(attr_name)
}
Attribute::Option(_, value) => value
.as_ref()
.map(|value| format!("{attr_name}=\"{value}\""))
.unwrap_or_default(),
Attribute::Bool(include) => {
if *include {
attr_name.to_string()
} else {
String::new()
}
}
}
}
/// Converts the attribute to its HTML value at that moment, not including
/// the attribute name, so it can be rendered on the server.
pub fn as_nameless_value_string(&self) -> Option<String> {
match self {
Attribute::String(value) => Some(value.to_string()),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
value = f();
}
value.as_nameless_value_string()
}
Attribute::Option(_, value) => {
value.as_ref().map(|value| value.to_string())
}
Attribute::Bool(include) => {
if *include {
Some("".to_string())
} else {
None
}
}
/// Converts the attribute to its HTML value at that moment, not including
/// the attribute name, so it can be rendered on the server.
pub fn as_nameless_value_string(&self) -> Option<String> {
match self {
Attribute::String(value) => Some(value.to_string()),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
value = f();
}
value.as_nameless_value_string()
}
Attribute::Option(_, value) => {
value.as_ref().map(|value| value.to_string())
}
Attribute::Bool(include) => {
if *include {
Some("".to_string())
} else {
None
}
}
}
}
}
impl PartialEq for Attribute {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::String(l0), Self::String(r0)) => l0 == r0,
(Self::Fn(_, _), Self::Fn(_, _)) => false,
(Self::Option(_, l0), Self::Option(_, r0)) => l0 == r0,
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
_ => false,
}
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::String(l0), Self::String(r0)) => l0 == r0,
(Self::Fn(_, _), Self::Fn(_, _)) => false,
(Self::Option(_, l0), Self::Option(_, r0)) => l0 == r0,
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
_ => false,
}
}
}
impl std::fmt::Debug for Attribute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
Self::Fn(_, _) => f.debug_tuple("Fn").finish(),
Self::Option(_, arg0) => {
f.debug_tuple("Option").field(arg0).finish()
}
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
}
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
Self::Fn(_, _) => f.debug_tuple("Fn").finish(),
Self::Option(_, arg0) => f.debug_tuple("Option").field(arg0).finish(),
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
}
}
}
/// Converts some type into an [Attribute].
///
/// This is implemented by default for Rust primitive and string types.
pub trait IntoAttribute {
/// Converts the object into an [Attribute].
fn into_attribute(self, cx: Scope) -> Attribute;
/// Helper function for dealing with `Box<dyn IntoAttribute>`.
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute;
/// Converts the object into an [Attribute].
fn into_attribute(self, cx: Scope) -> Attribute;
/// Helper function for dealing with [Box<dyn IntoAttribute>]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute;
}
impl<T: IntoAttribute + 'static> From<T> for Box<dyn IntoAttribute> {
fn from(value: T) -> Self {
Box::new(value)
}
fn from(value: T) -> Self {
Box::new(value)
}
}
impl IntoAttribute for Attribute {
#[inline]
fn into_attribute(self, _: Scope) -> Attribute {
self
}
#[inline]
fn into_attribute(self, _: Scope) -> Attribute {
self
}
#[inline]
fn into_attribute_boxed(self: Box<Self>, _: Scope) -> Attribute {
*self
}
#[inline]
fn into_attribute_boxed(self: Box<Self>, _: Scope) -> Attribute {
*self
}
}
macro_rules! impl_into_attr_boxed {
() => {
#[inline]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
self.into_attribute(cx)
}
};
() => {
#[inline]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
self.into_attribute(cx)
}
};
}
impl IntoAttribute for Option<Attribute> {
fn into_attribute(self, cx: Scope) -> Attribute {
self.unwrap_or(Attribute::Option(cx, None))
}
fn into_attribute(self, cx: Scope) -> Attribute {
self.unwrap_or(Attribute::Option(cx, None))
}
impl_into_attr_boxed! {}
impl_into_attr_boxed! {}
}
impl IntoAttribute for String {
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::String(self)
}
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::String(self)
}
impl_into_attr_boxed! {}
impl_into_attr_boxed! {}
}
impl IntoAttribute for bool {
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::Bool(self)
}
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::Bool(self)
}
impl_into_attr_boxed! {}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Option<String> {
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self)
}
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self)
}
impl_into_attr_boxed! {}
impl_into_attr_boxed! {}
}
impl<T, U> IntoAttribute for T
where
T: Fn() -> U + 'static,
U: IntoAttribute,
T: Fn() -> U + 'static,
U: IntoAttribute,
{
fn into_attribute(self, cx: Scope) -> Attribute {
let modified_fn = Rc::new(move || (self)().into_attribute(cx));
Attribute::Fn(cx, modified_fn)
}
fn into_attribute(self, cx: Scope) -> Attribute {
let modified_fn = Rc::new(move || (self)().into_attribute(cx));
Attribute::Fn(cx, modified_fn)
}
impl_into_attr_boxed! {}
impl_into_attr_boxed! {}
}
impl<T: IntoAttribute> IntoAttribute for (Scope, T) {
fn into_attribute(self, _: Scope) -> Attribute {
self.1.into_attribute(self.0)
}
fn into_attribute(self, _: Scope) -> Attribute {
self.1.into_attribute(self.0)
}
impl_into_attr_boxed! {}
impl_into_attr_boxed! {}
}
impl IntoAttribute for (Scope, Option<Box<dyn IntoAttribute>>) {
fn into_attribute(self, _: Scope) -> Attribute {
match self.1 {
Some(bx) => bx.into_attribute_boxed(self.0),
None => Attribute::Option(self.0, None),
}
fn into_attribute(self, _: Scope) -> Attribute {
match self.1 {
Some(bx) => bx.into_attribute_boxed(self.0),
None => Attribute::Option(self.0, None),
}
}
impl_into_attr_boxed! {}
impl_into_attr_boxed! {}
}
impl IntoAttribute for (Scope, Box<dyn IntoAttribute>) {
fn into_attribute(self, _: Scope) -> Attribute {
self.1.into_attribute_boxed(self.0)
}
fn into_attribute(self, _: Scope) -> Attribute {
self.1.into_attribute_boxed(self.0)
}
impl_into_attr_boxed! {}
impl_into_attr_boxed! {}
}
macro_rules! attr_type {
($attr_type:ty) => {
impl IntoAttribute for $attr_type {
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::String(self.to_string())
}
($attr_type:ty) => {
impl IntoAttribute for $attr_type {
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::String(self.to_string())
}
#[inline]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
self.into_attribute(cx)
}
}
#[inline]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
self.into_attribute(cx)
}
}
impl IntoAttribute for Option<$attr_type> {
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self.map(|n| n.to_string()))
}
impl IntoAttribute for Option<$attr_type> {
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self.map(|n| n.to_string()))
}
#[inline]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
self.into_attribute(cx)
}
}
};
#[inline]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
self.into_attribute(cx)
}
}
};
}
attr_type!(&String);
@@ -255,64 +253,64 @@ attr_type!(char);
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn attribute_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Attribute,
el: &web_sys::Element,
name: Cow<'static, str>,
value: Attribute,
) {
use leptos_reactive::create_render_effect;
match value {
Attribute::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(&el, &name, new.clone());
}
new
});
use leptos_reactive::create_render_effect;
match value {
Attribute::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(&el, &name, new.clone());
}
_ => attribute_expression(el, &name, value),
};
new
});
}
_ => attribute_expression(el, &name, value),
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn attribute_expression(
el: &web_sys::Element,
attr_name: &str,
value: Attribute,
el: &web_sys::Element,
attr_name: &str,
value: Attribute,
) {
match value {
Attribute::String(value) => {
let value = wasm_bindgen::intern(&value);
if attr_name == "inner_html" {
el.set_inner_html(value);
} else {
let attr_name = wasm_bindgen::intern(attr_name);
el.set_attribute(attr_name, value).unwrap_throw();
}
}
Attribute::Option(_, value) => {
if attr_name == "inner_html" {
el.set_inner_html(&value.unwrap_or_default());
} else {
let attr_name = wasm_bindgen::intern(attr_name);
match value {
Some(value) => {
let value = wasm_bindgen::intern(&value);
el.set_attribute(attr_name, value).unwrap_throw();
}
None => el.remove_attribute(attr_name).unwrap_throw(),
}
}
}
Attribute::Bool(value) => {
let attr_name = wasm_bindgen::intern(attr_name);
if value {
el.set_attribute(attr_name, attr_name).unwrap_throw();
} else {
el.remove_attribute(attr_name).unwrap_throw();
}
}
_ => panic!("Remove nested Fn in Attribute"),
match value {
Attribute::String(value) => {
let value = wasm_bindgen::intern(&value);
if attr_name == "inner_html" {
el.set_inner_html(value);
} else {
let attr_name = wasm_bindgen::intern(attr_name);
el.set_attribute(attr_name, value).unwrap_throw();
}
}
Attribute::Option(_, value) => {
if attr_name == "inner_html" {
el.set_inner_html(&value.unwrap_or_default());
} else {
let attr_name = wasm_bindgen::intern(attr_name);
match value {
Some(value) => {
let value = wasm_bindgen::intern(&value);
el.set_attribute(attr_name, value).unwrap_throw();
}
None => el.remove_attribute(attr_name).unwrap_throw(),
}
}
}
Attribute::Bool(value) => {
let attr_name = wasm_bindgen::intern(attr_name);
if value {
el.set_attribute(attr_name, attr_name).unwrap_throw();
} else {
el.remove_attribute(attr_name).unwrap_throw();
}
}
_ => panic!("Remove nested Fn in Attribute"),
}
}

View File

@@ -7,64 +7,63 @@ 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, but it can be useful for defining
/// permissive APIs for certain components.
/// macros use. You usually won't need to interact with it directly.
pub enum Class {
/// Whether the class is present.
Value(bool),
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
Fn(Scope, Box<dyn Fn() -> bool>),
/// Whether the class is present.
Value(bool),
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
Fn(Scope, Box<dyn Fn() -> bool>),
}
/// Converts some type into a [Class].
pub trait IntoClass {
/// Converts the object into a [Class].
fn into_class(self, cx: Scope) -> Class;
/// Converts the object into a [Class].
fn into_class(self, cx: Scope) -> Class;
}
impl IntoClass for bool {
fn into_class(self, _cx: Scope) -> Class {
Class::Value(self)
}
fn into_class(self, _cx: Scope) -> Class {
Class::Value(self)
}
}
impl<T> IntoClass for T
where
T: Fn() -> bool + 'static,
T: Fn() -> bool + 'static,
{
fn into_class(self, cx: Scope) -> Class {
let modified_fn = Box::new(self);
Class::Fn(cx, modified_fn)
}
fn into_class(self, cx: Scope) -> Class {
let modified_fn = Box::new(self);
Class::Fn(cx, modified_fn)
}
}
impl Class {
/// Converts the class to its HTML value at that moment so it can be rendered on the server.
pub fn as_value_string(&self, class_name: &'static str) -> &'static str {
match self {
Class::Value(value) => {
if *value {
class_name
} else {
""
}
}
Class::Fn(_, f) => {
let value = f();
if value {
class_name
} else {
""
}
}
/// Converts the class to its HTML value at that moment so it can be rendered on the server.
pub fn as_value_string(&self, class_name: &'static str) -> &'static str {
match self {
Class::Value(value) => {
if *value {
class_name
} else {
""
}
}
Class::Fn(_, f) => {
let value = f();
if value {
class_name
} else {
""
}
}
}
}
}
impl<T: IntoClass> IntoClass for (Scope, T) {
fn into_class(self, _: Scope) -> Class {
self.1.into_class(self.0)
}
fn into_class(self, _: Scope) -> Class {
self.1.into_class(self.0)
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -72,37 +71,37 @@ use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn class_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Class,
el: &web_sys::Element,
name: Cow<'static, str>,
value: Class,
) {
use leptos_reactive::create_render_effect;
use leptos_reactive::create_render_effect;
let class_list = el.class_list();
match value {
Class::Fn(cx, f) => {
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) && (old.is_some() || new) {
class_expression(&class_list, &name, new)
}
new
});
let class_list = el.class_list();
match value {
Class::Fn(cx, f) => {
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) && (old.is_some() || new) {
class_expression(&class_list, &name, new)
}
Class::Value(value) => class_expression(&class_list, &name, value),
};
new
});
}
Class::Value(value) => class_expression(&class_list, &name, value),
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn class_expression(
class_list: &web_sys::DomTokenList,
class_name: &str,
value: bool,
class_list: &web_sys::DomTokenList,
class_name: &str,
value: bool,
) {
let class_name = wasm_bindgen::intern(class_name);
if value {
class_list.add_1(class_name).unwrap_throw();
} else {
class_list.remove_1(class_name).unwrap_throw();
}
let class_name = wasm_bindgen::intern(class_name);
if value {
class_list.add_1(class_name).unwrap_throw();
} else {
class_list.remove_1(class_name).unwrap_throw();
}
}

View File

@@ -7,54 +7,53 @@ 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, but it can be useful for defining
/// permissive APIs for certain components.
/// macros use. You usually won't need to interact with it directly.
pub enum Property {
/// A static JavaScript value.
Value(JsValue),
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
Fn(Scope, Box<dyn Fn() -> JsValue>),
/// A static JavaScript value.
Value(JsValue),
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
Fn(Scope, Box<dyn Fn() -> JsValue>),
}
/// Converts some type into a [Property].
///
/// This is implemented by default for Rust primitive types, [String] and friends, and [JsValue].
pub trait IntoProperty {
/// Converts the object into a [Property].
fn into_property(self, cx: Scope) -> Property;
/// Converts the object into a [Property].
fn into_property(self, cx: Scope) -> Property;
}
impl<T, U> IntoProperty for T
where
T: Fn() -> U + 'static,
U: Into<JsValue>,
T: Fn() -> U + 'static,
U: Into<JsValue>,
{
fn into_property(self, cx: Scope) -> Property {
let modified_fn = Box::new(move || self().into());
Property::Fn(cx, modified_fn)
}
fn into_property(self, cx: Scope) -> Property {
let modified_fn = Box::new(move || self().into());
Property::Fn(cx, modified_fn)
}
}
impl<T: IntoProperty> IntoProperty for (Scope, T) {
fn into_property(self, _: Scope) -> Property {
self.1.into_property(self.0)
}
fn into_property(self, _: Scope) -> Property {
self.1.into_property(self.0)
}
}
macro_rules! prop_type {
($prop_type:ty) => {
impl IntoProperty for $prop_type {
fn into_property(self, _cx: Scope) -> Property {
Property::Value(self.into())
}
}
($prop_type:ty) => {
impl IntoProperty for $prop_type {
fn into_property(self, _cx: Scope) -> Property {
Property::Value(self.into())
}
}
impl IntoProperty for Option<$prop_type> {
fn into_property(self, _cx: Scope) -> Property {
Property::Value(self.into())
}
}
};
impl IntoProperty for Option<$prop_type> {
fn into_property(self, _cx: Scope) -> Property {
Property::Value(self.into())
}
}
};
}
prop_type!(JsValue);
@@ -82,40 +81,39 @@ use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn property_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Property,
el: &web_sys::Element,
name: Cow<'static, str>,
value: Property,
) {
use leptos_reactive::create_render_effect;
use leptos_reactive::create_render_effect;
match value {
Property::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
if old.as_ref() != Some(&new)
&& !(old.is_none()
&& new == wasm_bindgen::JsValue::UNDEFINED)
{
property_expression(&el, prop_name, new.clone())
}
new
});
match value {
Property::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
if old.as_ref() != Some(&new)
&& !(old.is_none() && new == wasm_bindgen::JsValue::UNDEFINED)
{
property_expression(&el, prop_name, new.clone())
}
Property::Value(value) => {
let prop_name = wasm_bindgen::intern(&name);
property_expression(el, prop_name, value)
}
};
new
});
}
Property::Value(value) => {
let prop_name = wasm_bindgen::intern(&name);
property_expression(el, prop_name, value)
}
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn property_expression(
el: &web_sys::Element,
prop_name: &str,
value: JsValue,
el: &web_sys::Element,
prop_name: &str,
value: JsValue,
) {
js_sys::Reflect::set(el, &JsValue::from_str(prop_name), &value)
.unwrap_throw();
js_sys::Reflect::set(el, &JsValue::from_str(prop_name), &value)
.unwrap_throw();
}

View File

@@ -1,163 +1,91 @@
use crate::{html::ElementDescriptor, HtmlElement};
use leptos_reactive::{create_effect, create_rw_signal, RwSignal, Scope};
use std::cell::Cell;
use leptos_reactive::{create_rw_signal, RwSignal, Scope};
/// Contains a shared reference to a DOM node created while using the `view`
/// Contains a shared reference to a DOM node creating 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 input_ref = NodeRef::<HtmlElement<Input>>::new(cx);
///
/// let on_click = move |_| {
/// let node =
/// input_ref.get().expect("input_ref should be loaded by now");
/// // `node` is strongly typed
/// // it is dereferenced to an `HtmlInputElement` automatically
/// log!("value is {:?}", node.value())
/// };
/// let on_click = move |_| {
/// let node = input_ref
/// .get()
/// .expect("input_ref should be loaded by now");
/// // `node` is strongly typed
/// // it is dereferenced to an `HtmlInputElement` automatically
/// log!("value is {:?}", node.value())
/// };
///
/// view! {
/// cx,
/// <div>
/// // `node_ref` loads the input
/// <input _ref=input_ref type="text"/>
/// // the button consumes it
/// <button on:click=on_click>"Click me"</button>
/// </div>
/// }
/// view! {
/// cx,
/// <div>
/// // `node_ref` loads the input
/// <input _ref=input_ref type="text"/>
/// // the button consumes it
/// <button on:click=on_click>"Click me"</button>
/// </div>
/// }
/// }
/// ```
pub struct NodeRef<T: ElementDescriptor + 'static>(
RwSignal<Option<HtmlElement<T>>>,
);
#[derive(Clone, PartialEq)]
pub struct NodeRef<T: Clone + 'static>(RwSignal<Option<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: Clone + 'static> NodeRef<T> {
/// Creates an empty reference.
pub fn new(cx: Scope) -> Self {
Self(create_rw_signal(cx, None))
}
/// Gets the element that is currently stored in the reference.
///
/// This tracks reactively, so that node references can be used in effects.
/// Initially, the value will be `None`, but once it is loaded the effect
/// will rerun and its value will be `Some(Element)`.
#[track_caller]
pub fn get(&self) -> Option<T> {
self.0.get()
}
#[doc(hidden)]
/// Loads an element into the reference. This tracks reactively,
/// so that effects that use the node reference will rerun once it is loaded,
/// i.e., effects can be forward-declared.
#[track_caller]
pub fn load(&self, node: &T) {
self.0.update(|current| {
if current.is_some() {
crate::debug_warn!(
"You are setting a NodeRef that has already been filled. Its \
possible this is intentional, but its also possible that youre \
accidentally using the same NodeRef for multiple _ref attributes."
);
}
*current = Some(node.clone());
});
}
}
impl<T: ElementDescriptor + 'static> NodeRef<T> {
/// Creates an empty reference.
#[deprecated = "Use `create_node_ref` instead of `NodeRef::new()`."]
pub fn new(cx: Scope) -> Self {
Self(create_rw_signal(cx, None))
}
/// Gets the element that is currently stored in the reference.
///
/// This tracks reactively, so that node references can be used in effects.
/// Initially, the value will be `None`, but once it is loaded the effect
/// will rerun and its value will be `Some(Element)`.
#[track_caller]
pub fn get(&self) -> Option<HtmlElement<T>>
where
T: Clone,
{
self.0.get()
}
#[doc(hidden)]
/// Loads an element into the reference. This tracks reactively,
/// so that effects that use the node reference will rerun once it is loaded,
/// i.e., effects can be forward-declared.
#[track_caller]
pub fn load(&self, node: &HtmlElement<T>)
where
T: Clone,
{
self.0.update(|current| {
if current.is_some() {
crate::debug_warn!(
"You are setting a NodeRef that has already been filled. \
Its possible this is intentional, but its also \
possible that youre accidentally using the same NodeRef \
for multiple _ref attributes."
);
}
*current = Some(node.clone());
});
}
/// Runs the provided closure when the `NodeRef` has been connected
/// with it's [`HtmlElement`].
pub fn on_load<F>(self, cx: Scope, f: F)
where
T: Clone,
F: FnOnce(HtmlElement<T>) + 'static,
{
let f = Cell::new(Some(f));
create_effect(cx, move |_| {
if let Some(node_ref) = self.get() {
f.take().unwrap()(node_ref);
}
});
}
}
impl<T: ElementDescriptor> Clone for NodeRef<T> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<T: ElementDescriptor + 'static> Copy for NodeRef<T> {}
impl<T: Clone + 'static> Copy for NodeRef<T> {}
cfg_if::cfg_if! {
if #[cfg(not(feature = "stable"))] {
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
type Output = Option<HtmlElement<T>>;
impl<T: Clone + 'static> FnOnce<()> for NodeRef<T> {
type Output = Option<T>;
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T: Clone + ElementDescriptor + 'static> FnMut<()> for NodeRef<T> {
impl<T: Clone + 'static> FnMut<()> for NodeRef<T> {
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T: Clone + ElementDescriptor + Clone + 'static> Fn<()> for NodeRef<T> {
impl<T: Clone + 'static> Fn<()> for NodeRef<T> {
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}

View File

@@ -1,7 +1,5 @@
#![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};
@@ -23,19 +21,19 @@ use std::borrow::Cow;
/// ```
pub fn render_to_string<F, N>(f: F) -> String
where
F: FnOnce(Scope) -> N + 'static,
N: IntoView,
F: FnOnce(Scope) -> N + 'static,
N: IntoView,
{
let runtime = leptos_reactive::create_runtime();
HydrationCtx::reset_id();
let runtime = leptos_reactive::create_runtime();
HydrationCtx::reset_id();
let html = leptos_reactive::run_scope(runtime, |cx| {
f(cx).into_view(cx).render_to_string(cx)
});
let html = leptos_reactive::run_scope(runtime, |cx| {
f(cx).into_view(cx).render_to_string(cx)
});
runtime.dispose();
runtime.dispose();
html.into()
html.into()
}
/// Renders a function to a stream of HTML strings.
@@ -51,9 +49,9 @@ where
/// 3) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
pub fn render_to_stream(
view: impl FnOnce(Scope) -> View + 'static,
view: impl FnOnce(Scope) -> View + 'static,
) -> impl Stream<Item = String> {
render_to_stream_with_prefix(view, |_| "".into())
render_to_stream_with_prefix(view, |_| "".into())
}
/// Renders a function to a stream of HTML strings. After the `view` runs, the `prefix` will run with
@@ -71,13 +69,13 @@ pub fn render_to_stream(
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
pub fn render_to_stream_with_prefix(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
) -> impl Stream<Item = String> {
let (stream, runtime, _) =
render_to_stream_with_prefix_undisposed(view, prefix);
runtime.dispose();
stream
let (stream, runtime, _) =
render_to_stream_with_prefix_undisposed(view, prefix);
runtime.dispose();
stream
}
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
@@ -96,10 +94,10 @@ pub fn render_to_stream_with_prefix(
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
pub fn render_to_stream_with_prefix_undisposed(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
render_to_stream_with_prefix_undisposed_with_context(view, prefix, |_cx| {})
render_to_stream_with_prefix_undisposed_with_context(view, prefix, |_cx| {})
}
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
@@ -118,50 +116,50 @@ pub fn render_to_stream_with_prefix_undisposed(
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
pub fn render_to_stream_with_prefix_undisposed_with_context(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
additional_context: impl FnOnce(Scope) + 'static,
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
additional_context: impl FnOnce(Scope) + 'static,
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
HydrationCtx::reset_id();
HydrationCtx::reset_id();
// create the runtime
let runtime = create_runtime();
// create the runtime
let runtime = create_runtime();
let (
(shell, prefix, pending_resources, pending_fragments, serializers),
scope,
_,
) = run_scope_undisposed(runtime, {
move |cx| {
// Add additional context items
additional_context(cx);
// the actual app body/template code
// this does NOT contain any of the data being loaded asynchronously in resources
let shell = view(cx).render_to_string(cx);
let (
(shell, prefix, pending_resources, pending_fragments, serializers),
scope,
_,
) = run_scope_undisposed(runtime, {
move |cx| {
// Add additional context items
additional_context(cx);
// the actual app body/template code
// this does NOT contain any of the data being loaded asynchronously in resources
let shell = view(cx).render_to_string(cx);
let resources = cx.pending_resources();
let pending_resources = serde_json::to_string(&resources).unwrap();
let prefix = prefix(cx);
let resources = cx.pending_resources();
let pending_resources = serde_json::to_string(&resources).unwrap();
let prefix = prefix(cx);
(
shell,
prefix,
pending_resources,
cx.pending_fragments(),
cx.serialization_resolvers(),
)
}
});
let fragments = FuturesUnordered::new();
for (fragment_id, (key_before, fut)) in pending_fragments {
fragments.push(async move { (fragment_id, key_before, fut.await) })
(
shell,
prefix,
pending_resources,
cx.pending_fragments(),
cx.serialization_resolvers(),
)
}
});
// resources and fragments
// stream HTML for each <Suspense/> as it resolves
// TODO can remove id_before_suspense entirely now
let fragments = fragments.map(|(fragment_id, _, html)| {
let fragments = FuturesUnordered::new();
for (fragment_id, (key_before, fut)) in pending_fragments {
fragments.push(async move { (fragment_id, key_before, fut.await) })
}
// resources and fragments
// stream HTML for each <Suspense/> as it resolves
// TODO can remove id_before_suspense entirely now
let fragments = fragments.map(|(fragment_id, _, html)| {
format!(
r#"
<template id="{fragment_id}f">{html}</template>
@@ -187,24 +185,24 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
"#
)
});
// stream data for each Resource as it resolves
let resources = serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
format!(
r#"<script>
// stream data for each Resource as it resolves
let resources = serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
format!(
r#"<script>
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
}} else {{
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
}}
</script>"#,
)
});
)
});
// HTML for the view function and script to store resources
let stream = futures::stream::once(async move {
format!(
r#"
// HTML for the view function and script to store resources
let stream = futures::stream::once(async move {
format!(
r#"
{prefix}
{shell}
<script>
@@ -213,269 +211,258 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
__LEPTOS_RESOURCE_RESOLVERS = new Map();
</script>
"#
)
})
// TODO these should be combined again in a way that chains them appropriately
// such that individual resources can resolve before all fragments are done
.chain(fragments)
.chain(resources);
)
})
// TODO these should be combined again in a way that chains them appropriately
// such that individual resources can resolve before all fragments are done
.chain(fragments)
.chain(resources);
(stream, runtime, scope)
(stream, runtime, scope)
}
impl View {
/// Consumes the node and renders it into an HTML string.
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
self.render_to_string_helper()
}
/// Consumes the node and renders it into an HTML string.
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
self.render_to_string_helper()
}
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
match self {
View::Text(node) => node.content,
View::Component(node) => {
let content = || {
node.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("")
};
cfg_if! {
if #[cfg(debug_assertions)] {
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
HydrationCtx::to_string(&node.id, false),
content(),
HydrationCtx::to_string(&node.id, true),
name = to_kebab_case(&node.name)
).into()
} else {
format!(
r#"{}<!--hk={}-->"#,
content(),
HydrationCtx::to_string(&node.id, true)
).into()
}
}
}
View::Suspense(id, node) => format!(
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
View::CoreComponent(node).render_to_string_helper()
)
.into(),
View::CoreComponent(node) => {
let (id, name, wrap, content) = match node {
CoreComponent::Unit(u) => (
u.id.clone(),
"",
false,
Box::new(move || {
#[cfg(debug_assertions)]
{
format!(
"<!--hk={}|leptos-unit-->",
HydrationCtx::to_string(&u.id, true)
)
.into()
}
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
match self {
View::Text(node) => node.content,
View::Component(node) => {
let content = || {
node
.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("")
};
cfg_if! {
if #[cfg(debug_assertions)] {
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
HydrationCtx::to_string(&node.id, false),
content(),
HydrationCtx::to_string(&node.id, true),
name = to_kebab_case(&node.name)
).into()
} else {
format!(
r#"{}<!--hk={}-->"#,
content(),
HydrationCtx::to_string(&node.id, true)
).into()
}
}
}
View::Suspense(id, node) => format!(
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
View::CoreComponent(node).render_to_string_helper()
)
.into(),
View::CoreComponent(node) => {
let (id, name, wrap, content) = match node {
CoreComponent::Unit(u) => (
u.id.clone(),
"",
false,
Box::new(move || {
#[cfg(debug_assertions)]
{
format!(
"<!--hk={}|leptos-unit-->",
HydrationCtx::to_string(&u.id, true)
)
.into()
}
#[cfg(not(debug_assertions))]
format!(
"<!--hk={}-->",
HydrationCtx::to_string(&u.id, true)
)
.into()
})
as Box<dyn FnOnce() -> Cow<'static, str>>,
),
CoreComponent::DynChild(node) => {
let child = node.child.take();
(
node.id,
"dyn-child",
true,
Box::new(move || {
if let Some(child) = *child {
// On debug builds, `DynChild` has two marker nodes,
// so there is no way for the text to be merged with
// surrounding text when the browser parses the HTML,
// but in release, `DynChild` only has a trailing marker,
// and the browser automatically merges the dynamic text
// into one single node, so we need to artificially make the
// browser create the dynamic text as it's own text node
if let View::Text(t) = child {
if !cfg!(debug_assertions) {
format!("<!>{}", t.content).into()
} else {
t.content
}
} else {
child.render_to_string_helper()
}
} else {
"".into()
}
})
as Box<dyn FnOnce() -> Cow<'static, str>>,
)
#[cfg(not(debug_assertions))]
format!("<!--hk={}-->", HydrationCtx::to_string(&u.id, true))
.into()
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
),
CoreComponent::DynChild(node) => {
let child = node.child.take();
(
node.id,
"dyn-child",
true,
Box::new(move || {
if let Some(child) = *child {
// On debug builds, `DynChild` has two marker nodes,
// so there is no way for the text to be merged with
// surrounding text when the browser parses the HTML,
// but in release, `DynChild` only has a trailing marker,
// and the browser automatically merges the dynamic text
// into one single node, so we need to artificially make the
// browser create the dynamic text as it's own text node
if let View::Text(t) = child {
if !cfg!(debug_assertions) {
format!("<!>{}", t.content).into()
} else {
t.content
}
CoreComponent::Each(node) => {
let children = node.children.take();
(
node.id,
"each",
true,
Box::new(move || {
children
.into_iter()
.flatten()
.map(|node| {
let id = node.id;
} else {
child.render_to_string_helper()
}
} else {
"".into()
}
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
)
}
CoreComponent::Each(node) => {
let children = node.children.take();
(
node.id,
"each",
true,
Box::new(move || {
children
.into_iter()
.flatten()
.map(|node| {
let id = node.id;
let content = || {
node.child.render_to_string_helper()
};
let content = || node.child.render_to_string_helper();
#[cfg(debug_assertions)]
{
format!(
#[cfg(debug_assertions)]
{
format!(
"<!--hk={}|leptos-each-item-start-->{}<!\
--hk={}|leptos-each-item-end-->",
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(&id, true),
)
}
#[cfg(not(debug_assertions))]
format!(
"{}<!--hk={}-->",
content(),
HydrationCtx::to_string(&id, true)
)
})
.join("")
.into()
})
as Box<dyn FnOnce() -> Cow<'static, str>>,
)
}
};
if wrap {
cfg_if! {
if #[cfg(debug_assertions)] {
format!(
r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(&id, true),
).into()
} else {
let _ = name;
#[cfg(not(debug_assertions))]
format!(
"{}<!--hk={}-->",
content(),
HydrationCtx::to_string(&id, true)
)
})
.join("")
.into()
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
)
}
};
format!(
r#"{}<!--hk={}-->"#,
content(),
HydrationCtx::to_string(&id, true)
).into()
}
}
} else {
content()
}
if wrap {
cfg_if! {
if #[cfg(debug_assertions)] {
format!(
r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(&id, true),
).into()
} else {
let _ = name;
format!(
r#"{}<!--hk={}-->"#,
content(),
HydrationCtx::to_string(&id, true)
).into()
}
View::Element(el) => {
if let Some(prerendered) = el.prerendered {
prerendered
} else {
let tag_name = el.name;
}
} else {
content()
}
}
View::Element(el) => {
if let Some(prerendered) = el.prerendered {
prerendered
} else {
let tag_name = el.name;
let mut inner_html = None;
let mut inner_html = None;
let attrs = el
.attrs
.into_iter()
.filter_map(
|(name, value)| -> Option<Cow<'static, str>> {
if value.is_empty() {
Some(format!(" {name}").into())
} else if name == "inner_html" {
inner_html = Some(value);
None
} else {
Some(
format!(
let attrs = el
.attrs
.into_iter()
.filter_map(|(name, value)| -> Option<Cow<'static, str>> {
if value.is_empty() {
Some(format!(" {name}").into())
} else if name == "inner_html" {
inner_html = Some(value);
None
} else {
Some(
format!(
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
.into(),
)
}
},
)
.join("");
.into(),
)
}
})
.join("");
if el.is_void {
format!("<{tag_name}{attrs}/>").into()
} else if let Some(inner_html) = inner_html {
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>")
.into()
} else {
let children = el
.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("");
if el.is_void {
format!("<{tag_name}{attrs}/>").into()
} else if let Some(inner_html) = inner_html {
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>").into()
} else {
let children = el
.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("");
format!("<{tag_name}{attrs}>{children}</{tag_name}>")
.into()
}
}
}
View::Transparent(_) => Default::default(),
format!("<{tag_name}{attrs}>{children}</{tag_name}>").into()
}
}
}
View::Transparent(_) => Default::default(),
}
}
}
#[cfg(debug_assertions)]
fn to_kebab_case(name: &str) -> String {
if name.is_empty() {
return String::new();
}
if name.is_empty() {
return String::new();
}
let mut new_name = String::with_capacity(name.len() + 8);
let mut new_name = String::with_capacity(name.len() + 8);
let mut chars = name.chars();
let mut chars = name.chars();
new_name.push(
chars
.next()
.map(|mut c| {
if c.is_ascii() {
c.make_ascii_lowercase();
}
c
})
.unwrap(),
);
for mut char in chars {
if char.is_ascii_uppercase() {
char.make_ascii_lowercase();
new_name.push('-');
new_name.push(
chars
.next()
.map(|mut c| {
if c.is_ascii() {
c.make_ascii_lowercase();
}
new_name.push(char);
c
})
.unwrap(),
);
for mut char in chars {
if char.is_ascii_uppercase() {
char.make_ascii_lowercase();
new_name.push('-');
}
new_name
new_name.push(char);
}
new_name
}
#[doc(hidden)]
pub fn escape_attr<T>(value: &T) -> Cow<'_, str>
where
T: AsRef<str>,
T: AsRef<str>,
{
html_escape::encode_double_quoted_attribute(value)
html_escape::encode_double_quoted_attribute(value)
}

View File

@@ -7,39 +7,39 @@ use std::{any::Any, fmt, rc::Rc};
pub struct Transparent(Rc<dyn Any>);
impl Transparent {
/// Creates a new wrapper for this data.
pub fn new<T>(value: T) -> Self
where
T: 'static,
{
Self(Rc::new(value))
}
/// Creates a new wrapper for this data.
pub fn new<T>(value: T) -> Self
where
T: 'static,
{
Self(Rc::new(value))
}
/// Returns some reference to the inner value if it is of type `T`, or `None` if it isn't.
pub fn downcast_ref<T>(&self) -> Option<&T>
where
T: 'static,
{
self.0.downcast_ref()
}
/// Returns some reference to the inner value if it is of type `T`, or `None` if it isn't.
pub fn downcast_ref<T>(&self) -> Option<&T>
where
T: 'static,
{
self.0.downcast_ref()
}
}
impl fmt::Debug for Transparent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Transparent").finish()
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Transparent").finish()
}
}
impl PartialEq for Transparent {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(&self.0, &other.0)
}
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(&self.0, &other.0)
}
}
impl Eq for Transparent {}
impl IntoView for Transparent {
fn into_view(self, _: Scope) -> View {
View::Transparent(self)
}
fn into_view(self, _: Scope) -> View {
View::Transparent(self)
}
}

View File

@@ -8,10 +8,9 @@ use proc_macro_error::ResultExt;
use quote::{format_ident, ToTokens, TokenStreamExt};
use std::collections::HashSet;
use syn::{
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute,
FnArg, GenericArgument, ItemFn, LitStr, Meta, MetaList, MetaNameValue,
NestedMeta, Pat, PatIdent, Path, PathArguments, ReturnType, Type, TypePath,
Visibility,
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument,
ItemFn, LitStr, Meta, MetaList, MetaNameValue, NestedMeta, Pat, PatIdent, Path, PathArguments,
ReturnType, Type, TypePath, Visibility,
};
pub struct Model {
@@ -63,8 +62,7 @@ impl Parse for Model {
item.sig.inputs.iter_mut().for_each(|arg| {
if let FnArg::Typed(ty) = arg {
drain_filter(&mut ty.attrs, |attr| {
attr.path == parse_quote!(doc)
|| attr.path == parse_quote!(prop)
attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop)
});
}
});
@@ -93,10 +91,7 @@ impl Parse for Model {
// implemented manually because Vec::drain_filter is nightly only
// follows std recommended parallel
fn drain_filter<T>(
vec: &mut Vec<T>,
mut some_predicate: impl FnMut(&mut T) -> bool,
) {
fn drain_filter<T>(vec: &mut Vec<T>, mut some_predicate: impl FnMut(&mut T) -> bool) {
let mut i = 0;
while i < vec.len() {
if some_predicate(&mut vec[i]) {
@@ -144,33 +139,32 @@ impl ToTokens for Model {
let prop_names = prop_names(props);
let builder_name_doc = LitStr::new(
&format!("Props for the [`{name}`] component."),
name.span(),
);
let builder_name_doc =
LitStr::new(&format!("Props for the [`{name}`] component."), name.span());
let component_fn_prop_docs = generate_component_fn_prop_docs(props);
let (tracing_instrument_attr, tracing_span_expr, tracing_guard_expr) =
if cfg!(feature = "tracing") {
(
quote! {
#[cfg_attr(
debug_assertions,
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
)]
},
quote! {
let span = ::leptos::leptos_dom::tracing::Span::current();
},
quote! {
#[cfg(debug_assertions)]
let _guard = span.entered();
},
)
} else {
(quote! {}, quote! {}, quote! {})
};
let (tracing_instrument_attr, tracing_span_expr, tracing_guard_expr) = if cfg!(
feature = "tracing"
) {
(
quote! {
#[cfg_attr(
debug_assertions,
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
)]
},
quote! {
let span = ::leptos::leptos_dom::tracing::Span::current();
},
quote! {
#[cfg(debug_assertions)]
let _guard = span.entered();
},
)
} else {
(quote! {}, quote! {}, quote! {})
};
let component = if *is_transparent {
quote! {
@@ -178,7 +172,7 @@ impl ToTokens for Model {
}
} else {
quote! {
::leptos::leptos_dom::Component::new(
::leptos::Component::new(
stringify!(#name),
move |cx| {
#tracing_guard_expr
@@ -255,16 +249,11 @@ impl Prop {
.attrs
.iter()
.enumerate()
.filter_map(|(i, attr)| {
PropOpt::from_attribute(attr).map(|opt| (i, opt))
})
.filter_map(|(i, attr)| PropOpt::from_attribute(attr).map(|opt| (i, opt)))
.fold(HashSet::new(), |mut acc, cur| {
// Make sure opts aren't repeated
if acc.intersection(&cur.1).next().is_some() {
abort!(
typed.attrs[cur.0],
"`#[prop]` options are repeated"
);
abort!(typed.attrs[cur.0], "`#[prop]` options are repeated");
}
acc.extend(cur.1);
@@ -273,13 +262,10 @@ impl Prop {
});
// Make sure conflicting options are not present
if prop_opts.contains(&PropOpt::Optional)
&& prop_opts.contains(&PropOpt::OptionalNoStrip)
{
if prop_opts.contains(&PropOpt::Optional) && prop_opts.contains(&PropOpt::OptionalNoStrip) {
abort!(
typed,
"`optional` and `optional_no_strip` options are mutually \
exclusive"
"`optional` and `optional_no_strip` options are mutually exclusive"
);
} else if prop_opts.contains(&PropOpt::Optional)
&& prop_opts.contains(&PropOpt::StripOption)
@@ -293,8 +279,7 @@ impl Prop {
{
abort!(
typed,
"`optional_no_strip` and `strip_option` options are mutually \
exclusive"
"`optional_no_strip` and `strip_option` options are mutually exclusive"
);
}
@@ -303,8 +288,8 @@ impl Prop {
} else {
abort!(
typed.pat,
"only `prop: bool` style types are allowed within the \
`#[component]` macro"
"only `prop: bool` style types are allowed within the `#[component]` \
macro"
);
};
@@ -348,8 +333,7 @@ impl Docs {
.iter()
.enumerate()
.map(|(idx, attr)| {
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) =
attr.parse_meta().unwrap()
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) = attr.parse_meta().unwrap()
{
let doc_str = quote!(#doc);
@@ -384,8 +368,7 @@ impl Docs {
.0
.iter()
.map(|attr| {
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) =
attr.parse_meta().unwrap()
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) = attr.parse_meta().unwrap()
{
let mut doc_str = quote!(#doc).to_string();
@@ -420,17 +403,15 @@ enum PropOpt {
impl PropOpt {
fn from_attribute(attr: &Attribute) -> Option<HashSet<Self>> {
const ABORT_OPT_MESSAGE: &str =
"only `optional`, `optional_no_strip`, `strip_option`, `default` \
and `into` are allowed as arguments to `#[prop()]`";
const ABORT_OPT_MESSAGE: &str = "only `optional`, `optional_no_strip`, \
`strip_option`, `default` and `into` are \
allowed as arguments to `#[prop()]`";
if attr.path != parse_quote!(prop) {
return None;
}
if let Meta::List(MetaList { nested, .. }) =
attr.parse_meta().unwrap_or_abort()
{
if let Meta::List(MetaList { nested, .. }) = attr.parse_meta().unwrap_or_abort() {
Some(
nested
.iter()
@@ -492,8 +473,7 @@ struct TypedBuilderOpts {
impl TypedBuilderOpts {
fn from_opts(opts: &HashSet<PropOpt>, is_ty_option: bool) -> Self {
Self {
default: opts.contains(&PropOpt::Optional)
|| opts.contains(&PropOpt::OptionalNoStrip),
default: opts.contains(&PropOpt::Optional) || opts.contains(&PropOpt::OptionalNoStrip),
default_with_value: opts.iter().find_map(|p| match p {
PropOpt::OptionalWithDefault(v) => Some(v.to_owned()),
_ => None,
@@ -551,8 +531,7 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
ty,
} = prop;
let builder_attrs =
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);
@@ -587,8 +566,7 @@ fn generate_component_fn_prop_docs(props: &[Prop]) -> TokenStream {
let optional_prop_docs = props
.iter()
.filter(|Prop { prop_opts, .. }| {
prop_opts.contains(&PropOpt::Optional)
|| prop_opts.contains(&PropOpt::OptionalNoStrip)
prop_opts.contains(&PropOpt::Optional) || prop_opts.contains(&PropOpt::OptionalNoStrip)
})
.map(|p| prop_to_doc(p, PropDocStyle::List))
.collect::<TokenStream>();
@@ -635,8 +613,8 @@ fn is_option(ty: &Type) -> bool {
fn unwrap_option(ty: &Type) -> Option<Type> {
const STD_OPTION_MSG: &str =
"make sure you're not shadowing the `std::option::Option` type that \
is automatically imported from the standard prelude";
"make sure you're not shadowing the `std::option::Option` type that is \
automatically imported from the standard prelude";
if let Type::Path(TypePath {
path: Path { segments, .. },
@@ -645,9 +623,9 @@ fn unwrap_option(ty: &Type) -> Option<Type> {
{
if let [first] = &segments.iter().collect::<Vec<_>>()[..] {
if first.ident == "Option" {
if let PathArguments::AngleBracketed(
AngleBracketedGenericArguments { args, .. },
) = &first.arguments
if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
args, ..
}) = &first.arguments
{
if let [first] = &args.iter().collect::<Vec<_>>()[..] {
if let GenericArgument::Type(ty) = first {
@@ -728,11 +706,7 @@ fn prop_to_doc(
&if !prop_opts.contains(&PropOpt::Into) {
format!("- **{}**: [`{}`]", quote!(#name), pretty_ty)
} else {
format!(
"- **{}**: `impl`[`Into<{}>`]",
quote!(#name),
pretty_ty
)
format!("- **{}**: `impl`[`Into<{}>`]", quote!(#name), pretty_ty)
},
name.ident.span(),
);

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;
use syn::{parse_macro_input, DeriveInput};
use syn_rsx::{parse, NodeElement};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -19,10 +19,7 @@ pub(crate) enum Mode {
impl Default for Mode {
fn default() -> Self {
if cfg!(feature = "hydrate")
|| cfg!(feature = "csr")
|| cfg!(feature = "web")
{
if cfg!(feature = "hydrate") || cfg!(feature = "csr") || cfg!(feature = "web") {
Mode::Client
} else {
Mode::Ssr
@@ -34,6 +31,7 @@ mod params;
mod view;
use view::render_view;
mod component;
mod props;
mod server;
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
@@ -120,7 +118,7 @@ mod server;
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! {
/// cx,
/// <button on:click=|ev| {
/// <button on:click=|ev: web_sys::MouseEvent| {
/// log::debug!("click event: {ev:#?}");
/// }>
/// "Click me"
@@ -215,8 +213,8 @@ mod server;
/// ```
///
/// 9. You can add the same class to every element in the view by passing in a special
/// `class = {/* ... */},` argument after `cx, `. This is useful for injecting a class
/// provided by a scoped styling library.
/// `class = {/* ... */}` argument after `cx, `. This is useful for injecting a class
/// providing by a scoped styling library.
/// ```rust
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
@@ -259,9 +257,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| set_value(0);
/// let decrement = move |_ev| set_value.update(|value| *value -= 1);
/// let increment = move |_ev| set_value.update(|value| *value += 1);
/// 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);
///
/// // this JSX is compiled to an HTML template string for performance
/// view! {
@@ -284,33 +282,22 @@ pub fn view(tokens: TokenStream) -> TokenStream {
let mut tokens = tokens.into_iter();
let (cx, comma) = (tokens.next(), tokens.next());
match (cx, comma) {
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct)))
if punct.as_char() == ',' =>
{
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct))) if punct.as_char() == ',' => {
let first = tokens.next();
let second = tokens.next();
let third = tokens.next();
let fourth = tokens.next();
let global_class = match (&first, &second) {
(Some(TokenTree::Ident(first)), Some(TokenTree::Punct(eq)))
if *first == "class" && eq.as_char() == '=' =>
let global_class = match (&first, &second, &third, &fourth) {
(
Some(TokenTree::Ident(first)),
Some(TokenTree::Punct(eq)),
Some(val),
Some(TokenTree::Punct(comma)),
) if *first == "class"
&& eq.to_string() == '='.to_string()
&& comma.to_string() == ','.to_string() =>
{
match &fourth {
Some(TokenTree::Punct(comma))
if comma.as_char() == ',' =>
{
third.clone()
}
_ => {
let error_msg = concat!(
"To create a scope class with the view! macro \
you must put a comma `,` after the value.\n",
"e.g., view!{cx, class=\"my-class\", \
<div>...</div>}"
);
panic!("{error_msg}")
}
}
Some(val.clone())
}
_ => None,
};
@@ -336,10 +323,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
.into()
}
_ => {
panic!(
"view! macro needs a context and RSX: e.g., view! {{ cx, \
<div>...</div> }}"
)
panic!("view! macro needs a context and RSX: e.g., view! {{ cx, <div>...</div> }}")
}
}
}
@@ -364,34 +348,33 @@ pub fn view(tokens: TokenStream) -> TokenStream {
///
/// #[component]
/// fn HelloComponent(
/// cx: Scope,
/// /// The user's name.
/// name: String,
/// /// The user's age.
/// age: u8,
/// cx: Scope,
/// /// The user's name.
/// name: String,
/// /// The user's age.
/// age: u8
/// ) -> impl IntoView {
/// // create the signals (reactive values) that will update the UI
/// let (age, set_age) = create_signal(cx, age);
/// // increase `age` by 1 every second
/// set_interval(
/// move || set_age.update(|age| *age += 1),
/// Duration::from_secs(1),
/// );
///
/// // return the user interface, which will be automatically updated
/// // when signal values change
/// view! { cx,
/// <p>"Your name is " {name} " and you are " {age} " years old."</p>
/// }
/// // create the signals (reactive values) that will update the UI
/// let (age, set_age) = create_signal(cx, age);
/// // increase `age` by 1 every second
/// set_interval(move || {
/// set_age.update(|age| *age += 1)
/// }, Duration::from_secs(1));
///
/// // return the user interface, which will be automatically updated
/// // when signal values change
/// view! { cx,
/// <p>"Your name is " {name} " and you are " {age} " years old."</p>
/// }
/// }
///
/// #[component]
/// fn App(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <main>
/// <HelloComponent name="Greg".to_string() age=32/>
/// </main>
/// }
/// view! { cx,
/// <main>
/// <HelloComponent name="Greg".to_string() age=32/>
/// </main>
/// }
/// }
/// ```
///
@@ -416,15 +399,11 @@ pub fn view(tokens: TokenStream) -> TokenStream {
///
/// // PascalCase: Generated component will be called MyComponent
/// #[component]
/// fn MyComponent(cx: Scope) -> impl IntoView {
/// todo!()
/// }
/// fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
///
/// // snake_case: Generated component will be called MySnakeCaseComponent
/// #[component]
/// fn my_snake_case_component(cx: Scope) -> impl IntoView {
/// todo!()
/// }
/// fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
/// ```
///
/// 3. The macro generates a type `ComponentProps` for every `Component` (so, `HomePage` generates `HomePageProps`,
@@ -437,28 +416,22 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// use component::{MyComponent, MyComponentProps};
///
/// mod component {
/// use leptos::*;
/// use leptos::*;
///
/// #[component]
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
/// todo!()
/// }
/// #[component]
/// pub fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
/// }
/// ```
/// ```
/// # use leptos::*;
///
/// use snake_case_component::{
/// MySnakeCaseComponent, MySnakeCaseComponentProps,
/// };
/// use snake_case_component::{MySnakeCaseComponent, MySnakeCaseComponentProps};
///
/// mod snake_case_component {
/// use leptos::*;
/// use leptos::*;
///
/// #[component]
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView {
/// todo!()
/// }
/// #[component]
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
/// }
/// ```
///
@@ -467,8 +440,6 @@ 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!()
@@ -478,14 +449,10 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// ```
/// // ✅ Do this instead
/// # use leptos::*;
/// use leptos::html::Div;
///
/// #[component]
/// fn MyComponent<T>(cx: Scope, render_prop: T) -> impl IntoView
/// where
/// T: Fn() -> HtmlElement<Div>,
/// {
/// todo!()
/// where T: Fn() -> HtmlElement<Div> {
/// todo!()
/// }
/// ```
///
@@ -498,26 +465,26 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// # use leptos::*;
/// #[component]
/// fn ComponentWithChildren(cx: Scope, children: Children) -> impl IntoView {
/// view! {
/// cx,
/// <ul>
/// {children(cx)
/// .nodes
/// .into_iter()
/// .map(|child| view! { cx, <li>{child}</li> })
/// .collect::<Vec<_>>()}
/// </ul>
/// }
/// view! {
/// cx,
/// <ul>
/// {children(cx)
/// .nodes
/// .into_iter()
/// .map(|child| view! { cx, <li>{child}</li> })
/// .collect::<Vec<_>>()}
/// </ul>
/// }
/// }
///
/// #[component]
/// fn WrapSomeChildren(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <ComponentWithChildren>
/// "Ooh, look at us!"
/// <span>"We're being projected!"</span>
/// </ComponentWithChildren>
/// }
/// view! { cx,
/// <ComponentWithChildren>
/// "Ooh, look at us!"
/// <span>"We're being projected!"</span>
/// </ComponentWithChildren>
/// }
/// }
/// ```
///
@@ -539,27 +506,30 @@ pub fn view(tokens: TokenStream) -> TokenStream {
///
/// #[component]
/// pub fn MyComponent(
/// cx: Scope,
/// #[prop(into)] name: String,
/// #[prop(optional)] optional_value: Option<i32>,
/// #[prop(optional_no_strip)] optional_no_strip: Option<i32>,
/// cx: Scope,
/// #[prop(into)]
/// name: String,
/// #[prop(optional)]
/// optional_value: Option<i32>,
/// #[prop(optional_no_strip)]
/// optional_no_strip: Option<i32>
/// ) -> impl IntoView {
/// // whatever UI you need
/// // whatever UI you need
/// }
///
/// #[component]
/// #[component]
/// pub fn App(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <MyComponent
/// name="Greg" // automatically converted to String with `.into()`
/// optional_value=42 // received as `Some(42)`
/// optional_no_strip=Some(42) // received as `Some(42)`
/// />
/// <MyComponent
/// name="Bob" // automatically converted to String with `.into()`
/// // optional values can both be omitted, and received as `None`
/// />
/// }
/// view! { cx,
/// <MyComponent
/// name="Greg" // automatically converted to String with `.into()`
/// optional_value=42 // received as `Some(42)`
/// optional_no_strip=Some(42) // received as `Some(42)`
/// />
/// <MyComponent
/// name="Bob" // automatically converted to String with `.into()`
/// // optional values can both be omitted, and received as `None`
/// />
/// }
/// }
/// ```
#[proc_macro_error::proc_macro_error]
@@ -632,9 +602,7 @@ 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 [`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`
/// - **Arguments must be implement [serde::Serialize].** 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
@@ -648,18 +616,26 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
}
}
/// 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(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
#[proc_macro_derive(Params, attributes(params))]
pub fn params_derive(
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
pub fn params_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = syn::parse(input).unwrap();
params::impl_params(&ast)
}
pub(crate) fn is_component_node(node: &NodeElement) -> bool {
node.name
.to_string()
.starts_with(|c: char| c.is_ascii_uppercase())
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)
}

1269
leptos_macro/src/props.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -23,10 +23,7 @@ fn fn_arg_is_cx(f: &syn::FnArg) -> bool {
}
}
pub fn server_macro_impl(
args: proc_macro::TokenStream,
s: TokenStream2,
) -> Result<TokenStream2> {
pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Result<TokenStream2> {
let ServerFnName {
struct_name,
prefix,
@@ -35,8 +32,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::leptos_server::Encoding::Cbor },
Encoding::Url => quote! { ::leptos::leptos_server::Encoding::Url },
Encoding::Cbor => quote! { ::leptos::Encoding::Cbor },
Encoding::Url => quote! { ::leptos::Encoding::Url },
};
let body = syn::parse::<ServerFnBody>(s.into())?;
@@ -60,21 +57,17 @@ pub fn server_macro_impl(
let fields = body.inputs.iter().filter(|f| !fn_arg_is_cx(f)).map(|f| {
let typed_arg = match f {
FnArg::Receiver(_) => {
panic!("cannot use receiver types in server function macro")
}
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
FnArg::Typed(t) => t,
};
quote! { pub #typed_arg }
});
let cx_arg = body.inputs.iter().next().and_then(|f| {
if fn_arg_is_cx(f) {
Some(f)
} else {
None
}
});
let cx_arg = body
.inputs
.iter()
.next()
.and_then(|f| if fn_arg_is_cx(f) { Some(f) } else { None });
let cx_assign_statement = if let Some(FnArg::Typed(arg)) = cx_arg {
if let Pat::Ident(id) = &*arg.pat {
quote! {
@@ -95,9 +88,7 @@ pub fn server_macro_impl(
let fn_args = body.inputs.iter().map(|f| {
let typed_arg = match f {
FnArg::Receiver(_) => {
panic!("cannot use receiver types in server function macro")
}
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
FnArg::Typed(t) => t,
};
let is_cx = fn_arg_is_cx(f);
@@ -133,14 +124,10 @@ pub fn server_macro_impl(
let output_ty = if let syn::Type::Path(pat) = &return_ty {
if pat.path.segments[0].ident == "Result" {
if let PathArguments::AngleBracketed(args) =
&pat.path.segments[0].arguments
{
if let PathArguments::AngleBracketed(args) = &pat.path.segments[0].arguments {
&args.args[0]
} else {
panic!(
"server functions should return Result<T, ServerFnError>"
);
panic!("server functions should return Result<T, ServerFnError>");
}
} else {
panic!("server functions should return Result<T, ServerFnError>");
@@ -166,7 +153,7 @@ pub fn server_macro_impl(
#url
}
fn encoding() -> ::leptos::leptos_server::Encoding {
fn encoding() -> ::leptos::Encoding {
#encoding
}
@@ -192,7 +179,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::leptos_server::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
::leptos::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
}
})
}

View File

@@ -1,8 +1,9 @@
use crate::{is_component_node, Mode};
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName, NodeValueExpr};
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName};
use crate::{is_component_node, Mode};
#[derive(Clone, Copy)]
enum TagType {
@@ -148,39 +149,32 @@ pub(crate) fn render_view(
global_class: Option<&TokenTree>,
) -> TokenStream {
if mode == Mode::Ssr {
match nodes.len() {
0 => {
let span = Span::call_site();
quote_spanned! {
span => leptos::Unit
}
if nodes.is_empty() {
let span = Span::call_site();
quote_spanned! {
span => leptos::Unit
}
1 => root_node_to_tokens_ssr(cx, &nodes[0], global_class),
_ => fragment_to_tokens_ssr(
cx,
Span::call_site(),
nodes,
global_class,
),
} else if nodes.len() == 1 {
root_node_to_tokens_ssr(cx, &nodes[0], global_class)
} else {
fragment_to_tokens_ssr(cx, Span::call_site(), nodes, global_class)
}
} else if nodes.is_empty() {
let span = Span::call_site();
quote_spanned! {
span => leptos::Unit
}
} else if nodes.len() == 1 {
node_to_tokens(cx, &nodes[0], TagType::Unknown, global_class)
} else {
match nodes.len() {
0 => {
let span = Span::call_site();
quote_spanned! {
span => leptos::Unit
}
}
1 => node_to_tokens(cx, &nodes[0], TagType::Unknown, global_class),
_ => fragment_to_tokens(
cx,
Span::call_site(),
nodes,
true,
TagType::Unknown,
global_class,
),
}
fragment_to_tokens(
cx,
Span::call_site(),
nodes,
true,
TagType::Unknown,
global_class,
)
}
}
@@ -190,17 +184,14 @@ fn root_node_to_tokens_ssr(
global_class: Option<&TokenTree>,
) -> TokenStream {
match node {
Node::Fragment(fragment) => fragment_to_tokens_ssr(
cx,
Span::call_site(),
&fragment.children,
global_class,
),
Node::Fragment(fragment) => {
fragment_to_tokens_ssr(cx, Span::call_site(), &fragment.children, global_class)
}
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
Node::Text(node) => {
let value = node.value.as_ref();
quote! {
leptos::leptos_dom::html::text(#value)
leptos::text(#value)
}
}
Node::Block(node) => {
@@ -210,9 +201,7 @@ fn root_node_to_tokens_ssr(
#value
}
}
Node::Element(node) => {
root_element_to_tokens_ssr(cx, node, global_class)
}
Node::Element(node) => root_element_to_tokens_ssr(cx, node, global_class),
}
}
@@ -273,13 +262,9 @@ fn root_element_to_tokens_ssr(
};
let tag_name = node.name.to_string();
let is_custom_element = is_custom_element(&tag_name);
let typed_element_name = if is_custom_element {
Ident::new("Custom", node.name.span())
} else {
let camel_cased = camel_case_tag_name(
&tag_name.replace("svg::", "").replace("math::", ""),
);
let typed_element_name = {
let camel_cased =
camel_case_tag_name(&tag_name.replace("svg::", "").replace("math::", ""));
Ident::new(&camel_cased, node.name.span())
};
let typed_element_name = if is_svg_element(&tag_name) {
@@ -289,19 +274,10 @@ fn root_element_to_tokens_ssr(
} else {
quote! { #typed_element_name }
};
let full_name = if is_custom_element {
quote! {
leptos::leptos_dom::html::Custom::new(#tag_name)
}
} else {
quote! {
leptos::leptos_dom::html::#typed_element_name::default()
}
};
quote! {
{
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_html(cx, #full_name, #template)
::leptos::HtmlElement::from_html(cx, leptos::leptos_dom::#typed_element_name::default(), #template)
}
}
}
@@ -331,25 +307,17 @@ fn element_to_tokens_ssr(
template.push('<');
template.push_str(&tag_name);
let mut inner_html = None;
for attr in &node.attributes {
if let Node::Attribute(attr) = attr {
inner_html = attribute_to_tokens_ssr(
cx,
attr,
template,
holes,
exprs_for_compiler,
);
attribute_to_tokens_ssr(cx, attr, template, holes, exprs_for_compiler);
}
}
// insert hydration ID
let hydration_id = if is_root {
quote! { leptos::leptos_dom::HydrationCtx::peek(), }
quote! { leptos::HydrationCtx::peek(), }
} else {
quote! { leptos::leptos_dom::HydrationCtx::id(), }
quote! { leptos::HydrationCtx::id(), }
};
match node
.attributes
@@ -371,54 +339,42 @@ fn element_to_tokens_ssr(
template.push_str("/>");
} else {
template.push('>');
for child in &node.children {
match child {
Node::Element(child) => element_to_tokens_ssr(
cx,
child,
template,
holes,
exprs_for_compiler,
false,
global_class,
),
Node::Text(text) => {
if let Some(value) = value_to_string(&text.value) {
template.push_str(&html_escape::encode_safe(&value));
} else {
template.push_str("{}");
let value = text.value.as_ref();
if let Some(inner_html) = inner_html {
template.push_str("{}");
let value = inner_html.as_ref();
holes.push(quote! {
(#value).into_attribute(cx).as_nameless_value_string().unwrap_or_default(),
})
} else {
for child in &node.children {
match child {
Node::Element(child) => element_to_tokens_ssr(
cx,
child,
template,
holes,
exprs_for_compiler,
false,
global_class,
),
Node::Text(text) => {
if let Some(value) = value_to_string(&text.value) {
template.push_str(&html_escape::encode_safe(
&value,
));
} else {
template.push_str("{}");
let value = text.value.as_ref();
holes.push(quote! {
#value.into_view(#cx).render_to_string(#cx),
})
}
holes.push(quote! {
#value.into_view(#cx).render_to_string(#cx),
})
}
Node::Block(block) => {
if let Some(value) = value_to_string(&block.value) {
template.push_str(&value);
} else {
template.push_str("{}");
let value = block.value.as_ref();
holes.push(quote! {
#value.into_view(#cx).render_to_string(#cx),
})
}
}
Node::Fragment(_) => todo!(),
_ => {}
}
Node::Block(block) => {
if let Some(value) = value_to_string(&block.value) {
template.push_str(&value);
} else {
template.push_str("{}");
let value = block.value.as_ref();
holes.push(quote! {
#value.into_view(#cx).render_to_string(#cx),
})
}
}
Node::Fragment(_) => todo!(),
_ => {}
}
}
@@ -442,29 +398,24 @@ fn value_to_string(value: &syn_rsx::NodeValueExpr) -> Option<String> {
}
}
// returns `inner_html`
fn attribute_to_tokens_ssr<'a>(
fn attribute_to_tokens_ssr(
cx: &Ident,
node: &'a NodeAttribute,
node: &NodeAttribute,
template: &mut String,
holes: &mut Vec<TokenStream>,
exprs_for_compiler: &mut Vec<TokenStream>,
) -> Option<&'a NodeValueExpr> {
) {
let name = node.key.to_string();
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
if name == "ref" || name == "_ref" || name == "node_ref" {
// ignore refs on SSR
} else if name.strip_prefix("on:").is_some() {
let (event_type, handler) = event_from_attribute_node(node, false);
exprs_for_compiler.push(quote! {
leptos::leptos_dom::helpers::ssr_event_listener(#event_type, #handler);
leptos::ssr_event_listener(#event_type, #handler);
})
} else if name.strip_prefix("prop:").is_some()
|| name.strip_prefix("class:").is_some()
{
} else if name.strip_prefix("prop:").is_some() || name.strip_prefix("class:").is_some() {
// ignore props for SSR
// ignore classes: we'll handle these separately
} else if name == "inner_html" {
return node.value.as_ref();
} else {
let name = name.replacen("attr:", "", 1);
@@ -483,14 +434,13 @@ fn attribute_to_tokens_ssr<'a>(
holes.push(quote! {
&{#value}.into_attribute(#cx)
.as_nameless_value_string()
.map(|a| format!("{}=\"{}\"", #name, leptos::leptos_dom::ssr::escape_attr(&a)))
.map(|a| format!("{}=\"{}\"", #name, leptos::escape_attr(&a)))
.unwrap_or_default(),
})
}
}
}
};
None
}
}
fn set_class_attribute_ssr(
@@ -500,32 +450,30 @@ fn set_class_attribute_ssr(
holes: &mut Vec<TokenStream>,
global_class: Option<&TokenTree>,
) {
let (static_global_class, dyn_global_class) = match global_class {
Some(TokenTree::Literal(lit)) => {
let str = lit.to_string();
// A lit here can be a string, byte_string, char, byte_char, int or float.
// If it's a string we remove the quotes so folks can use them directly
// without needing braces. E.g. view!{cx, class="my-class", ... }
let str = if str.starts_with('"') && str.ends_with('"') {
str[1..str.len() - 1].to_string()
} else {
str
};
(str, None)
}
None => (String::new(), None),
Some(val) => (String::new(), Some(val)),
let static_global_class = match global_class {
Some(TokenTree::Literal(lit)) => lit.to_string(),
_ => String::new(),
};
let dyn_global_class = match global_class {
None => None,
Some(TokenTree::Literal(_)) => None,
Some(val) => Some(val),
};
let static_class_attr = node
.attributes
.iter()
.filter_map(|a| match a {
Node::Attribute(attr) if attr.key.to_string() == "class" => {
attr.value.as_ref().and_then(value_to_string)
.filter_map(|a| {
if let Node::Attribute(a) = a {
if a.key.to_string() == "class" {
a.value.as_ref().and_then(value_to_string)
} else {
None
}
} else {
None
}
_ => None,
})
.chain(Some(static_global_class))
.chain(std::iter::once(static_global_class))
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
@@ -559,9 +507,7 @@ fn set_class_attribute_ssr(
if let Node::Attribute(node) = node {
let name = node.key.to_string();
if name == "class" {
return if let Some((_, name, value)) =
fancy_class_name(&name, cx, node)
{
return if let Some((_, name, value)) = fancy_class_name(&name, cx, node) {
let span = node.key.span();
Some((span, name, value))
} else {
@@ -607,7 +553,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::leptos_dom::ssr::escape_attr(&a).to_string())
.map(|a| leptos::escape_attr(&a).to_string())
.unwrap_or_default(),
});
}
@@ -682,7 +628,7 @@ fn node_to_tokens(
Node::Text(node) => {
let value = node.value.as_ref();
quote! {
leptos::leptos_dom::html::text(#value)
leptos::text(#value)
}
}
Node::Block(node) => {
@@ -690,9 +636,7 @@ fn node_to_tokens(
quote! { #value }
}
Node::Attribute(node) => attribute_to_tokens(cx, node),
Node::Element(node) => {
element_to_tokens(cx, node, parent_type, global_class)
}
Node::Element(node) => element_to_tokens(cx, node, parent_type, global_class),
}
}
@@ -708,7 +652,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::html::custom(#cx, leptos::leptos_dom::html::Custom::new(#name)) }
quote! { leptos::leptos_dom::custom(#cx, leptos::leptos_dom::Custom::new(#name)) }
} else if is_svg_element(&tag) {
let name = &node.name;
parent_type = TagType::Svg;
@@ -725,21 +669,17 @@ 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::html::#name(#cx)
leptos::leptos_dom::#name(#cx)
}
}
TagType::Html => {
quote! { leptos::leptos_dom::html::#name(#cx) }
}
TagType::Html => quote! { leptos::leptos_dom::#name(#cx) },
TagType::Svg => quote! { leptos::leptos_dom::svg::#name(#cx) },
TagType::Math => {
quote! { leptos::leptos_dom::math::#name(#cx) }
}
TagType::Math => quote! { leptos::leptos_dom::math::#name(#cx) },
}
} else {
let name = &node.name;
parent_type = TagType::Html;
quote! { leptos::leptos_dom::html::#name(#cx) }
quote! { leptos::leptos_dom::#name(#cx) }
};
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
@@ -778,12 +718,8 @@ fn element_to_tokens(
#[allow(unused_braces)] #value
}
}
Node::Element(node) => {
element_to_tokens(cx, node, parent_type, global_class)
}
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => {
quote! {}
}
Node::Element(node) => element_to_tokens(cx, node, parent_type, global_class),
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
};
quote! {
.child((#cx, #child))
@@ -801,7 +737,7 @@ fn element_to_tokens(
fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
let span = node.key.span();
let name = node.key.to_string();
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
if name == "ref" || name == "_ref" || name == "node_ref" {
let value = node
.value
.as_ref()
@@ -810,7 +746,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
let node_ref = quote_spanned! { span => node_ref };
quote! {
.#node_ref(#value)
.#node_ref(&#value)
}
} else if let Some(name) = name.strip_prefix("on:") {
let handler = node
@@ -832,7 +768,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
.expect("couldn't parse event name");
let event_type = if is_custom {
quote! { leptos::leptos_dom::leptos_dom::events::Custom::new(#name) }
quote! { Custom::new(#name) }
} else {
event_type
};
@@ -887,9 +823,9 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
} else {
quote! { undelegated }
};
quote! { ::leptos::leptos_dom::ev::#undelegated(::leptos::leptos_dom::ev::#event_type) }
quote! { ::leptos::ev::#undelegated(::leptos::ev::#event_type) }
} else {
quote! { ::leptos::leptos_dom::ev::#event_type }
quote! { ::leptos::ev::#event_type }
};
quote! {
@@ -990,8 +926,7 @@ fn component_to_tokens(
let props = attrs
.clone()
.filter(|attr| {
!attr.key.to_string().starts_with("clone:")
&& !attr.key.to_string().starts_with("on:")
!attr.key.to_string().starts_with("clone:") && !attr.key.to_string().starts_with("on:")
})
.map(|attr| {
let name = &attr.key;
@@ -1085,8 +1020,7 @@ fn event_from_attribute_node(
attr: &NodeAttribute,
force_undelegated: bool,
) -> (TokenStream, &Expr) {
let event_name =
attr.key.to_string().strip_prefix("on:").unwrap().to_owned();
let event_name = attr.key.to_string().strip_prefix("on:").unwrap().to_owned();
let handler = attr
.value
@@ -1107,9 +1041,9 @@ fn event_from_attribute_node(
.expect("couldn't parse event name");
let event_type = if force_undelegated || name_undelegated {
quote! { ::leptos::leptos_dom::ev::undelegated(::leptos::leptos_dom::ev::#event_type) }
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
} else {
quote! { ::leptos::leptos_dom::ev::#event_type }
quote! { ::leptos::ev::#event_type }
};
(event_type, handler)
}
@@ -1125,10 +1059,7 @@ fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
.expect("element needs to have a name"),
NodeName::Block(_) => {
let span = tag_name.span();
proc_macro_error::emit_error!(
span,
"blocks not allowed in tag-name position"
);
proc_macro_error::emit_error!(span, "blocks not allowed in tag-name position");
Ident::new("", span)
}
_ => Ident::new(
@@ -1329,8 +1260,7 @@ fn fancy_class_name<'a>(
};
let class_name = &tuple.elems[0];
let class_name = if let Expr::Lit(ExprLit {
lit: Lit::Str(s),
..
lit: Lit::Str(s), ..
}) = class_name
{
s.value()

View File

@@ -8,6 +8,7 @@ repository = "https://github.com/leptos-rs/leptos"
description = "Reactive system for the Leptos web framework."
[dependencies]
log = "0.4"
slotmap = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde-lite = { version = "0.3", optional = true }
@@ -32,7 +33,6 @@ web-sys = { version = "0.3", features = [
cfg-if = "1.0.0"
[dev-dependencies]
log = "0.4"
tokio-test = "0.4"
leptos = { path = "../leptos" }

View File

@@ -1,10 +1,11 @@
#![forbid(unsafe_code)]
use crate::{runtime::with_runtime, Scope};
use std::{
any::{Any, TypeId},
collections::HashMap,
};
use crate::{runtime::with_runtime, Scope};
/// Provides a context value of type `T` to the current reactive [Scope](crate::Scope)
/// and all of its descendants. This can be consumed using [use_context](crate::use_context).
///
@@ -57,8 +58,7 @@ where
_ = with_runtime(cx.runtime, |runtime| {
let mut contexts = runtime.scope_contexts.borrow_mut();
let context =
contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
let context = contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
context.insert(id, Box::new(value) as Box<dyn Any>);
});
}
@@ -119,25 +119,21 @@ where
let contexts = runtime.scope_contexts.borrow();
let context = contexts.get(cx.id);
context
.and_then(|context| {
context.get(&id).and_then(|val| val.downcast_ref::<T>())
})
.and_then(|context| context.get(&id).and_then(|val| val.downcast_ref::<T>()))
.cloned()
};
match local_value {
Some(val) => Some(val),
None => {
runtime
.scope_parents
.borrow()
.get(cx.id)
.and_then(|parent| {
use_context::<T>(Scope {
runtime: cx.runtime,
id: *parent,
})
None => runtime
.scope_parents
.borrow()
.get(cx.id)
.and_then(|parent| {
use_context::<T>(Scope {
runtime: cx.runtime,
id: *parent,
})
}
}),
}
})
.ok()

View File

@@ -1,11 +1,9 @@
#![forbid(unsafe_code)]
use crate::{
macros::debug_warn,
runtime::{with_runtime, RuntimeId},
Runtime, Scope, ScopeProperty,
};
use crate::runtime::{with_runtime, RuntimeId};
use crate::{debug_warn, Runtime, Scope, ScopeProperty};
use cfg_if::cfg_if;
use std::{cell::RefCell, fmt::Debug};
use std::cell::RefCell;
use std::fmt::Debug;
/// Effects run a certain chunk of code whenever the signals they depend on change.
/// `create_effect` immediately runs the given function once, tracks its dependence
@@ -116,10 +114,8 @@ where
)
)]
#[track_caller]
pub fn create_isomorphic_effect<T>(
cx: Scope,
f: impl Fn(Option<T>) -> T + 'static,
) where
pub fn create_isomorphic_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
where
T: 'static,
{
let e = cx.runtime.create_effect(f);
@@ -213,13 +209,7 @@ impl EffectId {
if let Some(effect) = effect {
effect.run(*self, runtime_id);
} else {
debug_warn!(
"[Effect] Trying to run an Effect that has been disposed. \
This is probably either a logic error in a component \
that creates and disposes of scopes, or a Resource \
resolving after its scope has been dropped without \
having been cleaned up."
);
debug_warn!("[Effect] Trying to run an Effect that has been disposed. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.")
}
})
}

View File

@@ -39,35 +39,35 @@
//! // this is omitted from most of the examples in the docs
//! // you usually won't need to call it yourself
//! create_scope(create_runtime(), |cx| {
//! // a signal: returns a (getter, setter) pair
//! let (count, set_count) = create_signal(cx, 0);
//! // a signal: returns a (getter, setter) pair
//! let (count, set_count) = create_signal(cx, 0);
//!
//! // calling the getter gets the value
//! assert_eq!(count(), 0);
//! // calling the setter sets the value
//! set_count(1);
//! // or we can mutate it in place with update()
//! set_count.update(|n| *n += 1);
//! // calling the getter gets the value
//! assert_eq!(count(), 0);
//! // calling the setter sets the value
//! set_count(1);
//! // or we can mutate it in place with update()
//! set_count.update(|n| *n += 1);
//!
//! // a derived signal: a plain closure that relies on the signal
//! // the closure will run whenever we *access* double_count()
//! let double_count = move || count() * 2;
//! assert_eq!(double_count(), 4);
//! // a derived signal: a plain closure that relies on the signal
//! // the closure will run whenever we *access* double_count()
//! let double_count = move || count() * 2;
//! assert_eq!(double_count(), 4);
//!
//! // a memo: subscribes to the signal
//! // the closure will run only when count changes
//! let memoized_triple_count = create_memo(cx, move |_| count() * 3);
//! assert_eq!(memoized_triple_count(), 6);
//!
//! // a memo: subscribes to the signal
//! // the closure will run only when count changes
//! let memoized_triple_count = create_memo(cx, move |_| count() * 3);
//! assert_eq!(memoized_triple_count(), 6);
//!
//! // this effect will run whenever count() changes
//! create_effect(cx, move |_| {
//! println!("Count = {}", count());
//! });
//! // this effect will run whenever count() changes
//! create_effect(cx, move |_| {
//! println!("Count = {}", count());
//! });
//! });
//! ```
#[cfg_attr(debug_assertions, macro_use)]
extern crate tracing;
pub extern crate tracing;
mod context;
mod effect;
@@ -136,34 +136,20 @@ pub trait UntrackedSettableSignal<T> {
/// Runs the provided closure with a mutable reference to the current
/// value without notifying dependents and returns
/// the value the closure returned.
fn update_returning_untracked<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U>;
fn update_returning_untracked<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U>;
}
mod macros {
macro_rules! debug_warn {
($($x:tt)*) => {
#[doc(hidden)]
#[macro_export]
macro_rules! debug_warn {
($($x:tt)*) => {
{
#[cfg(debug_assertions)]
{
#[cfg(debug_assertions)]
{
($crate::console_warn(&format_args!($($x)*).to_string()))
}
#[cfg(not(debug_assertions))]
{
($($x)*)
}
log::warn!($($x)*)
}
#[cfg(not(debug_assertions))]
{ }
}
}
pub(crate) use debug_warn;
}
pub(crate) fn console_warn(s: &str) {
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
eprintln!("{s}");
#[cfg(any(feature = "csr", feature = "hydrate"))]
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(s));
}

View File

@@ -65,10 +65,7 @@ use std::fmt::Debug;
)
)
)]
pub fn create_memo<T>(
cx: Scope,
f: impl Fn(Option<&T>) -> T + 'static,
) -> Memo<T>
pub fn create_memo<T>(cx: Scope, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + 'static,
{
@@ -273,13 +270,9 @@ where
.with(|n| f(n.as_ref().expect("Memo is missing its initial value")))
}
pub(crate) fn try_with<U>(
&self,
f: impl Fn(&T) -> U,
) -> Result<U, SignalError> {
self.0.try_with(|n| {
f(n.as_ref().expect("Memo is missing its initial value"))
})
pub(crate) fn try_with<U>(&self, f: impl Fn(&T) -> U) -> Result<U, SignalError> {
self.0
.try_with(|n| f(n.as_ref().expect("Memo is missing its initial value")))
}
#[cfg(feature = "hydrate")]

View File

@@ -1,12 +1,10 @@
#![forbid(unsafe_code)]
use crate::{
create_effect, create_isomorphic_effect, create_memo, create_signal,
queue_microtask,
create_effect, create_isomorphic_effect, create_memo, create_signal, queue_microtask,
runtime::{with_runtime, RuntimeId},
serialization::Serializable,
spawn::spawn_local,
use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext,
WriteSignal,
use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
};
use std::{
any::Any,
@@ -115,9 +113,7 @@ where
let (loading, set_loading) = create_signal(cx, false);
let fetcher = Rc::new(move |s| {
Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>
});
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let source = create_memo(cx, move |_| source());
let r = Rc::new(ResourceState {
@@ -174,18 +170,17 @@ where
/// # create_scope(create_runtime(), |cx| {
/// #[derive(Debug, Clone)] // doesn't implement Serialize, Deserialize
/// struct ComplicatedUnserializableStruct {
/// // something here that can't be serialized
/// // something here that can't be serialized
/// }
/// // any old async function; maybe this is calling a REST API or something
/// async fn setup_complicated_struct() -> ComplicatedUnserializableStruct {
/// // do some work
/// ComplicatedUnserializableStruct {}
/// // do some work
/// ComplicatedUnserializableStruct { }
/// }
///
/// // create the resource; it will run but not be serialized
/// # if cfg!(not(any(feature = "csr", feature = "hydrate"))) {
/// let result =
/// create_local_resource(cx, move || (), |_| setup_complicated_struct());
/// let result = create_local_resource(cx, move || (), |_| setup_complicated_struct());
/// # }
/// # }).dispose();
/// ```
@@ -239,9 +234,7 @@ where
let (loading, set_loading) = create_signal(cx, false);
let fetcher = Rc::new(move |s| {
Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>
});
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let source = create_memo(cx, move |_| source());
let r = Rc::new(ResourceState {
@@ -307,8 +300,7 @@ where
context.pending_resources.remove(&id); // no longer pending
r.resolved.set(true);
let res = T::from_json(&data)
.expect_throw("could not deserialize Resource JSON");
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
r.set_value.update(|n| *n = Some(res));
r.set_loading.update(|n| *n = false);
@@ -326,25 +318,21 @@ where
let set_value = r.set_value;
let set_loading = r.set_loading;
move |res: String| {
let res = T::from_json(&res)
.expect_throw("could not deserialize Resource JSON");
let res =
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
resolved.set(true);
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
}
};
let resolve = wasm_bindgen::closure::Closure::wrap(
Box::new(resolve) as Box<dyn Fn(String)>,
);
let resolve =
wasm_bindgen::closure::Closure::wrap(Box::new(resolve) as Box<dyn Fn(String)>);
let resource_resolvers = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOURCE_RESOLVERS"),
)
.expect_throw(
"no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope",
);
let id = serde_json::to_string(&id)
.expect_throw("could not serialize Resource ID");
.expect_throw("no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope");
let id = serde_json::to_string(&id).expect_throw("could not serialize Resource ID");
_ = js_sys::Reflect::set(
&resource_resolvers,
&wasm_bindgen::JsValue::from_str(&id),
@@ -377,9 +365,7 @@ where
T: Clone,
{
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.read()
})
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.read())
})
.ok()
.flatten()
@@ -394,9 +380,7 @@ where
/// [Resource::read].
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.with(f)
})
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.with(f))
})
.ok()
.flatten()
@@ -405,22 +389,15 @@ where
/// Returns a signal that indicates whether the resource is currently loading.
pub fn loading(&self) -> ReadSignal<bool> {
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.loading
})
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.loading)
})
.expect(
"tried to call Resource::loading() in a runtime that has already \
been disposed.",
)
.expect("tried to call Resource::loading() in a runtime that has already been disposed.")
}
/// Re-runs the async function with the current source data.
pub fn refetch(&self) {
_ = with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.refetch()
})
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.refetch())
});
}
@@ -436,10 +413,7 @@ where
resource.to_serialization_resolver(self.id)
})
})
.expect(
"tried to serialize a Resource in a runtime that has already been \
disposed",
)
.expect("tried to serialize a Resource in a runtime that has already been disposed")
.await
}
}
@@ -699,16 +673,8 @@ where
let mut tx = tx.clone();
move |value| {
if let Some(value) = value.as_ref() {
tx.try_send((
id,
value
.to_json()
.expect("could not serialize Resource"),
))
.expect(
"failed while trying to write to Resource \
serializer",
);
tx.try_send((id, value.to_json().expect("could not serialize Resource")))
.expect("failed while trying to write to Resource serializer");
}
}
})

View File

@@ -1,9 +1,8 @@
#![forbid(unsafe_code)]
use crate::{
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo,
ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer,
ScopeId, ScopeProperty, SerializableResource, SignalId,
UnserializableResource, WriteSignal,
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo, ReadSignal,
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId, ScopeProperty,
SerializableResource, SignalId, UnserializableResource, WriteSignal,
};
use cfg_if::cfg_if;
use futures::stream::FuturesUnordered;
@@ -35,10 +34,7 @@ cfg_if! {
/// Get the selected runtime from the thread-local set of runtimes. On the server,
/// this will return the correct runtime. In the browser, there should only be one runtime.
pub(crate) fn with_runtime<T>(
id: RuntimeId,
f: impl FnOnce(&Runtime) -> T,
) -> Result<T, ()> {
pub(crate) fn with_runtime<T>(id: RuntimeId, f: impl FnOnce(&Runtime) -> T) -> Result<T, ()> {
// in the browser, everything should exist under one runtime
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
@@ -92,10 +88,7 @@ impl RuntimeId {
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(scope, disposer)
})
.expect(
"tried to create raw scope in a runtime that has already been \
disposed",
)
.expect("tried to create raw scope in a runtime that has already been disposed")
}
pub(crate) fn run_scope_undisposed<T>(
@@ -116,38 +109,24 @@ impl RuntimeId {
.expect("tried to run scope in a runtime that has been disposed")
}
pub(crate) fn run_scope<T>(
self,
f: impl FnOnce(Scope) -> T,
parent: Option<Scope>,
) -> T {
pub(crate) fn run_scope<T>(self, f: impl FnOnce(Scope) -> T, parent: Option<Scope>) -> T {
let (ret, _, disposer) = self.run_scope_undisposed(f, parent);
disposer.dispose();
ret
}
#[track_caller]
pub(crate) fn create_concrete_signal(
self,
value: Rc<RefCell<dyn Any>>,
) -> SignalId {
pub(crate) fn create_concrete_signal(self, value: Rc<RefCell<dyn Any>>) -> SignalId {
with_runtime(self, |runtime| runtime.signals.borrow_mut().insert(value))
.expect(
"tried to create a signal in a runtime that has been disposed",
)
.expect("tried to create a signal in a runtime that has been disposed")
}
#[track_caller]
pub(crate) fn create_signal<T>(
self,
value: T,
) -> (ReadSignal<T>, WriteSignal<T>)
pub(crate) fn create_signal<T>(self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
where
T: Any + 'static,
{
let id = self.create_concrete_signal(
Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>
);
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
(
ReadSignal {
@@ -171,9 +150,7 @@ impl RuntimeId {
where
T: Any + 'static,
{
let id = self.create_concrete_signal(
Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>
);
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
RwSignal {
runtime: self,
id,
@@ -184,21 +161,13 @@ impl RuntimeId {
}
#[track_caller]
pub(crate) fn create_concrete_effect(
self,
effect: Rc<dyn AnyEffect>,
) -> EffectId {
with_runtime(self, |runtime| {
runtime.effects.borrow_mut().insert(effect)
})
.expect("tried to create an effect in a runtime that has been disposed")
pub(crate) fn create_concrete_effect(self, effect: Rc<dyn AnyEffect>) -> EffectId {
with_runtime(self, |runtime| runtime.effects.borrow_mut().insert(effect))
.expect("tried to create an effect in a runtime that has been disposed")
}
#[track_caller]
pub(crate) fn create_effect<T>(
self,
f: impl Fn(Option<T>) -> T + 'static,
) -> EffectId
pub(crate) fn create_effect<T>(self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
where
T: Any + 'static,
{
@@ -218,10 +187,7 @@ impl RuntimeId {
}
#[track_caller]
pub(crate) fn create_memo<T>(
self,
f: impl Fn(Option<&T>) -> T + 'static,
) -> Memo<T>
pub(crate) fn create_memo<T>(self, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + Any + 'static,
{
@@ -258,17 +224,13 @@ pub(crate) struct Runtime {
pub scope_parents: RefCell<SparseSecondaryMap<ScopeId, ScopeId>>,
pub scope_children: RefCell<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
#[allow(clippy::type_complexity)]
pub scope_contexts:
RefCell<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
pub scope_contexts: RefCell<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
#[allow(clippy::type_complexity)]
pub scope_cleanups:
RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
pub scope_cleanups: RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
pub signals: RefCell<SlotMap<SignalId, Rc<RefCell<dyn Any>>>>,
pub signal_subscribers:
RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
pub signal_subscribers: RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
pub effects: RefCell<SlotMap<EffectId, Rc<dyn AnyEffect>>>,
pub effect_sources:
RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
pub effect_sources: RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
}

View File

@@ -14,10 +14,7 @@ use std::{collections::HashMap, fmt};
/// values will not have access to values created under another `create_scope`.
///
/// You usually don't need to call this manually.
pub fn create_scope(
runtime: RuntimeId,
f: impl FnOnce(Scope) + 'static,
) -> ScopeDisposer {
pub fn create_scope(runtime: RuntimeId, f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
runtime.run_scope_undisposed(f, None).2
}
@@ -40,10 +37,7 @@ pub fn raw_scope_and_disposer(runtime: RuntimeId) -> (Scope, ScopeDisposer) {
/// of the synchronous operation.
///
/// You usually don't need to call this manually.
pub fn run_scope<T>(
runtime: RuntimeId,
f: impl FnOnce(Scope) -> T + 'static,
) -> T {
pub fn run_scope<T>(runtime: RuntimeId, f: impl FnOnce(Scope) -> T + 'static) -> T {
runtime.run_scope(f, None)
}
@@ -122,20 +116,13 @@ impl Scope {
/// This is useful for applications like a list or a router, which may want to create child scopes and
/// dispose of them when they are no longer needed (e.g., a list item has been destroyed or the user
/// has navigated away from the route.)
pub fn run_child_scope<T>(
self,
f: impl FnOnce(Scope) -> T,
) -> (T, ScopeDisposer) {
let (res, child_id, disposer) =
self.runtime.run_scope_undisposed(f, Some(self));
pub fn run_child_scope<T>(self, f: impl FnOnce(Scope) -> T) -> (T, ScopeDisposer) {
let (res, child_id, disposer) = self.runtime.run_scope_undisposed(f, Some(self));
_ = with_runtime(self.runtime, |runtime| {
let mut children = runtime.scope_children.borrow_mut();
children
.entry(self.id)
.expect(
"trying to add a child to a Scope that has already been \
disposed",
)
.expect("trying to add a child to a Scope that has already been disposed")
.or_default()
.push(child_id);
});
@@ -174,10 +161,7 @@ impl Scope {
runtime.observer.set(prev_observer);
untracked_result
})
.expect(
"tried to run untracked function in a runtime that has been \
disposed",
)
.expect("tried to run untracked function in a runtime that has been disposed")
}
}
@@ -207,10 +191,9 @@ impl Scope {
.dispose();
}
}
// run cleanups
if let Some(cleanups) =
runtime.scope_cleanups.borrow_mut().remove(self.id)
{
if let Some(cleanups) = runtime.scope_cleanups.borrow_mut().remove(self.id) {
for cleanup in cleanups {
cleanup();
}
@@ -227,20 +210,14 @@ impl Scope {
ScopeProperty::Signal(id) => {
// remove the signal
runtime.signals.borrow_mut().remove(id);
let subs = runtime
.signal_subscribers
.borrow_mut()
.remove(id);
let subs = runtime.signal_subscribers.borrow_mut().remove(id);
// each of the subs needs to remove the signal from its dependencies
// so that it doesn't try to read the (now disposed) signal
if let Some(subs) = subs {
let source_map =
runtime.effect_sources.borrow();
let source_map = runtime.effect_sources.borrow();
for effect in subs.borrow().iter() {
if let Some(effect_sources) =
source_map.get(*effect)
{
if let Some(effect_sources) = source_map.get(*effect) {
effect_sources.borrow_mut().remove(&id);
}
}
@@ -259,15 +236,12 @@ impl Scope {
})
}
pub(crate) fn with_scope_property(
&self,
f: impl FnOnce(&mut Vec<ScopeProperty>),
) {
pub(crate) fn with_scope_property(&self, f: impl FnOnce(&mut Vec<ScopeProperty>)) {
_ = with_runtime(self.runtime, |runtime| {
let scopes = runtime.scopes.borrow();
let scope = scopes.get(self.id).expect(
"tried to add property to a scope that has been disposed",
);
let scope = scopes
.get(self.id)
.expect("tried to add property to a scope that has been disposed");
f(&mut scope.borrow_mut());
})
}
@@ -337,23 +311,18 @@ impl ScopeDisposer {
impl Scope {
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
pub fn all_resources(&self) -> Vec<ResourceId> {
with_runtime(self.runtime, |runtime| runtime.all_resources())
.unwrap_or_default()
with_runtime(self.runtime, |runtime| runtime.all_resources()).unwrap_or_default()
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope that are
/// pending from the server.
pub fn pending_resources(&self) -> Vec<ResourceId> {
with_runtime(self.runtime, |runtime| runtime.pending_resources())
.unwrap_or_default()
with_runtime(self.runtime, |runtime| runtime.pending_resources()).unwrap_or_default()
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
pub fn serialization_resolvers(
&self,
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
with_runtime(self.runtime, |runtime| runtime.serialization_resolvers())
.unwrap_or_default()
pub fn serialization_resolvers(&self) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
with_runtime(self.runtime, |runtime| runtime.serialization_resolvers()).unwrap_or_default()
}
/// Registers the given [SuspenseContext](crate::SuspenseContext) with the current scope,
@@ -373,8 +342,7 @@ impl Scope {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
create_isomorphic_effect(*self, move |_| {
let pending =
context.pending_resources.try_with(|n| *n).unwrap_or(0);
let pending = context.pending_resources.try_with(|n| *n).unwrap_or(0);
if pending == 0 {
_ = tx.unbounded_send(());
}
@@ -396,9 +364,7 @@ impl Scope {
/// The set of all HTML fragments currently pending.
/// Returns a tuple of the hydration ID of the previous element, and a pinned `Future` that will yield the
/// `<Suspense/>` HTML when all resources are resolved.
pub fn pending_fragments(
&self,
) -> HashMap<String, (String, PinnedFuture<String>)> {
pub fn pending_fragments(&self) -> HashMap<String, (String, PinnedFuture<String>)> {
with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
std::mem::take(&mut shared_context.pending_fragments)

View File

@@ -1,10 +1,7 @@
#![forbid(unsafe_code)]
use crate::{
create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSignal,
};
use std::{
cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, rc::Rc,
};
use std::{cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, rc::Rc};
use crate::{create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSignal};
/// Creates a conditional signal that only notifies subscribers when a change
/// in the source signals value changes whether it is equal to the key value
@@ -19,29 +16,26 @@ use std::{
/// # use std::rc::Rc;
/// # use std::cell::RefCell;
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let is_selected = create_selector(cx, a);
/// let total_notifications = Rc::new(RefCell::new(0));
/// let not = Rc::clone(&total_notifications);
/// create_isomorphic_effect(cx, {
/// let is_selected = is_selected.clone();
/// move |_| {
/// if is_selected(5) {
/// *not.borrow_mut() += 1;
/// }
/// }
/// });
/// let (a, set_a) = create_signal(cx, 0);
/// let is_selected = create_selector(cx, a);
/// let total_notifications = Rc::new(RefCell::new(0));
/// let not = Rc::clone(&total_notifications);
/// create_isomorphic_effect(cx, {let is_selected = is_selected.clone(); move |_| {
/// if is_selected(5) {
/// *not.borrow_mut() += 1;
/// }
/// }});
///
/// assert_eq!(is_selected(5), false);
/// assert_eq!(*total_notifications.borrow(), 0);
/// set_a(5);
/// assert_eq!(is_selected(5), true);
/// assert_eq!(*total_notifications.borrow(), 1);
/// set_a(5);
/// assert_eq!(is_selected(5), true);
/// assert_eq!(*total_notifications.borrow(), 1);
/// set_a(4);
/// assert_eq!(is_selected(5), false);
/// assert_eq!(is_selected(5), false);
/// assert_eq!(*total_notifications.borrow(), 0);
/// set_a(5);
/// assert_eq!(is_selected(5), true);
/// assert_eq!(*total_notifications.borrow(), 1);
/// set_a(5);
/// assert_eq!(is_selected(5), true);
/// assert_eq!(*total_notifications.borrow(), 1);
/// set_a(4);
/// assert_eq!(is_selected(5), false);
/// # })
/// # .dispose()
/// ```
@@ -70,9 +64,8 @@ where
T: PartialEq + Eq + Debug + Clone + Hash + 'static,
{
#[allow(clippy::type_complexity)]
let subs: Rc<
RefCell<HashMap<T, (ReadSignal<bool>, WriteSignal<bool>)>>,
> = Rc::new(RefCell::new(HashMap::new()));
let subs: Rc<RefCell<HashMap<T, (ReadSignal<bool>, WriteSignal<bool>)>>> =
Rc::new(RefCell::new(HashMap::new()));
let v = Rc::new(RefCell::new(None));
create_isomorphic_effect(cx, {
@@ -85,9 +78,7 @@ where
if prev.as_ref() != Some(&next_value) {
let subs = { subs.borrow().clone() };
for (key, signal) in subs.into_iter() {
if f(&key, &next_value)
|| (prev.is_some() && f(&key, prev.as_ref().unwrap()))
{
if f(&key, &next_value) || (prev.is_some() && f(&key, prev.as_ref().unwrap())) {
signal.1.update(|n| *n = true);
}
}

View File

@@ -1,9 +1,8 @@
#![forbid(unsafe_code)]
use crate::{
macros::debug_warn,
debug_warn,
runtime::{with_runtime, RuntimeId},
Runtime, Scope, ScopeProperty, UntrackedGettableSignal,
UntrackedSettableSignal,
Runtime, Scope, ScopeProperty, UntrackedGettableSignal, UntrackedSettableSignal,
};
use cfg_if::cfg_if;
use futures::Stream;
@@ -60,10 +59,7 @@ use thiserror::Error;
)
)]
#[track_caller]
pub fn create_signal<T>(
cx: Scope,
value: T,
) -> (ReadSignal<T>, WriteSignal<T>) {
pub fn create_signal<T>(cx: Scope, value: T) -> (ReadSignal<T>, WriteSignal<T>) {
let s = cx.runtime.create_signal(value);
cx.with_scope_property(|prop| prop.push(ScopeProperty::Signal(s.0.id)));
s
@@ -289,12 +285,8 @@ where
/// Applies the function to the current Signal, if it exists, and subscribes
/// the running effect.
pub(crate) fn try_with<U>(
&self,
f: impl FnOnce(&T) -> U,
) -> Result<U, SignalError> {
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
{
pub(crate) fn try_with<U>(&self, f: impl FnOnce(&T) -> U) -> Result<U, SignalError> {
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f)) {
Ok(Ok(v)) => Ok(v),
Ok(Err(e)) => Err(e),
Err(_) => Err(SignalError::RuntimeDisposed),
@@ -373,7 +365,7 @@ where
/// Calling [WriteSignal::update] will mutate the signals value in place,
/// and notify all subscribers that the signals value has changed.
///
/// `WriteSignal` implements [Fn], such that `set_value(new_value)` is equivalent to
/// `ReadSignal` 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
@@ -461,10 +453,7 @@ where
)
)
)]
fn update_returning_untracked<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
fn update_returning_untracked<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
self.id.update_with_no_effect(self.runtime, f)
}
}
@@ -522,17 +511,11 @@ where
/// let (count, set_count) = create_signal(cx, 0);
///
/// // notifies subscribers
/// let value = set_count.update_returning(|n| {
/// *n = 1;
/// *n * 10
/// });
/// let value = set_count.update_returning(|n| { *n = 1; *n * 10 });
/// assert_eq!(value, Some(10));
/// assert_eq!(count(), 1);
///
/// let value = set_count.update_returning(|n| {
/// *n += 1;
/// *n * 10
/// });
/// let value = set_count.update_returning(|n| { *n += 1; *n * 10 });
/// assert_eq!(value, Some(20));
/// assert_eq!(count(), 2);
/// # }).dispose();
@@ -550,10 +533,7 @@ where
)
)
)]
pub fn update_returning<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
pub fn update_returning<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
self.id.update(self.runtime, f)
}
@@ -813,10 +793,7 @@ impl<T> UntrackedSettableSignal<T> for RwSignal<T> {
)
)
)]
fn update_returning_untracked<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
fn update_returning_untracked<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
self.id.update_with_no_effect(self.runtime, f)
}
}
@@ -908,11 +885,7 @@ where
///
/// // you can include arbitrary logic in this update function
/// // also notifies subscribers, even though the value hasn't changed
/// count.update(|n| {
/// if *n > 3 {
/// *n += 1
/// }
/// });
/// count.update(|n| if *n > 3 { *n += 1 });
/// assert_eq!(count(), 1);
/// # }).dispose();
/// ```
@@ -943,17 +916,11 @@ where
/// let count = create_rw_signal(cx, 0);
///
/// // notifies subscribers
/// let value = count.update_returning(|n| {
/// *n = 1;
/// *n * 10
/// });
/// let value = count.update_returning(|n| { *n = 1; *n * 10 });
/// assert_eq!(value, Some(10));
/// assert_eq!(count(), 1);
///
/// let value = count.update_returning(|n| {
/// *n += 1;
/// *n * 10
/// });
/// let value = count.update_returning(|n| { *n += 1; *n * 10 });
/// assert_eq!(value, Some(20));
/// assert_eq!(count(), 2);
/// # }).dispose();
@@ -971,10 +938,7 @@ where
)
)
)]
pub fn update_returning<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
pub fn update_returning<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
self.id.update(self.runtime, f)
}
@@ -1244,9 +1208,7 @@ impl SignalId {
}?;
let value = value.try_borrow().unwrap_or_else(|e| {
debug_warn!(
"Signal::try_with_no_subscription failed on Signal<{}>. It \
seems you're trying to read the value of a signal within an \
effect caused by updating the signal.",
"Signal::try_with_no_subscription failed on Signal<{}>. It seems you're trying to read the value of a signal within an effect caused by updating the signal.",
std::any::type_name::<T>()
);
panic!("{e}");
@@ -1284,25 +1246,15 @@ impl SignalId {
.expect("tried to access a signal in a runtime that has been disposed")
}
pub(crate) fn with<T, U>(
&self,
runtime: RuntimeId,
f: impl FnOnce(&T) -> U,
) -> U
pub(crate) fn with<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&T) -> U) -> U
where
T: 'static,
{
with_runtime(runtime, |runtime| self.try_with(runtime, f).unwrap())
.expect(
"tried to access a signal in a runtime that has been disposed",
)
.expect("tried to access a signal in a runtime that has been disposed")
}
fn update_value<T, U>(
&self,
runtime: RuntimeId,
f: impl FnOnce(&mut T) -> U,
) -> Option<U>
fn update_value<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&mut T) -> U) -> Option<U>
where
T: 'static,
{
@@ -1317,19 +1269,14 @@ impl SignalId {
Some(f(value))
} else {
debug_warn!(
"[Signal::update] failed when downcasting to \
Signal<{}>",
"[Signal::update] failed when downcasting to Signal<{}>",
std::any::type_name::<T>()
);
None
}
} else {
debug_warn!(
"[Signal::update] Youre trying to update a Signal<{}> \
that has already been disposed of. This is probably \
either a logic error in a component that creates and \
disposes of scopes, or a Resource resolving after its \
scope has been dropped without having been cleaned up.",
"[Signal::update] Youre trying to update a Signal<{}> that has already been disposed of. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.",
std::any::type_name::<T>()
);
None

View File

@@ -1,8 +1,5 @@
#![forbid(unsafe_code)]
use crate::{
store_value, Memo, ReadSignal, RwSignal, Scope, StoredValue,
UntrackedGettableSignal,
};
use crate::{store_value, Memo, ReadSignal, RwSignal, Scope, StoredValue, UntrackedGettableSignal};
/// Helper trait for converting `Fn() -> T` closures into
/// [`Signal<T>`].
@@ -36,9 +33,9 @@ where
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &Signal<i32>) -> bool {
/// // ✅ calling the signal clones and returns the value
/// // it is a shorthand for arg.get()
/// arg() > 3
/// // ✅ calling the signal clones and returns the value
/// // it is a shorthand for arg.get()
/// arg() > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);
@@ -115,7 +112,7 @@ where
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &Signal<i32>) -> bool {
/// arg() > 3
/// arg() > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);
@@ -142,10 +139,7 @@ where
};
Self {
inner: SignalTypes::DerivedSignal(
cx,
store_value(cx, Box::new(derived_signal)),
),
inner: SignalTypes::DerivedSignal(cx, store_value(cx, Box::new(derived_signal))),
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
@@ -213,7 +207,7 @@ where
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &Signal<i32>) -> bool {
/// arg.get() > 3
/// arg.get() > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);
@@ -300,9 +294,7 @@ impl<T> Clone for SignalTypes<T> {
match self {
Self::ReadSignal(arg0) => Self::ReadSignal(*arg0),
Self::Memo(arg0) => Self::Memo(*arg0),
Self::DerivedSignal(arg0, arg1) => {
Self::DerivedSignal(*arg0, *arg1)
}
Self::DerivedSignal(arg0, arg1) => Self::DerivedSignal(*arg0, *arg1),
}
}
}
@@ -315,13 +307,9 @@ where
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ReadSignal(arg0) => {
f.debug_tuple("ReadSignal").field(arg0).finish()
}
Self::ReadSignal(arg0) => f.debug_tuple("ReadSignal").field(arg0).finish(),
Self::Memo(arg0) => f.debug_tuple("Memo").field(arg0).finish(),
Self::DerivedSignal(_, _) => {
f.debug_tuple("DerivedSignal").finish()
}
Self::DerivedSignal(_, _) => f.debug_tuple("DerivedSignal").finish(),
}
}
}
@@ -334,9 +322,7 @@ where
match (self, other) {
(Self::ReadSignal(l0), Self::ReadSignal(r0)) => l0 == r0,
(Self::Memo(l0), Self::Memo(r0)) => l0 == r0,
(Self::DerivedSignal(_, l0), Self::DerivedSignal(_, r0)) => {
std::ptr::eq(l0, r0)
}
(Self::DerivedSignal(_, l0), Self::DerivedSignal(_, r0)) => std::ptr::eq(l0, r0),
_ => false,
}
}
@@ -391,9 +377,9 @@ where
///
/// // this function takes either a reactive or non-reactive value
/// fn above_3(arg: &MaybeSignal<i32>) -> bool {
/// // ✅ calling the signal clones and returns the value
/// // it is a shorthand for arg.get()
/// arg() > 3
/// // ✅ calling the signal clones and returns the value
/// // it is a shorthand for arg.get()
/// arg() > 3
/// }
///
/// assert_eq!(above_3(&static_value.into()), true);
@@ -455,7 +441,7 @@ where
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &Signal<i32>) -> bool {
/// arg() > 3
/// arg() > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);
@@ -541,7 +527,7 @@ where
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &MaybeSignal<i32>) -> bool {
/// arg.get() > 3
/// arg.get() > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);

View File

@@ -32,9 +32,9 @@ where
///
/// // this function takes any kind of signal setter
/// fn set_to_4(setter: &SignalSetter<i32>) {
/// // ✅ calling the signal sets the value
/// // it is a shorthand for arg.set()
/// setter(4);
/// // ✅ calling the signal sets the value
/// // it is a shorthand for arg.set()
/// setter(4);
/// }
///
/// set_to_4(&set_count.into());
@@ -90,9 +90,9 @@ where
///
/// // this function takes any kind of signal setter
/// fn set_to_4(setter: &SignalSetter<i32>) {
/// // ✅ calling the signal sets the value
/// // it is a shorthand for arg.set()
/// setter(4)
/// // ✅ calling the signal sets the value
/// // it is a shorthand for arg.set()
/// setter(4)
/// }
///
/// set_to_4(&set_count.into());
@@ -114,10 +114,7 @@ where
)]
pub fn map(cx: Scope, mapped_setter: impl Fn(T) + 'static) -> Self {
Self {
inner: SignalSetterTypes::Mapped(
cx,
store_value(cx, Box::new(mapped_setter)),
),
inner: SignalSetterTypes::Mapped(cx, store_value(cx, Box::new(mapped_setter))),
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
@@ -212,9 +209,7 @@ where
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Write(arg0) => {
f.debug_tuple("WriteSignal").field(arg0).finish()
}
Self::Write(arg0) => f.debug_tuple("WriteSignal").field(arg0).finish(),
Self::Mapped(_, _) => f.debug_tuple("Mapped").finish(),
Self::Default => f.debug_tuple("SignalSetter<Default>").finish(),
}

View File

@@ -1,6 +1,4 @@
use crate::{
create_memo, IntoSignalSetter, RwSignal, Scope, Signal, SignalSetter,
};
use crate::{create_memo, IntoSignalSetter, RwSignal, Scope, Signal, SignalSetter};
/// Derives a reactive slice of an [RwSignal](crate::RwSignal).
///

View File

@@ -1,8 +1,5 @@
#![forbid(unsafe_code)]
use crate::{
create_rw_signal, RwSignal, Scope, UntrackedGettableSignal,
UntrackedSettableSignal,
};
use crate::{create_rw_signal, RwSignal, Scope, UntrackedGettableSignal, UntrackedSettableSignal};
/// A **non-reactive** wrapper for any value, which can be created with [store_value].
///
@@ -112,10 +109,7 @@ where
/// assert_eq!(updated, Some(String::from("b")));
/// });
/// ```
pub fn update_returning<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
pub fn update_returning<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
self.0.update_returning_untracked(f)
}
@@ -163,7 +157,7 @@ where
/// # create_scope(create_runtime(), |cx| {
/// // this structure is neither `Copy` nor `Clone`
/// pub struct MyUncloneableData {
/// pub value: String,
/// pub value: String
/// }
///
/// // ✅ you can move the `StoredValue` and access it with .with()

View File

@@ -1,13 +1,13 @@
#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_isomorphic_effect, create_memo, create_runtime, create_scope,
create_signal,
create_isomorphic_effect, create_memo, create_runtime, create_scope, create_signal,
};
#[cfg(not(feature = "stable"))]
#[test]
fn effect_runs() {
use std::{cell::RefCell, rc::Rc};
use std::cell::RefCell;
use std::rc::Rc;
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
@@ -35,7 +35,8 @@ fn effect_runs() {
#[cfg(not(feature = "stable"))]
#[test]
fn effect_tracks_memo() {
use std::{cell::RefCell, rc::Rc};
use std::cell::RefCell;
use std::rc::Rc;
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
@@ -65,7 +66,8 @@ fn effect_tracks_memo() {
#[cfg(not(feature = "stable"))]
#[test]
fn untrack_mutes_effect() {
use std::{cell::RefCell, rc::Rc};
use std::cell::RefCell;
use std::rc::Rc;
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);

View File

@@ -1,7 +1,5 @@
#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_memo, create_runtime, create_scope, create_signal,
};
use leptos_reactive::{create_memo, create_runtime, create_scope, create_signal};
#[cfg(not(feature = "stable"))]
#[test]

View File

@@ -1,13 +1,14 @@
#[cfg(not(feature = "stable"))]
//#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_isomorphic_effect, create_runtime, create_scope, create_signal,
UntrackedGettableSignal, UntrackedSettableSignal,
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};
use std::cell::RefCell;
use std::rc::Rc;
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
@@ -36,10 +37,10 @@ fn untracked_set_doesnt_trigger_effect() {
.dispose()
}
#[cfg(not(feature = "stable"))]
#[test]
fn untracked_get_doesnt_trigger_effect() {
use std::{cell::RefCell, rc::Rc};
use std::cell::RefCell;
use std::rc::Rc;
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
@@ -51,8 +52,7 @@ fn untracked_get_doesnt_trigger_effect() {
create_isomorphic_effect(cx, {
let b = b.clone();
move |_| {
let formatted =
format!("Values are {} and {}", a(), a2.get_untracked());
let formatted = format!("Values are {} and {}", a(), a2.get_untracked());
*b.borrow_mut() = formatted;
}
});

View File

@@ -13,7 +13,6 @@ 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,7 +1,6 @@
use crate::{ServerFn, ServerFnError};
use leptos_reactive::{
create_rw_signal, spawn_local, store_value, ReadSignal, RwSignal, Scope,
StoredValue,
create_rw_signal, spawn_local, store_value, ReadSignal, RwSignal, Scope, StoredValue,
};
use std::{future::Future, pin::Pin, rc::Rc};
@@ -66,16 +65,15 @@ use std::{future::Future, pin::Pin, rc::Rc};
/// # run_scope(create_runtime(), |cx| {
/// // if there's a single argument, just use that
/// let action1 = create_action(cx, |input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_action(cx, |input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 =
/// create_action(cx, |input: &(usize, String)| async { todo!() });
/// let action3 = create_action(cx, |input: &(usize, String)| async { todo!() });
/// # });
/// ```
pub struct Action<I, O>(StoredValue<ActionState<I, O>>)
@@ -261,16 +259,15 @@ where
/// # run_scope(create_runtime(), |cx| {
/// // if there's a single argument, just use that
/// let action1 = create_action(cx, |input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_action(cx, |input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 =
/// create_action(cx, |input: &(usize, String)| async { todo!() });
/// let action3 = create_action(cx, |input: &(usize, String)| async { todo!() });
/// # });
/// ```
pub fn create_action<I, O, F, Fu>(cx: Scope, action_fn: F) -> Action<I, O>
@@ -309,16 +306,14 @@ where
///
/// #[server(MyServerFn)]
/// async fn my_server_fn() -> Result<(), ServerFnError> {
/// todo!()
/// todo!()
/// }
///
/// # run_scope(create_runtime(), |cx| {
/// let my_server_action = create_server_action::<MyServerFn>(cx);
/// # });
/// ```
pub fn create_server_action<S>(
cx: Scope,
) -> Action<S, Result<S::Output, ServerFnError>>
pub fn create_server_action<S>(cx: Scope) -> Action<S, Result<S::Output, ServerFnError>>
where
S: Clone + ServerFn,
{

View File

@@ -78,7 +78,9 @@
//! 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;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -93,6 +95,7 @@ mod action;
mod multi_action;
pub use action::*;
pub use multi_action::*;
#[cfg(any(feature = "ssr", doc))]
use std::{
collections::HashMap,
@@ -100,10 +103,7 @@ use std::{
};
#[cfg(any(feature = "ssr", doc))]
type ServerFnTraitObj = dyn Fn(
Scope,
&[u8],
) -> Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
type ServerFnTraitObj = dyn Fn(Scope, &[u8]) -> Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
+ Send
+ Sync;
@@ -302,18 +302,16 @@ where
// serialize the output
let result = match Self::encoding() {
Encoding::Url => match serde_json::to_string(&result)
.map_err(|e| {
ServerFnError::Serialization(e.to_string())
}) {
.map_err(|e| ServerFnError::Serialization(e.to_string()))
{
Ok(r) => Payload::Url(r),
Err(e) => return Err(e),
},
Encoding::Cbor => {
let mut buffer: Vec<u8> = Vec::new();
match ciborium::ser::into_writer(&result, &mut buffer)
.map_err(|e| {
ServerFnError::Serialization(e.to_string())
}) {
.map_err(|e| ServerFnError::Serialization(e.to_string()))
{
Ok(_) => Payload::Binary(buffer),
Err(e) => return Err(e),
}
@@ -321,8 +319,7 @@ where
};
Ok(result)
})
as Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
}) as Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
});
// store it in the hashmap
@@ -335,9 +332,8 @@ where
// return Err
match prev {
Some(_) => Err(ServerFnError::Registration(format!(
"There was already a server function registered at {:?}. This \
can happen if you use the same server function name in two \
different modules
"There was already a server function registered at {:?}. \
This can happen if you use the same server function name in two different modules
on `stable` or in `release` mode.",
Self::url()
))),
@@ -383,7 +379,7 @@ where
T: serde::Serialize + serde::de::DeserializeOwned + Sized,
{
use ciborium::ser::into_writer;
use js_sys::Uint8Array;
use leptos_dom::js_sys::Uint8Array;
use serde_json::Deserializer as JSONDeserializer;
#[derive(Debug)]
@@ -456,7 +452,6 @@ where
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
let mut deserializer = JSONDeserializer::from_str(&text);
T::deserialize(&mut deserializer)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
T::deserialize(&mut deserializer).map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}

View File

@@ -1,7 +1,6 @@
use crate::{ServerFn, ServerFnError};
use leptos_reactive::{
create_rw_signal, spawn_local, store_value, ReadSignal, RwSignal, Scope,
StoredValue,
create_rw_signal, spawn_local, store_value, ReadSignal, RwSignal, Scope, StoredValue,
};
use std::{future::Future, pin::Pin, rc::Rc};
@@ -47,16 +46,15 @@ use std::{future::Future, pin::Pin, rc::Rc};
/// # run_scope(create_runtime(), |cx| {
/// // if there's a single argument, just use that
/// let action1 = create_multi_action(cx, |input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_multi_action(cx, |input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 =
/// create_multi_action(cx, |input: &(usize, String)| async { todo!() });
/// let action3 = create_multi_action(cx, |input: &(usize, String)| async { todo!() });
/// # });
/// ```
pub struct MultiAction<I, O>(StoredValue<MultiActionState<I, O>>)
@@ -271,22 +269,18 @@ where
/// # run_scope(create_runtime(), |cx| {
/// // if there's a single argument, just use that
/// let action1 = create_multi_action(cx, |input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_multi_action(cx, |input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 =
/// create_multi_action(cx, |input: &(usize, String)| async { todo!() });
/// let action3 = create_multi_action(cx, |input: &(usize, String)| async { todo!() });
/// # });
/// ```
pub fn create_multi_action<I, O, F, Fu>(
cx: Scope,
action_fn: F,
) -> MultiAction<I, O>
pub fn create_multi_action<I, O, F, Fu>(cx: Scope, action_fn: F) -> MultiAction<I, O>
where
I: 'static,
O: 'static,
@@ -319,16 +313,14 @@ where
///
/// #[server(MyServerFn)]
/// async fn my_server_fn() -> Result<(), ServerFnError> {
/// todo!()
/// todo!()
/// }
///
/// # run_scope(create_runtime(), |cx| {
/// let my_server_multi_action = create_server_multi_action::<MyServerFn>(cx);
/// # });
/// ```
pub fn create_server_multi_action<S>(
cx: Scope,
) -> MultiAction<S, Result<S::Output, ServerFnError>>
pub fn create_server_multi_action<S>(cx: Scope) -> MultiAction<S, Result<S::Output, ServerFnError>>
where
S: Clone + ServerFn,
{

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.2.0-alpha"
version = "0.1.3"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -12,7 +12,6 @@ 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

@@ -34,21 +34,19 @@ impl std::fmt::Debug for BodyContext {
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
/// let (prefers_dark, set_prefers_dark) = create_signal(cx, false);
/// let body_class = move || {
/// if prefers_dark() {
/// "dark".to_string()
/// } else {
/// "light".to_string()
/// }
/// };
/// provide_meta_context(cx);
/// let (prefers_dark, set_prefers_dark) = create_signal(cx, false);
/// let body_class = move || if prefers_dark() {
/// "dark".to_string()
/// } else {
/// "light".to_string()
/// };
///
/// view! { cx,
/// <main>
/// <Body class=body_class/>
/// </main>
/// }
/// view! { cx,
/// <main>
/// <Body class=body_class/>
/// </main>
/// }
/// }
/// ```
#[component(transparent)]

View File

@@ -39,13 +39,13 @@ impl std::fmt::Debug for HtmlContext {
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>
/// <Html lang="he" dir="rtl"/>
/// </main>
/// }
/// view! { cx,
/// <main>
/// <Html lang="he" dir="rtl"/>
/// </main>
/// }
/// }
/// ```
#[component(transparent)]

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