Compare commits

...

22 Commits

Author SHA1 Message Date
Greg Johnston
e5fc3eccec CI: narrow scope of Wasm testing 2023-02-12 19:02:51 -05:00
Greg Johnston
147770b714 regression test 2023-02-12 18:58:07 -05:00
Greg Johnston
236807135e fix: misnamed import 2023-02-12 18:56:35 -05:00
Greg Johnston
1cba54d47e fix: <For/> in todomvc example (#504) 2023-02-11 16:30:09 -05:00
Greg Johnston
d1ae3b49cc docs: further additions (#505) 2023-02-11 15:55:43 -05:00
Greg Johnston
6bab4ad966 apply new formatting everywhere (#502) 2023-02-11 14:30:06 -05:00
jquesada2016
d4648da5c6 chore: add workspace rustfmt.tml (#483) 2023-02-11 14:25:55 -05:00
Greg Johnston
cf7deaaea3 fix: proper disposal of nested route scopes (#499) 2023-02-11 14:12:59 -05:00
g-re-g
d0cacecfc6 Allow literal string as class in view macro (#500) 2023-02-10 22:43:40 -05:00
Greg Johnston
ce2c3ec97c examples: remove unused index.html (#497) 2023-02-10 08:02:26 -05:00
martin frances
b9f05f94ce chore: remove unused .clone() call in <Suspense/>. (#486) 2023-02-08 20:44:10 -05:00
Greg Johnston
fe7aacb0c8 Handle <ErrorBoundary/> hydration correctly (closes #456) 2023-02-08 20:32:59 -05:00
Greg Johnston
3fd3e73a10 Correctly handle custom elements in SSR 2023-02-08 20:32:59 -05:00
Greg Johnston
7dca740e47 Add error boundary example to list 2023-02-08 20:32:59 -05:00
Greg Johnston
73420affed Basic error boundary example 2023-02-08 20:32:59 -05:00
Greg Johnston
7c25f59a68 Update README.md 2023-02-08 20:32:32 -05:00
Greg Johnston
c24874d9c8 change: add Scope to view function in <For/> to avoid memory "leak" (#492) 2023-02-08 20:28:04 -05:00
Greg Johnston
4759dfcb60 missing ; 2023-02-08 14:34:57 -05:00
Greg Johnston
ca9419b53f fix: fix debug_warn behavior in reactive crate and remove log dependency (#491) 2023-02-08 07:04:01 -05:00
jquesada2016
765006158a change: NodeRef<HtmlElement<Div>> generics to NodeRef<Div> (#481) 2023-02-07 20:13:25 -05:00
Greg Johnston
8a1adaefaf fix: typed route params with #[derive(Params)] (#488) 2023-02-07 17:28:46 -05:00
Greg Johnston
086326324e Fix inner_html in SSR (#487) 2023-02-07 13:14:14 -05:00
115 changed files with 5742 additions and 4427 deletions

View File

@@ -12,13 +12,21 @@ dependencies = ["build", "check-examples", "test"]
[tasks.build]
clear = true
dependencies = ["build-all"]
dependencies = ["build-all", "build-wasm"]
[tasks.build-all]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.build-wasm]
clear = true
dependencies = [
{ name = "build-wasm", path = "leptos_reactive" },
{ name = "build-wasm", path = "leptos_dom" },
{ name = "build-wasm", path = "leptos_server" },
]
[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-alpha", features = ["stable"] }`
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0", features = ["stable"] }`
2. `nightly` enables the function call syntax for accessing and setting signals. If 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)

1
docs/book/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
book

View File

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

View File

@@ -0,0 +1,76 @@
# Interlude: Reactivity and Functions
One of our core contributors said to me recently: “I never used closures this often
until I started using Leptos.” And 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

@@ -187,6 +187,8 @@ 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

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

@@ -66,9 +66,8 @@ pub fn Counters(cx: Scope) -> impl IntoView {
<For
each=counters
key=|counter| counter.0
view=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! {
cx,
view=move |cx, (id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! { cx,
<Counter id value set_value/>
}
}

View File

@@ -72,7 +72,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |(id, (value, set_value))| {
view=move |cx, (id, (value, set_value))| {
view! {
cx,
<Counter id value set_value/>

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

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

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

View File

@@ -1,14 +1,18 @@
mod api;
use crate::api::*;
use leptos::*;
use leptos_router::*;
use crate::api::{get_contact, get_contacts};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct ExampleContext(i32);
#[component]
pub fn RouterExample(cx: Scope) -> impl IntoView {
log::debug!("rendering <RouterExample/>");
// contexts are passed down through the route tree
provide_context(cx, ExampleContext(0));
view! { cx,
<Router>
<nav>
@@ -59,6 +63,13 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
pub fn ContactList(cx: Scope) -> impl IntoView {
log::debug!("rendering <ContactList/>");
// contexts are passed down through the route tree
provide_context(cx, ExampleContext(42));
on_cleanup(cx, || {
log!("cleaning up <ContactList/>");
});
let location = use_location(cx);
let contacts = create_resource(cx, move || location.search.get(), get_contacts);
let contacts = move || {
@@ -86,21 +97,28 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
}
}
#[derive(Params, PartialEq, Clone, Debug)]
pub struct ContactParams {
id: usize,
}
#[component]
pub fn Contact(cx: Scope) -> impl IntoView {
log::debug!("rendering <Contact/>");
let params = use_params_map(cx);
log::debug!(
"ExampleContext should be Some(42). It is {:?}",
use_context::<ExampleContext>(cx)
);
on_cleanup(cx, || {
log!("cleaning up <Contact/>");
});
let params = use_params::<ContactParams>(cx);
let contact = create_resource(
cx,
move || {
params()
.get("id")
.cloned()
.unwrap_or_default()
.parse::<usize>()
.ok()
},
move || params().map(|params| params.id).ok(),
// any of the following would work (they're identical)
// move |id| async move { get_contact(id).await }
// move |id| get_contact(id),
@@ -138,6 +156,16 @@ pub fn Contact(cx: Scope) -> impl IntoView {
#[component]
pub fn About(cx: Scope) -> impl IntoView {
log::debug!("rendering <About/>");
on_cleanup(cx, || {
log!("cleaning up <About/>");
});
log::debug!(
"ExampleContext should be Some(0). It is {:?}",
use_context::<ExampleContext>(cx)
);
// use_navigate allows you to navigate programmatically by calling a function
let navigate = use_navigate(cx);
@@ -159,6 +187,11 @@ pub fn About(cx: Scope) -> impl IntoView {
#[component]
pub fn Settings(cx: Scope) -> impl IntoView {
log::debug!("rendering <Settings/>");
on_cleanup(cx, || {
log!("cleaning up <Settings/>");
});
view! { cx,
<>
<h1>"Settings"</h1>

View File

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

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 |error| {
view= move |cx, error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {

View File

@@ -12,12 +12,15 @@ const STORAGE_KEY: &str = "todos-leptos";
// Basic operations to manipulate the todo list: nothing really interesting here
impl Todos {
pub fn new(cx: Scope) -> Self {
let starting_todos = if let Ok(Some(storage)) = window().local_storage() {
let starting_todos = if let Ok(Some(storage)) = window().local_storage()
{
storage
.get_item(STORAGE_KEY)
.ok()
.flatten()
.and_then(|value| serde_json::from_str::<Vec<TodoSerialized>>(&value).ok())
.and_then(|value| {
serde_json::from_str::<Vec<TodoSerialized>>(&value).ok()
})
.map(|values| {
values
.into_iter()
@@ -89,7 +92,12 @@ impl Todo {
Self::new_with_completed(cx, id, title, false)
}
pub fn new_with_completed(cx: Scope, id: Uuid, title: String, completed: bool) -> Self {
pub fn new_with_completed(
cx: Scope,
id: Uuid,
title: String,
completed: bool,
) -> Self {
// RwSignal combines the getter and setter in one struct, rather than separating
// the getter from the setter. This makes it more convenient in some cases, such
// as when we're putting the signals into a struct and passing it around. There's
@@ -129,7 +137,8 @@ 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);
});
@@ -184,7 +193,8 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
.iter()
.map(TodoSerialized::from)
.collect::<Vec<_>>();
let json = serde_json::to_string(&objs).expect("couldn't serialize Todos");
let json =
serde_json::to_string(&objs).expect("couldn't serialize Todos");
if storage.set_item(STORAGE_KEY, &json).is_err() {
log::error!("error while trying to set item in localStorage");
}
@@ -216,7 +226,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
<For
each=filtered_todos
key=|todo| todo.id
view=move |todo: Todo| view! { cx, <Todo todo /> }
view=move |cx, todo: Todo| view! { cx, <Todo todo /> }
/>
</ul>
</section>
@@ -262,7 +272,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
// this will be filled by _ref=input below
let todo_input = NodeRef::<HtmlElement<Input>>::new(cx);
let todo_input = NodeRef::<Input>::new(cx);
let save = move |value: &str| {
let value = value.trim();

View File

@@ -32,11 +32,19 @@ pub struct ResponseParts {
impl ResponseParts {
/// Insert a header, overwriting any previous value with the same key
pub fn insert_header(&mut self, key: header::HeaderName, value: header::HeaderValue) {
pub fn insert_header(
&mut self,
key: header::HeaderName,
value: header::HeaderValue,
) {
self.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub fn append_header(&mut self, key: header::HeaderName, value: header::HeaderValue) {
pub fn append_header(
&mut self,
key: header::HeaderName,
value: header::HeaderValue,
) {
self.headers.append(key, value);
}
}
@@ -60,13 +68,21 @@ impl ResponseOptions {
res_parts.status = Some(status);
}
/// Insert a header, overwriting any previous value with the same key
pub fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue) {
pub fn insert_header(
&self,
key: header::HeaderName,
value: header::HeaderValue,
) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub fn append_header(&self, key: header::HeaderName, value: header::HeaderValue) {
pub fn append_header(
&self,
key: header::HeaderName,
value: header::HeaderValue,
) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
@@ -81,7 +97,8 @@ pub fn redirect(cx: leptos::Scope, path: &str) {
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
header::HeaderValue::from_str(path)
.expect("Failed to create HeaderValue"),
);
}
@@ -173,7 +190,8 @@ pub fn handle_server_fns_with_context(
match server_fn(cx, body).await {
Ok(serialized) => {
let res_options = use_context::<ResponseOptions>(cx).unwrap();
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
@@ -183,7 +201,8 @@ pub fn handle_server_fns_with_context(
let mut res_parts = res_options.0.write();
if accept_header == Some("application/json")
|| accept_header == Some("application/x-www-form-urlencoded")
|| accept_header
== Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = HttpResponse::Ok();
@@ -221,7 +240,9 @@ pub fn handle_server_fns_with_context(
res.body(Bytes::from(data))
}
Payload::Url(data) => {
res.content_type("application/x-www-form-urlencoded");
res.content_type(
"application/x-www-form-urlencoded",
);
res.body(data)
}
Payload::Json(data) => {
@@ -230,13 +251,15 @@ pub fn handle_server_fns_with_context(
}
}
}
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
Err(e) => HttpResponse::InternalServerError()
.body(e.to_string()),
}
} else {
HttpResponse::BadRequest().body(format!(
"Could not find a server function at the route {:?}. \
\n\nIt's likely that you need to call ServerFn::register() on the \
server function type, somewhere in your `main` function.",
\n\nIt's likely that you need to call \
ServerFn::register() on the server function type, \
somewhere in your `main` function.",
req.path()
))
}
@@ -256,13 +279,13 @@ pub fn handle_server_fns_with_context(
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{HttpServer, App};
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use std::{env,net::SocketAddr};
/// use std::{env, net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
@@ -272,11 +295,17 @@ pub fn handle_server_fns_with_context(
/// let addr = conf.leptos_options.site_addr.clone();
/// HttpServer::new(move || {
/// let leptos_options = &conf.leptos_options;
///
///
/// App::new()
/// // {tail:.*} passes the remainder of the URL as the route
/// // the actual routing will be handled by `leptos_router`
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream(leptos_options.to_owned(), |cx| view! { cx, <MyApp/> }))
/// .route(
/// "/{tail:.*}",
/// leptos_actix::render_app_to_stream(
/// leptos_options.to_owned(),
/// |cx| view! { cx, <MyApp/> },
/// ),
/// )
/// })
/// .bind(&addr)?
/// .run()
@@ -353,14 +382,14 @@ where
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{HttpServer, App};
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use std::{env,net::SocketAddr};
/// use leptos_actix::DataResponse;
/// use std::{env, net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope, data: &'static str) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
@@ -370,14 +399,21 @@ where
/// let addr = conf.leptos_options.site_addr.clone();
/// HttpServer::new(move || {
/// let leptos_options = &conf.leptos_options;
///
///
/// App::new()
/// // {tail:.*} passes the remainder of the URL as the route
/// // the actual routing will be handled by `leptos_router`
/// .route("/{tail:.*}", leptos_actix::render_preloaded_data_app(
/// leptos_options.to_owned(),
/// |req| async move { Ok(DataResponse::Data("async func that can preload data")) },
/// |cx, data| view! { cx, <MyApp data/> })
/// .route(
/// "/{tail:.*}",
/// leptos_actix::render_preloaded_data_app(
/// leptos_options.to_owned(),
/// |req| async move {
/// Ok(DataResponse::Data(
/// "async func that can preload data",
/// ))
/// },
/// |cx, data| view! { cx, <MyApp data/> },
/// ),
/// )
/// })
/// .bind(&addr)?
@@ -430,7 +466,11 @@ where
})
}
fn provide_contexts(cx: leptos::Scope, req: &HttpRequest, res_options: ResponseOptions) {
fn provide_contexts(
cx: leptos::Scope,
req: &HttpRequest,
res_options: ResponseOptions,
) {
let path = leptos_corrected_path(req);
let integration = ServerIntegration { path };
@@ -457,25 +497,27 @@ 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() })
@@ -493,11 +535,13 @@ async fn stream_app(
let res_options = res_options.0.read();
let (status, mut headers) = (res_options.status, res_options.headers.clone());
let (status, mut headers) =
(res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
let complete_stream =
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()]).chain(stream);
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
.chain(stream);
let mut res = HttpResponse::Ok()
.content_type("text/html")
.streaming(complete_stream);
@@ -514,7 +558,10 @@ 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
@@ -578,7 +625,9 @@ fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (S
/// 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,
{
@@ -658,7 +707,12 @@ 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,
@@ -671,7 +725,10 @@ 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
}
@@ -693,7 +750,11 @@ 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,7 +9,10 @@
use axum::{
body::{Body, Bytes, Full, StreamBody},
extract::Path,
http::{header::HeaderName, header::HeaderValue, HeaderMap, Request, StatusCode},
http::{
header::{HeaderName, HeaderValue},
HeaderMap, Request, StatusCode,
},
response::IntoResponse,
routing::get,
};
@@ -21,7 +24,7 @@ use leptos_meta::MetaContext;
use leptos_router::*;
use parking_lot::RwLock;
use std::{io, pin::Pin, sync::Arc};
use tokio::{task::spawn_blocking, task::LocalSet};
use tokio::task::{spawn_blocking, 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
@@ -92,7 +95,8 @@ pub fn redirect(cx: leptos::Scope, path: &str) {
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
header::HeaderValue::from_str(path)
.expect("Failed to create HeaderValue"),
);
}
@@ -118,8 +122,8 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
///
/// ```
/// use axum::{handler::Handler, routing::post, Router};
/// use std::net::SocketAddr;
/// use leptos::*;
/// use std::net::SocketAddr;
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[tokio::main]
@@ -128,7 +132,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`
@@ -196,9 +200,12 @@ 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);
@@ -211,34 +218,43 @@ 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);
}
@@ -246,7 +262,9 @@ 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
@@ -260,16 +278,23 @@ 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)),
}
}
@@ -280,11 +305,13 @@ 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");
@@ -297,7 +324,8 @@ 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.
@@ -310,28 +338,28 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::handler::Handler;
/// use axum::Router;
/// use std::{net::SocketAddr, env};
/// use axum::{handler::Handler, Router};
/// 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`
@@ -354,8 +382,13 @@ 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
@@ -395,8 +428,13 @@ 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
@@ -502,13 +540,16 @@ 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
@@ -522,7 +563,10 @@ 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;
@@ -564,7 +608,8 @@ fn html_parts(options: &LeptosOptions, meta: Option<&MetaContext>) -> (String, &
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}>
@@ -584,7 +629,9 @@ fn html_parts(options: &LeptosOptions, meta: Option<&MetaContext>) -> (String, &
/// 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,
{
@@ -645,7 +692,11 @@ 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;
@@ -696,7 +747,11 @@ 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,6 +1,6 @@
use crate::Children;
use leptos_dom::{Errors, IntoView};
use leptos_macro::component;
use leptos_macro::{component, view};
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,8 +45,16 @@ 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 => fallback(cx, errors).into_view(cx),
false => view! { cx,
<>
{fallback(cx, errors)}
<leptos-error-boundary style="display: none">{children.clone()}</leptos-error-boundary>
</>
}
.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 |counter: Counter| {
/// view=move |cx, counter: Counter| {
/// view! {
/// cx,
/// <button>"Value: " {move || counter.count.get()}</button>
@@ -54,7 +54,7 @@ pub fn For<IF, I, T, EF, N, KF, K>(
where
IF: Fn() -> I + 'static,
I: IntoIterator<Item = T>,
EF: Fn(T) -> N + 'static,
EF: Fn(Scope, T) -> N + 'static,
N: IntoView,
KF: Fn(&T) -> K + 'static,
K: Eq + Hash + 'static,

View File

@@ -34,6 +34,8 @@
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
//! - [`counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable) adapts the `counters` example
//! to show how to use Leptos with `stable` Rust.
//! - [`error_boundary`](https://github.com/leptos-rs/leptos/tree/main/examples/error_boundary) shows how to use
//! `Result` types to handle errors.
//! - [`parent_child`](https://github.com/leptos-rs/leptos/tree/main/examples/parent_child) shows four different
//! ways a parent component can communicate with a child, including passing a closure, context, and more
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) implements the classic to-do
@@ -130,7 +132,7 @@
//!
//! #[component]
//! fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
//! todo!()
//! todo!()
//! }
//!
//! pub fn main() {
@@ -140,14 +142,14 @@
//! ```
pub use leptos_config::*;
pub use leptos_dom;
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};
pub use leptos_dom::*;
pub use leptos_dom::{
self,
wasm_bindgen::{JsCast, UnwrapThrowExt},
*,
};
pub use leptos_macro::*;
pub use leptos_reactive::*;
pub use leptos_server;
pub use leptos_server::*;
pub use leptos_server::{self, *};
pub use tracing;
pub use typed_builder;
mod error_boundary;
@@ -159,10 +161,9 @@ pub use show::*;
mod suspense;
pub use suspense::*;
mod transition;
pub use leptos_dom::debug_warn;
pub use transition::*;
pub use leptos_reactive::debug_warn;
extern crate self as leptos;
/// The most common type for the `children` property on components,
@@ -186,15 +187,14 @@ pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
///
/// #[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,6 +1,5 @@
use cfg_if::cfg_if;
use leptos_dom::HydrationCtx;
use leptos_dom::{DynChild, Fragment, IntoView};
use leptos_dom::{DynChild, Fragment, HydrationCtx, IntoView};
use leptos_macro::component;
use leptos_reactive::{provide_context, Scope, SuspenseContext};
use std::rc::Rc;
@@ -92,7 +91,7 @@ where
let initial = {
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
child.clone()
child
}
// show the fallback, but also prepare to stream HTML
else {

View File

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

View File

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

View File

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

View File

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

4
leptos_dom/Makefile.toml Normal file
View File

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

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=|i| view! { cx, {i} }
view=|cx, i| view! { cx, {i} }
/>
}
.into_view(cx);

View File

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

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,320 +16,349 @@ 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);
mount_child(
MountKind::Before(&closing),
&new_child,
);
}
// If we are not hydrating, we simply mount the child
if !HydrationCtx::is_hydrating() {
mount_child(
MountKind::Before(&closing),
&new_child,
);
}
// We want to update text nodes, rather than replace them, so
// make sure to hold onto the text node
let t = new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
(t, disposer)
}
},
);
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let new_child = child_fn().into_view(cx);
**child.borrow_mut() = Some(new_child);
}
// If we are not hydrating, we simply mount the child
if !HydrationCtx::is_hydrating() {
mount_child(MountKind::Before(&closing), &new_child);
}
component
}
// We want to update text nodes, rather than replace them, so
// make sure to hold onto the text node
let t = new_child.get_text().map(|t| t.node.clone());
// monomorphized outer function
let Self { id, child_fn } = self;
**child_borrow = Some(new_child);
let component = DynChildRepr::new_with_id(id);
let component = create_dyn_view(
cx,
component,
Box::new(move || child_fn().into_view(cx)),
);
(t, disposer)
}
},
);
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let new_child = child_fn().into_view(cx);
**child.borrow_mut() = Some(new_child);
}
component
View::CoreComponent(crate::CoreComponent::DynChild(component))
}
// monomorphized outer function
let Self { id, child_fn } = self;
let component = DynChildRepr::new_with_id(id);
let component = create_dyn_view(
cx,
component,
Box::new(move || child_fn().into_view(cx)),
);
View::CoreComponent(crate::CoreComponent::DynChild(component))
}
}

File diff suppressed because it is too large Load Diff

View File

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

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,135 +14,141 @@ 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;
if ev.cancel_bubble() {
return;
}
}
}
// navigate up tree
let host =
js_sys::Reflect::get(&node, &JsValue::from_str("host"))
.unwrap_throw();
if host.is_truthy()
&& host != node
&& host.dyn_ref::<web_sys::Node>().is_some()
{
node = host;
} else if let Some(parent) =
node.unchecked_into::<web_sys::Node>().parent_node()
{
node = parent.into()
} else {
node = JsValue::null()
}
}
};
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let handler = move |e| {
let _guard = span.enter();
handler(e);
};
}
}
}
// navigate up tree
let host = js_sys::Reflect::get(&node, &JsValue::from_str("host"))
.unwrap_throw();
if host.is_truthy()
&& host != node
&& host.dyn_ref::<web_sys::Node>().is_some()
{
node = host;
} else if let Some(parent) =
node.unchecked_into::<web_sys::Node>().parent_node()
{
node = parent.into()
} else {
node = JsValue::null()
}
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
let handler = Closure::wrap(handler).into_js_value();
_ = crate::window().add_event_listener_with_callback(
&event_name,
handler.unchecked_ref(),
);
// register that we've created handler
events.insert(event_name);
}
};
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let handler = move |e| {
let _guard = span.enter();
handler(e);
};
}
}
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
let handler = Closure::wrap(handler).into_js_value();
_ = crate::window()
.add_event_listener_with_callback(&event_name, handler.unchecked_ref());
// register that we've created handler
events.insert(event_name);
}
})
})
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn event_delegation_key(event_name: &str) -> String {
let event_name = intern(event_name);
let mut n = String::from("$$$");
n.push_str(event_name);
n
let event_name = intern(event_name);
let mut n = String::from("$$$");
n.push_str(event_name);
n
}

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

@@ -4,53 +4,53 @@ use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
/// Sets a property on a DOM element.
pub fn set_property(
el: &web_sys::Element,
prop_name: &str,
value: &Option<JsValue>,
el: &web_sys::Element,
prop_name: &str,
value: &Option<JsValue>,
) {
let key = JsValue::from_str(prop_name);
match value {
Some(value) => _ = js_sys::Reflect::set(el, &key, value),
None => _ = js_sys::Reflect::delete_property(el, &key),
};
let key = JsValue::from_str(prop_name);
match value {
Some(value) => _ = js_sys::Reflect::set(el, &key, value),
None => _ = js_sys::Reflect::delete_property(el, &key),
};
}
/// Gets the value of a property set on a DOM element.
pub fn get_property(
el: &web_sys::Element,
prop_name: &str,
el: &web_sys::Element,
prop_name: &str,
) -> Result<JsValue, JsValue> {
let key = JsValue::from_str(prop_name);
js_sys::Reflect::get(el, &key)
let key = JsValue::from_str(prop_name);
js_sys::Reflect::get(el, &key)
}
/// Returns the current [`window.location`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
pub fn location() -> web_sys::Location {
window().location()
window().location()
}
/// Current [`window.location.hash`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location)
/// without the beginning #.
pub fn location_hash() -> Option<String> {
if is_server() {
None
} else {
location().hash().ok().map(|hash| hash.replace('#', ""))
}
if is_server() {
None
} else {
location().hash().ok().map(|hash| hash.replace('#', ""))
}
}
/// Current [`window.location.pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
pub fn location_pathname() -> Option<String> {
location().pathname().ok()
location().pathname().ok()
}
/// Helper function to extract [`Event.target`](https://developer.mozilla.org/en-US/docs/Web/API/Event/target)
/// from any event.
pub fn event_target<T>(event: &web_sys::Event) -> T
where
T: JsCast,
T: JsCast,
{
event.target().unwrap_throw().unchecked_into::<T>()
event.target().unwrap_throw().unchecked_into::<T>()
}
/// Helper function to extract `event.target.value` from an event.
@@ -58,60 +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.
@@ -121,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.
@@ -143,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.
@@ -157,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`.
@@ -185,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

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

@@ -51,25 +51,25 @@ cfg_if! {
/// A stable identifer within the server-rendering or hydration process.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct HydrationKey {
/// The key of the previous component.
pub previous: String,
/// The element offset within the current component.
pub offset: usize,
/// The key of the previous component.
pub previous: String,
/// The element offset within the current component.
pub offset: usize,
}
impl Display for HydrationKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.previous, self.offset)
}
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.previous, self.offset)
}
}
impl Default for HydrationKey {
fn default() -> Self {
Self {
previous: "0-".to_string(),
offset: 0,
fn default() -> Self {
Self {
previous: "0-".to_string(),
offset: 0,
}
}
}
}
thread_local!(static ID: RefCell<HydrationKey> = Default::default());
@@ -78,65 +78,65 @@ thread_local!(static ID: RefCell<HydrationKey> = Default::default());
pub struct HydrationCtx;
impl HydrationCtx {
/// Get the next `id` without incrementing it.
pub fn peek() -> HydrationKey {
ID.with(|id| id.borrow().clone())
}
/// Increments the current hydration `id` and returns it
pub fn id() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
id.offset = id.offset.wrapping_add(1);
id.clone()
})
}
/// Resets the hydration `id` for the next component, and returns it
pub fn next_component() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
let offset = id.offset;
id.previous.push_str(&offset.to_string());
id.previous.push('-');
id.offset = 0;
id.clone()
})
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub fn reset_id() {
ID.with(|id| *id.borrow_mut() = Default::default());
}
/// Resums hydration from the provided `id`. Usefull for
/// `Suspense` and other fancy things.
pub fn continue_from(id: HydrationKey) {
ID.with(|i| *i.borrow_mut() = id);
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn stop_hydrating() {
IS_HYDRATING.with(|is_hydrating| {
std::mem::take(&mut *is_hydrating.borrow_mut());
})
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn is_hydrating() -> bool {
IS_HYDRATING.with(|is_hydrating| **is_hydrating.borrow())
}
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
#[cfg(debug_assertions)]
return format!("_{id}{}", if closing { 'c' } else { 'o' });
#[cfg(not(debug_assertions))]
{
let _ = closing;
format!("_{id}")
/// Get the next `id` without incrementing it.
pub fn peek() -> HydrationKey {
ID.with(|id| id.borrow().clone())
}
/// Increments the current hydration `id` and returns it
pub fn id() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
id.offset = id.offset.wrapping_add(1);
id.clone()
})
}
/// Resets the hydration `id` for the next component, and returns it
pub fn next_component() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
let offset = id.offset;
id.previous.push_str(&offset.to_string());
id.previous.push('-');
id.offset = 0;
id.clone()
})
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub fn reset_id() {
ID.with(|id| *id.borrow_mut() = Default::default());
}
/// 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,6 +1,5 @@
use std::rc::Rc;
use leptos_reactive::Scope;
use std::rc::Rc;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::UnwrapThrowExt;
@@ -10,225 +9,227 @@ use wasm_bindgen::UnwrapThrowExt;
/// 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();
/// Converts the attribute to its HTML value at that moment, including the attribute name,
/// so it can be rendered on the server.
pub fn as_value_string(&self, attr_name: &'static str) -> String {
match self {
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
value = f();
}
value.as_value_string(attr_name)
}
Attribute::Option(_, value) => value
.as_ref()
.map(|value| format!("{attr_name}=\"{value}\""))
.unwrap_or_default(),
Attribute::Bool(include) => {
if *include {
attr_name.to_string()
} else {
String::new()
}
}
}
value.as_value_string(attr_name)
}
Attribute::Option(_, value) => value
.as_ref()
.map(|value| format!("{attr_name}=\"{value}\""))
.unwrap_or_default(),
Attribute::Bool(include) => {
if *include {
attr_name.to_string()
} else {
String::new()
}
}
}
}
/// Converts the attribute to its HTML value at that moment, not including
/// the attribute name, so it can be rendered on the server.
pub fn as_nameless_value_string(&self) -> Option<String> {
match self {
Attribute::String(value) => Some(value.to_string()),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
value = f();
/// Converts the attribute to its HTML value at that moment, not including
/// the attribute name, so it can be rendered on the server.
pub fn as_nameless_value_string(&self) -> Option<String> {
match self {
Attribute::String(value) => Some(value.to_string()),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
value = f();
}
value.as_nameless_value_string()
}
Attribute::Option(_, value) => {
value.as_ref().map(|value| value.to_string())
}
Attribute::Bool(include) => {
if *include {
Some("".to_string())
} else {
None
}
}
}
value.as_nameless_value_string()
}
Attribute::Option(_, value) => {
value.as_ref().map(|value| value.to_string())
}
Attribute::Bool(include) => {
if *include {
Some("".to_string())
} else {
None
}
}
}
}
}
impl PartialEq for Attribute {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::String(l0), Self::String(r0)) => l0 == r0,
(Self::Fn(_, _), Self::Fn(_, _)) => false,
(Self::Option(_, l0), Self::Option(_, r0)) => l0 == r0,
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
_ => false,
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::String(l0), Self::String(r0)) => l0 == r0,
(Self::Fn(_, _), Self::Fn(_, _)) => false,
(Self::Option(_, l0), Self::Option(_, r0)) => l0 == r0,
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
_ => false,
}
}
}
}
impl std::fmt::Debug for Attribute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
Self::Fn(_, _) => f.debug_tuple("Fn").finish(),
Self::Option(_, arg0) => f.debug_tuple("Option").field(arg0).finish(),
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
Self::Fn(_, _) => f.debug_tuple("Fn").finish(),
Self::Option(_, arg0) => {
f.debug_tuple("Option").field(arg0).finish()
}
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
}
}
}
}
/// Converts some type into an [Attribute].
///
/// This is implemented by default for Rust primitive and string types.
pub trait IntoAttribute {
/// Converts the object into an [Attribute].
fn into_attribute(self, cx: Scope) -> Attribute;
/// 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);
@@ -253,64 +254,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());
use leptos_reactive::create_render_effect;
match value {
Attribute::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(&el, &name, new.clone());
}
new
});
}
new
});
}
_ => attribute_expression(el, &name, value),
};
_ => attribute_expression(el, &name, value),
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn attribute_expression(
el: &web_sys::Element,
attr_name: &str,
value: Attribute,
el: &web_sys::Element,
attr_name: &str,
value: Attribute,
) {
match value {
Attribute::String(value) => {
let value = wasm_bindgen::intern(&value);
if attr_name == "inner_html" {
el.set_inner_html(value);
} else {
let attr_name = wasm_bindgen::intern(attr_name);
el.set_attribute(attr_name, value).unwrap_throw();
}
}
Attribute::Option(_, value) => {
if attr_name == "inner_html" {
el.set_inner_html(&value.unwrap_or_default());
} else {
let attr_name = wasm_bindgen::intern(attr_name);
match value {
Some(value) => {
match value {
Attribute::String(value) => {
let value = wasm_bindgen::intern(&value);
el.set_attribute(attr_name, value).unwrap_throw();
}
None => el.remove_attribute(attr_name).unwrap_throw(),
if attr_name == "inner_html" {
el.set_inner_html(value);
} else {
let attr_name = wasm_bindgen::intern(attr_name);
el.set_attribute(attr_name, value).unwrap_throw();
}
}
}
Attribute::Option(_, value) => {
if attr_name == "inner_html" {
el.set_inner_html(&value.unwrap_or_default());
} else {
let attr_name = wasm_bindgen::intern(attr_name);
match value {
Some(value) => {
let value = wasm_bindgen::intern(&value);
el.set_attribute(attr_name, value).unwrap_throw();
}
None => el.remove_attribute(attr_name).unwrap_throw(),
}
}
}
Attribute::Bool(value) => {
let attr_name = wasm_bindgen::intern(attr_name);
if value {
el.set_attribute(attr_name, attr_name).unwrap_throw();
} else {
el.remove_attribute(attr_name).unwrap_throw();
}
}
_ => panic!("Remove nested Fn in Attribute"),
}
Attribute::Bool(value) => {
let attr_name = wasm_bindgen::intern(attr_name);
if value {
el.set_attribute(attr_name, attr_name).unwrap_throw();
} else {
el.remove_attribute(attr_name).unwrap_throw();
}
}
_ => panic!("Remove nested Fn in Attribute"),
}
}

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
use leptos_reactive::{create_rw_signal, RwSignal, Scope};
use crate::{ElementDescriptor, HtmlElement};
use leptos_reactive::{create_effect, create_rw_signal, RwSignal, Scope};
use std::cell::Cell;
/// Contains a shared reference to a DOM node creating while using the `view`
/// macro to create your UI.
@@ -7,85 +9,114 @@ use leptos_reactive::{create_rw_signal, RwSignal, Scope};
/// # use leptos::*;
/// #[component]
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
/// let input_ref = NodeRef::<HtmlElement<Input>>::new(cx);
/// let input_ref = NodeRef::<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>
/// }
/// }
/// ```
#[derive(Clone, PartialEq)]
pub struct NodeRef<T: Clone + 'static>(RwSignal<Option<T>>);
pub struct NodeRef<T: ElementDescriptor + 'static>(
RwSignal<Option<HtmlElement<T>>>,
);
impl<T: Clone + 'static> NodeRef<T> {
/// Creates an empty reference.
pub fn new(cx: Scope) -> Self {
Self(create_rw_signal(cx, None))
}
impl<T: ElementDescriptor + '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()
}
/// 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: &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());
});
}
#[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: Clone + 'static> Copy for NodeRef<T> {}
impl<T: ElementDescriptor> Clone for NodeRef<T> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<T: ElementDescriptor + 'static> Copy for NodeRef<T> {}
cfg_if::cfg_if! {
if #[cfg(not(feature = "stable"))] {
impl<T: Clone + 'static> FnOnce<()> for NodeRef<T> {
type Output = Option<T>;
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
type Output = Option<HtmlElement<T>>;
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T: Clone + 'static> FnMut<()> for NodeRef<T> {
impl<T: Clone + ElementDescriptor + 'static> FnMut<()> for NodeRef<T> {
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T: Clone + 'static> Fn<()> for NodeRef<T> {
impl<T: Clone + ElementDescriptor + Clone + 'static> Fn<()> for NodeRef<T> {
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}

View File

@@ -21,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.
@@ -49,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
@@ -69,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
@@ -94,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
@@ -116,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(),
)
(
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) })
}
});
let fragments = FuturesUnordered::new();
for (fragment_id, (key_before, fut)) in pending_fragments {
fragments.push(async move { (fragment_id, key_before, fut.await) })
}
// resources and fragments
// stream HTML for each <Suspense/> as it resolves
// TODO can remove id_before_suspense entirely now
let fragments = fragments.map(|(fragment_id, _, html)| {
// resources and fragments
// stream HTML for each <Suspense/> as it resolves
// TODO can remove id_before_suspense entirely now
let fragments = fragments.map(|(fragment_id, _, html)| {
format!(
r#"
<template id="{fragment_id}f">{html}</template>
@@ -185,24 +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>
@@ -211,258 +211,269 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
__LEPTOS_RESOURCE_RESOLVERS = new Map();
</script>
"#
)
})
// TODO these should be combined again in a way that chains them appropriately
// such that individual resources can resolve before all fragments are done
.chain(fragments)
.chain(resources);
)
})
// TODO these should be combined again in a way that chains them appropriately
// such that individual resources can resolve before all fragments are done
.chain(fragments)
.chain(resources);
(stream, runtime, scope)
(stream, runtime, scope)
}
impl View {
/// Consumes the node and renders it into an HTML string.
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
self.render_to_string_helper()
}
/// Consumes the node and renders it into an HTML string.
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
self.render_to_string_helper()
}
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
match self {
View::Text(node) => node.content,
View::Component(node) => {
let content = || {
node
.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("")
};
cfg_if! {
if #[cfg(debug_assertions)] {
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
HydrationCtx::to_string(&node.id, false),
content(),
HydrationCtx::to_string(&node.id, true),
name = to_kebab_case(&node.name)
).into()
} else {
format!(
r#"{}<!--hk={}-->"#,
content(),
HydrationCtx::to_string(&node.id, true)
).into()
}
}
}
View::Suspense(id, node) => format!(
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
View::CoreComponent(node).render_to_string_helper()
)
.into(),
View::CoreComponent(node) => {
let (id, name, wrap, content) = match node {
CoreComponent::Unit(u) => (
u.id.clone(),
"",
false,
Box::new(move || {
#[cfg(debug_assertions)]
{
format!(
"<!--hk={}|leptos-unit-->",
HydrationCtx::to_string(&u.id, true)
)
.into()
}
#[cfg(not(debug_assertions))]
format!("<!--hk={}-->", HydrationCtx::to_string(&u.id, true))
.into()
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
),
CoreComponent::DynChild(node) => {
let child = node.child.take();
(
node.id,
"dyn-child",
true,
Box::new(move || {
if let Some(child) = *child {
// On debug builds, `DynChild` has two marker nodes,
// so there is no way for the text to be merged with
// surrounding text when the browser parses the HTML,
// but in release, `DynChild` only has a trailing marker,
// and the browser automatically merges the dynamic text
// into one single node, so we need to artificially make the
// browser create the dynamic text as it's own text node
if let View::Text(t) = child {
if !cfg!(debug_assertions) {
format!("<!>{}", t.content).into()
} else {
t.content
}
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
match self {
View::Text(node) => node.content,
View::Component(node) => {
let content = || {
node.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("")
};
cfg_if! {
if #[cfg(debug_assertions)] {
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
HydrationCtx::to_string(&node.id, false),
content(),
HydrationCtx::to_string(&node.id, true),
name = to_kebab_case(&node.name)
).into()
} else {
child.render_to_string_helper()
format!(
r#"{}<!--hk={}-->"#,
content(),
HydrationCtx::to_string(&node.id, true)
).into()
}
} else {
"".into()
}
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
}
View::Suspense(id, node) => format!(
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
View::CoreComponent(node).render_to_string_helper()
)
}
CoreComponent::Each(node) => {
let children = node.children.take();
(
node.id,
"each",
true,
Box::new(move || {
children
.into_iter()
.flatten()
.map(|node| {
let id = node.id;
.into(),
View::CoreComponent(node) => {
let (id, name, wrap, content) = match node {
CoreComponent::Unit(u) => (
u.id.clone(),
"",
false,
Box::new(move || {
#[cfg(debug_assertions)]
{
format!(
"<!--hk={}|leptos-unit-->",
HydrationCtx::to_string(&u.id, true)
)
.into()
}
let content = || node.child.render_to_string_helper();
#[cfg(not(debug_assertions))]
format!(
"<!--hk={}-->",
HydrationCtx::to_string(&u.id, true)
)
.into()
})
as Box<dyn FnOnce() -> Cow<'static, str>>,
),
CoreComponent::DynChild(node) => {
let child = node.child.take();
(
node.id,
"dyn-child",
true,
Box::new(move || {
if let Some(child) = *child {
// On debug builds, `DynChild` has two marker nodes,
// so there is no way for the text to be merged with
// surrounding text when the browser parses the HTML,
// but in release, `DynChild` only has a trailing marker,
// and the browser automatically merges the dynamic text
// into one single node, so we need to artificially make the
// browser create the dynamic text as it's own text node
if let View::Text(t) = child {
if !cfg!(debug_assertions) {
format!("<!>{}", t.content).into()
} else {
t.content
}
} else {
child.render_to_string_helper()
}
} else {
"".into()
}
})
as Box<dyn FnOnce() -> Cow<'static, str>>,
)
}
CoreComponent::Each(node) => {
let children = node.children.take();
(
node.id,
"each",
true,
Box::new(move || {
children
.into_iter()
.flatten()
.map(|node| {
let id = node.id;
#[cfg(debug_assertions)]
{
format!(
let content = || {
node.child.render_to_string_helper()
};
#[cfg(debug_assertions)]
{
format!(
"<!--hk={}|leptos-each-item-start-->{}<!\
--hk={}|leptos-each-item-end-->",
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(&id, true),
)
}
#[cfg(not(debug_assertions))]
format!(
"{}<!--hk={}-->",
content(),
HydrationCtx::to_string(&id, true)
)
})
.join("")
.into()
})
as Box<dyn FnOnce() -> Cow<'static, str>>,
)
}
};
#[cfg(not(debug_assertions))]
format!(
"{}<!--hk={}-->",
content(),
HydrationCtx::to_string(&id, true)
)
})
.join("")
.into()
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
)
}
};
if wrap {
cfg_if! {
if #[cfg(debug_assertions)] {
format!(
r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(&id, true),
).into()
} else {
let _ = name;
if wrap {
cfg_if! {
if #[cfg(debug_assertions)] {
format!(
r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(&id, true),
).into()
} else {
let _ = name;
format!(
r#"{}<!--hk={}-->"#,
content(),
HydrationCtx::to_string(&id, true)
).into()
format!(
r#"{}<!--hk={}-->"#,
content(),
HydrationCtx::to_string(&id, true)
).into()
}
}
} else {
content()
}
}
}
} else {
content()
}
}
View::Element(el) => {
if let Some(prerendered) = el.prerendered {
prerendered
} else {
let tag_name = el.name;
View::Element(el) => {
if let Some(prerendered) = el.prerendered {
prerendered
} else {
let tag_name = el.name;
let mut inner_html = None;
let mut inner_html = None;
let attrs = el
.attrs
.into_iter()
.filter_map(|(name, value)| -> Option<Cow<'static, str>> {
if value.is_empty() {
Some(format!(" {name}").into())
} else if name == "inner_html" {
inner_html = Some(value);
None
} else {
Some(
format!(
let attrs = el
.attrs
.into_iter()
.filter_map(
|(name, value)| -> Option<Cow<'static, str>> {
if value.is_empty() {
Some(format!(" {name}").into())
} else if name == "inner_html" {
inner_html = Some(value);
None
} else {
Some(
format!(
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
.into(),
)
}
})
.join("");
.into(),
)
}
},
)
.join("");
if el.is_void {
format!("<{tag_name}{attrs}/>").into()
} else if let Some(inner_html) = inner_html {
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>").into()
} else {
let children = el
.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("");
if el.is_void {
format!("<{tag_name}{attrs}/>").into()
} else if let Some(inner_html) = inner_html {
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>")
.into()
} else {
let children = el
.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("");
format!("<{tag_name}{attrs}>{children}</{tag_name}>").into()
}
format!("<{tag_name}{attrs}>{children}</{tag_name}>")
.into()
}
}
}
View::Transparent(_) => Default::default(),
}
}
View::Transparent(_) => Default::default(),
}
}
}
#[cfg(debug_assertions)]
fn to_kebab_case(name: &str) -> String {
if name.is_empty() {
return String::new();
}
let mut new_name = String::with_capacity(name.len() + 8);
let mut chars = name.chars();
new_name.push(
chars
.next()
.map(|mut c| {
if c.is_ascii() {
c.make_ascii_lowercase();
}
c
})
.unwrap(),
);
for mut char in chars {
if char.is_ascii_uppercase() {
char.make_ascii_lowercase();
new_name.push('-');
if name.is_empty() {
return String::new();
}
new_name.push(char);
}
let mut new_name = String::with_capacity(name.len() + 8);
new_name
let mut chars = name.chars();
new_name.push(
chars
.next()
.map(|mut c| {
if c.is_ascii() {
c.make_ascii_lowercase();
}
c
})
.unwrap(),
);
for mut char in chars {
if char.is_ascii_uppercase() {
char.make_ascii_lowercase();
new_name.push('-');
}
new_name.push(char);
}
new_name
}
#[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,9 +8,10 @@ use proc_macro_error::ResultExt;
use quote::{format_ident, ToTokens, TokenStreamExt};
use std::collections::HashSet;
use syn::{
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument,
ItemFn, LitStr, Meta, MetaList, MetaNameValue, NestedMeta, Pat, PatIdent, Path, PathArguments,
ReturnType, Type, TypePath, Visibility,
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute,
FnArg, GenericArgument, ItemFn, LitStr, Meta, MetaList, MetaNameValue,
NestedMeta, Pat, PatIdent, Path, PathArguments, ReturnType, Type, TypePath,
Visibility,
};
pub struct Model {
@@ -62,7 +63,8 @@ impl Parse for Model {
item.sig.inputs.iter_mut().for_each(|arg| {
if let FnArg::Typed(ty) = arg {
drain_filter(&mut ty.attrs, |attr| {
attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop)
attr.path == parse_quote!(doc)
|| attr.path == parse_quote!(prop)
});
}
});
@@ -91,7 +93,10 @@ impl Parse for Model {
// implemented manually because Vec::drain_filter is nightly only
// follows std recommended parallel
fn drain_filter<T>(vec: &mut Vec<T>, mut some_predicate: impl FnMut(&mut T) -> bool) {
fn drain_filter<T>(
vec: &mut Vec<T>,
mut some_predicate: impl FnMut(&mut T) -> bool,
) {
let mut i = 0;
while i < vec.len() {
if some_predicate(&mut vec[i]) {
@@ -139,32 +144,33 @@ impl ToTokens for Model {
let prop_names = prop_names(props);
let builder_name_doc =
LitStr::new(&format!("Props for the [`{name}`] component."), name.span());
let builder_name_doc = LitStr::new(
&format!("Props for the [`{name}`] component."),
name.span(),
);
let component_fn_prop_docs = generate_component_fn_prop_docs(props);
let (tracing_instrument_attr, tracing_span_expr, tracing_guard_expr) = if cfg!(
feature = "tracing"
) {
(
quote! {
#[cfg_attr(
debug_assertions,
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
)]
},
quote! {
let span = ::leptos::leptos_dom::tracing::Span::current();
},
quote! {
#[cfg(debug_assertions)]
let _guard = span.entered();
},
)
} else {
(quote! {}, quote! {}, quote! {})
};
let (tracing_instrument_attr, tracing_span_expr, tracing_guard_expr) =
if cfg!(feature = "tracing") {
(
quote! {
#[cfg_attr(
debug_assertions,
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
)]
},
quote! {
let span = ::leptos::leptos_dom::tracing::Span::current();
},
quote! {
#[cfg(debug_assertions)]
let _guard = span.entered();
},
)
} else {
(quote! {}, quote! {}, quote! {})
};
let component = if *is_transparent {
quote! {
@@ -249,11 +255,16 @@ impl Prop {
.attrs
.iter()
.enumerate()
.filter_map(|(i, attr)| PropOpt::from_attribute(attr).map(|opt| (i, opt)))
.filter_map(|(i, attr)| {
PropOpt::from_attribute(attr).map(|opt| (i, opt))
})
.fold(HashSet::new(), |mut acc, cur| {
// Make sure opts aren't repeated
if acc.intersection(&cur.1).next().is_some() {
abort!(typed.attrs[cur.0], "`#[prop]` options are repeated");
abort!(
typed.attrs[cur.0],
"`#[prop]` options are repeated"
);
}
acc.extend(cur.1);
@@ -262,10 +273,13 @@ impl Prop {
});
// Make sure conflicting options are not present
if prop_opts.contains(&PropOpt::Optional) && prop_opts.contains(&PropOpt::OptionalNoStrip) {
if prop_opts.contains(&PropOpt::Optional)
&& prop_opts.contains(&PropOpt::OptionalNoStrip)
{
abort!(
typed,
"`optional` and `optional_no_strip` options are mutually exclusive"
"`optional` and `optional_no_strip` options are mutually \
exclusive"
);
} else if prop_opts.contains(&PropOpt::Optional)
&& prop_opts.contains(&PropOpt::StripOption)
@@ -279,7 +293,8 @@ impl Prop {
{
abort!(
typed,
"`optional_no_strip` and `strip_option` options are mutually exclusive"
"`optional_no_strip` and `strip_option` options are mutually \
exclusive"
);
}
@@ -288,8 +303,8 @@ impl Prop {
} else {
abort!(
typed.pat,
"only `prop: bool` style types are allowed within the `#[component]` \
macro"
"only `prop: bool` style types are allowed within the \
`#[component]` macro"
);
};
@@ -333,7 +348,8 @@ impl Docs {
.iter()
.enumerate()
.map(|(idx, attr)| {
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) = attr.parse_meta().unwrap()
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) =
attr.parse_meta().unwrap()
{
let doc_str = quote!(#doc);
@@ -368,7 +384,8 @@ impl Docs {
.0
.iter()
.map(|attr| {
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) = attr.parse_meta().unwrap()
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) =
attr.parse_meta().unwrap()
{
let mut doc_str = quote!(#doc).to_string();
@@ -403,15 +420,17 @@ enum PropOpt {
impl PropOpt {
fn from_attribute(attr: &Attribute) -> Option<HashSet<Self>> {
const ABORT_OPT_MESSAGE: &str = "only `optional`, `optional_no_strip`, \
`strip_option`, `default` and `into` are \
allowed as arguments to `#[prop()]`";
const ABORT_OPT_MESSAGE: &str =
"only `optional`, `optional_no_strip`, `strip_option`, `default` \
and `into` are allowed as arguments to `#[prop()]`";
if attr.path != parse_quote!(prop) {
return None;
}
if let Meta::List(MetaList { nested, .. }) = attr.parse_meta().unwrap_or_abort() {
if let Meta::List(MetaList { nested, .. }) =
attr.parse_meta().unwrap_or_abort()
{
Some(
nested
.iter()
@@ -473,7 +492,8 @@ struct TypedBuilderOpts {
impl TypedBuilderOpts {
fn from_opts(opts: &HashSet<PropOpt>, is_ty_option: bool) -> Self {
Self {
default: opts.contains(&PropOpt::Optional) || opts.contains(&PropOpt::OptionalNoStrip),
default: opts.contains(&PropOpt::Optional)
|| opts.contains(&PropOpt::OptionalNoStrip),
default_with_value: opts.iter().find_map(|p| match p {
PropOpt::OptionalWithDefault(v) => Some(v.to_owned()),
_ => None,
@@ -531,7 +551,8 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
ty,
} = prop;
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
let builder_attrs =
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);
@@ -566,7 +587,8 @@ fn generate_component_fn_prop_docs(props: &[Prop]) -> TokenStream {
let optional_prop_docs = props
.iter()
.filter(|Prop { prop_opts, .. }| {
prop_opts.contains(&PropOpt::Optional) || prop_opts.contains(&PropOpt::OptionalNoStrip)
prop_opts.contains(&PropOpt::Optional)
|| prop_opts.contains(&PropOpt::OptionalNoStrip)
})
.map(|p| prop_to_doc(p, PropDocStyle::List))
.collect::<TokenStream>();
@@ -613,8 +635,8 @@ fn is_option(ty: &Type) -> bool {
fn unwrap_option(ty: &Type) -> Option<Type> {
const STD_OPTION_MSG: &str =
"make sure you're not shadowing the `std::option::Option` type that is \
automatically imported from the standard prelude";
"make sure you're not shadowing the `std::option::Option` type that \
is automatically imported from the standard prelude";
if let Type::Path(TypePath {
path: Path { segments, .. },
@@ -623,9 +645,9 @@ fn unwrap_option(ty: &Type) -> Option<Type> {
{
if let [first] = &segments.iter().collect::<Vec<_>>()[..] {
if first.ident == "Option" {
if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
args, ..
}) = &first.arguments
if let PathArguments::AngleBracketed(
AngleBracketedGenericArguments { args, .. },
) = &first.arguments
{
if let [first] = &args.iter().collect::<Vec<_>>()[..] {
if let GenericArgument::Type(ty) = first {
@@ -706,7 +728,11 @@ fn prop_to_doc(
&if !prop_opts.contains(&PropOpt::Into) {
format!("- **{}**: [`{}`]", quote!(#name), pretty_ty)
} else {
format!("- **{}**: `impl`[`Into<{}>`]", quote!(#name), pretty_ty)
format!(
"- **{}**: `impl`[`Into<{}>`]",
quote!(#name),
pretty_ty
)
},
name.ident.span(),
);

View File

@@ -19,7 +19,10 @@ pub(crate) enum Mode {
impl Default for Mode {
fn default() -> Self {
if cfg!(feature = "hydrate") || cfg!(feature = "csr") || cfg!(feature = "web") {
if cfg!(feature = "hydrate")
|| cfg!(feature = "csr")
|| cfg!(feature = "web")
{
Mode::Client
} else {
Mode::Ssr
@@ -213,8 +216,8 @@ mod server;
/// ```
///
/// 9. You can add the same class to every element in the view by passing in a special
/// `class = {/* ... */}` argument after `cx, `. This is useful for injecting a class
/// providing by a scoped styling library.
/// `class = {/* ... */},` argument after `cx, `. This is useful for injecting a class
/// provided by a scoped styling library.
/// ```rust
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
@@ -282,22 +285,33 @@ pub fn view(tokens: TokenStream) -> TokenStream {
let mut tokens = tokens.into_iter();
let (cx, comma) = (tokens.next(), tokens.next());
match (cx, comma) {
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct))) if punct.as_char() == ',' => {
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct)))
if punct.as_char() == ',' =>
{
let first = tokens.next();
let second = tokens.next();
let third = tokens.next();
let fourth = tokens.next();
let global_class = match (&first, &second, &third, &fourth) {
(
Some(TokenTree::Ident(first)),
Some(TokenTree::Punct(eq)),
Some(val),
Some(TokenTree::Punct(comma)),
) if *first == "class"
&& eq.to_string() == '='.to_string()
&& comma.to_string() == ','.to_string() =>
let global_class = match (&first, &second) {
(Some(TokenTree::Ident(first)), Some(TokenTree::Punct(eq)))
if *first == "class" && eq.as_char() == '=' =>
{
Some(val.clone())
match &fourth {
Some(TokenTree::Punct(comma))
if comma.as_char() == ',' =>
{
third.clone()
}
_ => {
let error_msg = concat!(
"To create a scope class with the view! macro \
you must put a comma `,` after the value.\n",
"e.g., view!{cx, class=\"my-class\", \
<div>...</div>}"
);
panic!("{error_msg}")
}
}
}
_ => None,
};
@@ -323,7 +337,10 @@ 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> }}"
)
}
}
}
@@ -348,33 +365,34 @@ pub fn view(tokens: TokenStream) -> TokenStream {
///
/// #[component]
/// fn HelloComponent(
/// cx: Scope,
/// /// The user's name.
/// name: String,
/// /// The user's age.
/// age: u8
/// cx: Scope,
/// /// The user's name.
/// name: String,
/// /// The user's age.
/// age: u8,
/// ) -> impl IntoView {
/// // create the signals (reactive values) that will update the UI
/// let (age, set_age) = create_signal(cx, age);
/// // increase `age` by 1 every second
/// set_interval(move || {
/// set_age.update(|age| *age += 1)
/// }, Duration::from_secs(1));
///
/// // return the user interface, which will be automatically updated
/// // when signal values change
/// view! { cx,
/// <p>"Your name is " {name} " and you are " {age} " years old."</p>
/// }
/// // create the signals (reactive values) that will update the UI
/// let (age, set_age) = create_signal(cx, age);
/// // increase `age` by 1 every second
/// set_interval(
/// move || set_age.update(|age| *age += 1),
/// Duration::from_secs(1),
/// );
///
/// // return the user interface, which will be automatically updated
/// // when signal values change
/// view! { cx,
/// <p>"Your name is " {name} " and you are " {age} " years old."</p>
/// }
/// }
///
/// #[component]
/// fn App(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <main>
/// <HelloComponent name="Greg".to_string() age=32/>
/// </main>
/// }
/// view! { cx,
/// <main>
/// <HelloComponent name="Greg".to_string() age=32/>
/// </main>
/// }
/// }
/// ```
///
@@ -399,11 +417,15 @@ pub fn view(tokens: TokenStream) -> TokenStream {
///
/// // PascalCase: Generated component will be called MyComponent
/// #[component]
/// fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
/// fn MyComponent(cx: Scope) -> impl IntoView {
/// todo!()
/// }
///
/// // snake_case: Generated component will be called MySnakeCaseComponent
/// #[component]
/// fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
/// fn my_snake_case_component(cx: Scope) -> impl IntoView {
/// todo!()
/// }
/// ```
///
/// 3. The macro generates a type `ComponentProps` for every `Component` (so, `HomePage` generates `HomePageProps`,
@@ -416,22 +438,28 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// use component::{MyComponent, MyComponentProps};
///
/// mod component {
/// use leptos::*;
/// use leptos::*;
///
/// #[component]
/// pub fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
/// #[component]
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
/// todo!()
/// }
/// }
/// ```
/// ```
/// # use leptos::*;
///
/// use snake_case_component::{MySnakeCaseComponent, MySnakeCaseComponentProps};
/// use snake_case_component::{
/// MySnakeCaseComponent, MySnakeCaseComponentProps,
/// };
///
/// mod snake_case_component {
/// use leptos::*;
/// use leptos::*;
///
/// #[component]
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
/// #[component]
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView {
/// todo!()
/// }
/// }
/// ```
///
@@ -451,8 +479,10 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// # use leptos::*;
/// #[component]
/// fn MyComponent<T>(cx: Scope, render_prop: T) -> impl IntoView
/// where T: Fn() -> HtmlElement<Div> {
/// todo!()
/// where
/// T: Fn() -> HtmlElement<Div>,
/// {
/// todo!()
/// }
/// ```
///
@@ -465,26 +495,26 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// # use leptos::*;
/// #[component]
/// fn ComponentWithChildren(cx: Scope, children: Children) -> impl IntoView {
/// view! {
/// cx,
/// <ul>
/// {children(cx)
/// .nodes
/// .into_iter()
/// .map(|child| view! { cx, <li>{child}</li> })
/// .collect::<Vec<_>>()}
/// </ul>
/// }
/// view! {
/// cx,
/// <ul>
/// {children(cx)
/// .nodes
/// .into_iter()
/// .map(|child| view! { cx, <li>{child}</li> })
/// .collect::<Vec<_>>()}
/// </ul>
/// }
/// }
///
/// #[component]
/// fn WrapSomeChildren(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <ComponentWithChildren>
/// "Ooh, look at us!"
/// <span>"We're being projected!"</span>
/// </ComponentWithChildren>
/// }
/// view! { cx,
/// <ComponentWithChildren>
/// "Ooh, look at us!"
/// <span>"We're being projected!"</span>
/// </ComponentWithChildren>
/// }
/// }
/// ```
///
@@ -506,30 +536,27 @@ pub fn view(tokens: TokenStream) -> TokenStream {
///
/// #[component]
/// pub fn MyComponent(
/// cx: Scope,
/// #[prop(into)]
/// name: String,
/// #[prop(optional)]
/// optional_value: Option<i32>,
/// #[prop(optional_no_strip)]
/// optional_no_strip: Option<i32>
/// cx: Scope,
/// #[prop(into)] name: String,
/// #[prop(optional)] optional_value: Option<i32>,
/// #[prop(optional_no_strip)] optional_no_strip: Option<i32>,
/// ) -> impl IntoView {
/// // whatever UI you need
/// // whatever UI you need
/// }
///
/// #[component]
/// #[component]
/// pub fn App(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <MyComponent
/// name="Greg" // automatically converted to String with `.into()`
/// optional_value=42 // received as `Some(42)`
/// optional_no_strip=Some(42) // received as `Some(42)`
/// />
/// <MyComponent
/// name="Bob" // automatically converted to String with `.into()`
/// // optional values can both be omitted, and received as `None`
/// />
/// }
/// view! { cx,
/// <MyComponent
/// name="Greg" // automatically converted to String with `.into()`
/// optional_value=42 // received as `Some(42)`
/// optional_no_strip=Some(42) // received as `Some(42)`
/// />
/// <MyComponent
/// name="Bob" // automatically converted to String with `.into()`
/// // optional values can both be omitted, and received as `None`
/// />
/// }
/// }
/// ```
#[proc_macro_error::proc_macro_error]
@@ -627,7 +654,9 @@ pub fn derive_prop(input: TokenStream) -> TokenStream {
// 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)
}

View File

@@ -29,7 +29,7 @@ pub fn impl_params(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let gen = quote! {
impl Params for #name {
fn from_map(map: &::leptos_router::ParamsMap) -> Result<Self, ::leptos_router::RouterError> {
fn from_map(map: &::leptos_router::ParamsMap) -> Result<Self, ::leptos_router::ParamsError> {
Ok(Self {
#(#fields,)*
})

View File

@@ -4,14 +4,14 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::{DeriveInput, Error, Result};
use syn::{spanned::Spanned, DeriveInput, Error, Result};
pub fn impl_derive_prop(ast: &DeriveInput) -> Result<TokenStream> {
let data = match &ast.data {
syn::Data::Struct(data) => match &data.fields {
syn::Fields::Named(fields) => {
let struct_info = struct_info::StructInfo::new(ast, fields.named.iter())?;
let struct_info =
struct_info::StructInfo::new(ast, fields.named.iter())?;
let builder_creation = struct_info.builder_creation_impl()?;
let conversion_helper = struct_info.conversion_helper_impl()?;
let fields = struct_info
@@ -48,26 +48,34 @@ pub fn impl_derive_prop(ast: &DeriveInput) -> Result<TokenStream> {
}
},
syn::Data::Enum(_) => {
return Err(Error::new(ast.span(), "Prop is not supported for enums"))
return Err(Error::new(
ast.span(),
"Prop is not supported for enums",
))
}
syn::Data::Union(_) => {
return Err(Error::new(ast.span(), "Prop is not supported for unions"))
return Err(Error::new(
ast.span(),
"Prop is not supported for unions",
))
}
};
Ok(data)
}
mod struct_info {
use super::{
field_info::{FieldBuilderAttr, FieldInfo},
util::{
empty_type, empty_type_tuple, expr_to_single_string,
make_punctuated_single, modify_types_generics_hack,
path_to_single_string, strip_raw_ident_prefix, type_tuple,
},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::Error;
use super::field_info::{FieldBuilderAttr, FieldInfo};
use super::util::{
empty_type, empty_type_tuple, expr_to_single_string, make_punctuated_single,
modify_types_generics_hack, path_to_single_string, strip_raw_ident_prefix, type_tuple,
};
#[derive(Debug)]
pub struct StructInfo<'a> {
pub vis: &'a syn::Visibility,
@@ -93,17 +101,27 @@ mod struct_info {
fields: impl Iterator<Item = &'a syn::Field>,
) -> Result<StructInfo<'a>, Error> {
let builder_attr = TypeBuilderAttr::new(&ast.attrs)?;
let builder_name = strip_raw_ident_prefix(format!("{}Builder", ast.ident));
let builder_name =
strip_raw_ident_prefix(format!("{}Builder", ast.ident));
Ok(StructInfo {
vis: &ast.vis,
name: &ast.ident,
generics: &ast.generics,
fields: fields
.enumerate()
.map(|(i, f)| FieldInfo::new(i, f, builder_attr.field_defaults.clone()))
.map(|(i, f)| {
FieldInfo::new(
i,
f,
builder_attr.field_defaults.clone(),
)
})
.collect::<Result<_, _>>()?,
builder_attr,
builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()),
builder_name: syn::Ident::new(
&builder_name,
proc_macro2::Span::call_site(),
),
conversion_helper_trait_name: syn::Ident::new(
&format!("{builder_name}_Optional"),
proc_macro2::Span::call_site(),
@@ -115,7 +133,10 @@ mod struct_info {
})
}
fn modify_generics<F: FnMut(&mut syn::Generics)>(&self, mut mutator: F) -> syn::Generics {
fn modify_generics<F: FnMut(&mut syn::Generics)>(
&self,
mut mutator: F,
) -> syn::Generics {
let mut generics = self.generics.clone();
mutator(&mut generics);
generics
@@ -128,38 +149,51 @@ mod struct_info {
ref builder_name,
..
} = *self;
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
let (impl_generics, ty_generics, where_clause) =
self.generics.split_for_impl();
let all_fields_param = syn::GenericParam::Type(
syn::Ident::new("PropFields", proc_macro2::Span::call_site()).into(),
syn::Ident::new("PropFields", proc_macro2::Span::call_site())
.into(),
);
let b_generics = self.modify_generics(|g| {
g.params.insert(0, all_fields_param.clone());
});
let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type()));
let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
});
let phantom_generics = self.generics.params.iter().map(|param| match param {
syn::GenericParam::Lifetime(lifetime) => {
let lifetime = &lifetime.lifetime;
quote!(::core::marker::PhantomData<&#lifetime ()>)
}
syn::GenericParam::Type(ty) => {
let ty = &ty.ident;
quote!(::core::marker::PhantomData<#ty>)
}
syn::GenericParam::Const(_cnst) => {
quote!()
}
});
let builder_method_doc = match self.builder_attr.builder_method_doc {
let empties_tuple =
type_tuple(self.included_fields().map(|_| empty_type()));
let generics_with_empty =
modify_types_generics_hack(&ty_generics, |args| {
args.insert(
0,
syn::GenericArgument::Type(
empties_tuple.clone().into(),
),
);
});
let phantom_generics =
self.generics.params.iter().map(|param| match param {
syn::GenericParam::Lifetime(lifetime) => {
let lifetime = &lifetime.lifetime;
quote!(::core::marker::PhantomData<&#lifetime ()>)
}
syn::GenericParam::Type(ty) => {
let ty = &ty.ident;
quote!(::core::marker::PhantomData<#ty>)
}
syn::GenericParam::Const(_cnst) => {
quote!()
}
});
let builder_method_doc = match self.builder_attr.builder_method_doc
{
Some(ref doc) => quote!(#doc),
None => {
let doc = format!(
"
Create a builder for building `{name}`.
On the builder, call {setters} to set the values of the fields.
Finally, call `.build()` to create the instance of `{name}`.
On the builder, call {setters} to set the values of the \
fields.
Finally, call `.build()` to create the instance of \
`{name}`.
",
name = self.name,
setters = {
@@ -172,7 +206,8 @@ mod struct_info {
} else {
write!(&mut result, ", ").unwrap();
}
write!(&mut result, "`.{}(...)`", field.name).unwrap();
write!(&mut result, "`.{}(...)`", field.name)
.unwrap();
if field.builder_attr.default.is_some() {
write!(&mut result, "(optional)").unwrap();
}
@@ -188,8 +223,9 @@ mod struct_info {
Some(ref doc) => quote!(#[doc = #doc]),
None => {
let doc = format!(
"Builder for [`{name}`] instances.\n\nSee [`{name}::builder()`] for more info."
);
"Builder for [`{name}`] instances.\n\nSee \
[`{name}::builder()`] for more info."
);
quote!(#[doc = #doc])
}
}
@@ -197,8 +233,11 @@ mod struct_info {
quote!(#[doc(hidden)])
};
let (b_generics_impl, b_generics_ty, b_generics_where_extras_predicates) =
b_generics.split_for_impl();
let (
b_generics_impl,
b_generics_ty,
b_generics_where_extras_predicates,
) = b_generics.split_for_impl();
let mut b_generics_where: syn::WhereClause = syn::parse2(quote! {
where PropFields: Clone
})?;
@@ -266,7 +305,10 @@ mod struct_info {
})
}
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
pub fn field_impl(
&self,
field: &FieldInfo,
) -> Result<TokenStream, Error> {
let StructInfo {
ref builder_name, ..
} = *self;
@@ -296,7 +338,9 @@ mod struct_info {
syn::parse(quote!(#ident).into()).unwrap()
}
syn::GenericParam::Lifetime(lifetime_def) => {
syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone())
syn::GenericArgument::Lifetime(
lifetime_def.lifetime.clone(),
)
}
syn::GenericParam::Const(const_param) => {
let ident = const_param.ident.clone();
@@ -319,11 +363,17 @@ mod struct_info {
.elems
.push_value(f.tuplized_type_ty_param());
} else {
g.params
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
g.params.insert(
index_after_lifetime_in_generics,
f.generic_ty_param(),
);
let generic_argument: syn::Type = f.type_ident();
ty_generics_tuple.elems.push_value(generic_argument.clone());
target_generics_tuple.elems.push_value(generic_argument);
ty_generics_tuple
.elems
.push_value(generic_argument.clone());
target_generics_tuple
.elems
.push_value(generic_argument);
}
ty_generics_tuple.elems.push_punct(Default::default());
target_generics_tuple.elems.push_punct(Default::default());
@@ -362,26 +412,29 @@ mod struct_info {
} else {
field_type
};
let (arg_type, arg_expr) = if field.builder_attr.setter.auto_into.is_some() {
(
quote!(impl ::core::convert::Into<#arg_type>),
quote!(#field_name.into()),
)
} else {
(quote!(#arg_type), quote!(#field_name))
};
let (param_list, arg_expr) =
if let Some(transform) = &field.builder_attr.setter.transform {
let params = transform.params.iter().map(|(pat, ty)| quote!(#pat: #ty));
let body = &transform.body;
(quote!(#(#params),*), quote!({ #body }))
} else if field.builder_attr.setter.strip_option.is_some() {
(quote!(#field_name: #arg_type), quote!(Some(#arg_expr)))
let (arg_type, arg_expr) =
if field.builder_attr.setter.auto_into.is_some() {
(
quote!(impl ::core::convert::Into<#arg_type>),
quote!(#field_name.into()),
)
} else {
(quote!(#field_name: #arg_type), arg_expr)
(quote!(#arg_type), quote!(#field_name))
};
let (param_list, arg_expr) = if let Some(transform) =
&field.builder_attr.setter.transform
{
let params =
transform.params.iter().map(|(pat, ty)| quote!(#pat: #ty));
let body = &transform.body;
(quote!(#(#params),*), quote!({ #body }))
} else if field.builder_attr.setter.strip_option.is_some() {
(quote!(#field_name: #arg_type), quote!(Some(#arg_expr)))
} else {
(quote!(#field_name: #arg_type), arg_expr)
};
let repeated_fields_error_type_name = syn::Ident::new(
&format!(
"{}_Error_Repeated_field_{}",
@@ -390,7 +443,8 @@ mod struct_info {
),
proc_macro2::Span::call_site(),
);
let repeated_fields_error_message = format!("Repeated field {field_name}");
let repeated_fields_error_message =
format!("Repeated field {field_name}");
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
@@ -421,7 +475,10 @@ mod struct_info {
})
}
pub fn required_field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
pub fn required_field_impl(
&self,
field: &FieldInfo,
) -> Result<TokenStream, Error> {
let StructInfo {
ref name,
ref builder_name,
@@ -442,7 +499,9 @@ mod struct_info {
syn::parse(quote!(#ident).into()).unwrap()
}
syn::GenericParam::Lifetime(lifetime_def) => {
syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone())
syn::GenericArgument::Lifetime(
lifetime_def.lifetime.clone(),
)
}
syn::GenericParam::Const(const_param) => {
let ident = &const_param.ident;
@@ -464,11 +523,14 @@ mod struct_info {
// whether or not `f` is set.
assert!(
f.ordinal != field.ordinal,
"`required_field_impl` called for optional field {}",
"`required_field_impl` called for optional field \
{}",
field.name
);
g.params
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
g.params.insert(
index_after_lifetime_in_generics,
f.generic_ty_param(),
);
builder_generics_tuple.elems.push_value(f.type_ident());
} else if f.ordinal < field.ordinal {
// Only add a `build` method that warns about missing `field` if `f` is set.
@@ -484,8 +546,10 @@ mod struct_info {
// missing we will show a warning for `field` and
// not for `f` - which means this warning should appear whether
// or not `f` is set.
g.params
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
g.params.insert(
index_after_lifetime_in_generics,
f.generic_ty_param(),
);
builder_generics_tuple.elems.push_value(f.type_ident());
}
@@ -512,7 +576,8 @@ mod struct_info {
),
proc_macro2::Span::call_site(),
);
let early_build_error_message = format!("Missing required field {field_name}");
let early_build_error_message =
format!("Missing required field {field_name}");
Ok(quote! {
#[doc(hidden)]
@@ -551,24 +616,31 @@ mod struct_info {
lifetimes: None,
modifier: syn::TraitBoundModifier::None,
path: syn::PathSegment {
ident: self.conversion_helper_trait_name.clone(),
ident: self
.conversion_helper_trait_name
.clone(),
arguments: syn::PathArguments::AngleBracketed(
syn::AngleBracketedGenericArguments {
colon2_token: None,
lt_token: Default::default(),
args: make_punctuated_single(syn::GenericArgument::Type(
field.ty.clone(),
)),
args: make_punctuated_single(
syn::GenericArgument::Type(
field.ty.clone(),
),
),
gt_token: Default::default(),
},
),
}
.into(),
};
let mut generic_param: syn::TypeParam = field.generic_ident.clone().into();
let mut generic_param: syn::TypeParam =
field.generic_ident.clone().into();
generic_param.bounds.push(trait_ref.into());
g.params
.insert(index_after_lifetime_in_generics, generic_param.into());
g.params.insert(
index_after_lifetime_in_generics,
generic_param.into(),
);
}
}
});
@@ -576,21 +648,22 @@ mod struct_info {
let (_, ty_generics, where_clause) = self.generics.split_for_impl();
let modified_ty_generics = modify_types_generics_hack(&ty_generics, |args| {
args.insert(
0,
syn::GenericArgument::Type(
type_tuple(self.included_fields().map(|field| {
if field.builder_attr.default.is_some() {
field.type_ident()
} else {
field.tuplized_type_ty_param()
}
}))
.into(),
),
);
});
let modified_ty_generics =
modify_types_generics_hack(&ty_generics, |args| {
args.insert(
0,
syn::GenericArgument::Type(
type_tuple(self.included_fields().map(|field| {
if field.builder_attr.default.is_some() {
field.type_ident()
} else {
field.tuplized_type_ty_param()
}
}))
.into(),
),
);
});
let descructuring = self.included_fields().map(|f| f.name);
@@ -620,8 +693,10 @@ mod struct_info {
None => {
// I'd prefer “a” or “an” to “its”, but determining which is grammatically
// correct is roughly impossible.
let doc =
format!("Finalise the builder and create its [`{name}`] instance");
let doc = format!(
"Finalise the builder and create its [`{name}`] \
instance"
);
quote!(#[doc = #doc])
}
}
@@ -668,7 +743,9 @@ mod struct_info {
pub fn new(attrs: &[syn::Attribute]) -> Result<TypeBuilderAttr, Error> {
let mut result = TypeBuilderAttr::default();
for attr in attrs {
if path_to_single_string(&attr.path).as_deref() != Some("builder") {
if path_to_single_string(&attr.path).as_deref()
!= Some("builder")
{
continue;
}
@@ -687,7 +764,10 @@ mod struct_info {
}
}
_ => {
return Err(Error::new_spanned(attr.tokens.clone(), "Expected (<...>)"));
return Err(Error::new_spanned(
attr.tokens.clone(),
"Expected (<...>)",
));
}
}
}
@@ -698,8 +778,14 @@ mod struct_info {
fn apply_meta(&mut self, expr: syn::Expr) -> Result<(), Error> {
match expr {
syn::Expr::Assign(assign) => {
let name = expr_to_single_string(&assign.left)
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
let name = expr_to_single_string(&assign.left).ok_or_else(
|| {
Error::new_spanned(
&assign.left,
"Expected identifier",
)
},
)?;
match name.as_str() {
"builder_method_doc" => {
self.builder_method_doc = Some(*assign.right);
@@ -722,8 +808,10 @@ mod struct_info {
}
}
syn::Expr::Path(path) => {
let name = path_to_single_string(&path.path)
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
let name =
path_to_single_string(&path.path).ok_or_else(|| {
Error::new_spanned(&path, "Expected identifier")
})?;
match name.as_str() {
"doc" => {
self.doc = true;
@@ -736,19 +824,22 @@ mod struct_info {
}
}
syn::Expr::Call(call) => {
let subsetting_name = if let syn::Expr::Path(path) = &*call.func {
path_to_single_string(&path.path)
} else {
None
}
.ok_or_else(|| {
let call_func = &call.func;
let call_func = quote!(#call_func);
Error::new_spanned(
&call.func,
format!("Illegal builder setting group {call_func}"),
)
})?;
let subsetting_name =
if let syn::Expr::Path(path) = &*call.func {
path_to_single_string(&path.path)
} else {
None
}
.ok_or_else(|| {
let call_func = &call.func;
let call_func = quote!(#call_func);
Error::new_spanned(
&call.func,
format!(
"Illegal builder setting group {call_func}"
),
)
})?;
match subsetting_name.as_str() {
"field_defaults" => {
for arg in call.args {
@@ -758,7 +849,10 @@ mod struct_info {
}
_ => Err(Error::new_spanned(
&call.func,
format!("Illegal builder setting group name {subsetting_name}"),
format!(
"Illegal builder setting group name \
{subsetting_name}"
),
)),
}
}
@@ -769,14 +863,13 @@ mod struct_info {
}
mod field_info {
use super::util::{
expr_to_single_string, ident_to_type, path_to_single_string,
strip_raw_ident_prefix,
};
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::parse::Error;
use syn::spanned::Spanned;
use super::util::{
expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
};
use syn::{parse::Error, spanned::Spanned};
#[derive(Debug)]
pub struct FieldInfo<'a> {
@@ -798,7 +891,10 @@ mod field_info {
ordinal,
name,
generic_ident: syn::Ident::new(
&format!("__{}", strip_raw_ident_prefix(name.to_string())),
&format!(
"__{}",
strip_raw_ident_prefix(name.to_string())
),
Span::call_site(),
),
ty: &field.ty,
@@ -843,12 +939,16 @@ mod field_info {
return None;
}
let generic_params =
if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments {
if let syn::PathArguments::AngleBracketed(generic_params) =
&segment.arguments
{
generic_params
} else {
return None;
};
if let syn::GenericArgument::Type(ty) = generic_params.args.first()? {
if let syn::GenericArgument::Type(ty) =
generic_params.args.first()?
{
Some(ty)
} else {
None
@@ -874,7 +974,9 @@ mod field_info {
impl FieldBuilderAttr {
pub fn with(mut self, attrs: &[syn::Attribute]) -> Result<Self, Error> {
for attr in attrs {
if path_to_single_string(&attr.path).as_deref() != Some("builder") {
if path_to_single_string(&attr.path).as_deref()
!= Some("builder")
{
continue;
}
@@ -893,7 +995,10 @@ mod field_info {
}
}
_ => {
return Err(Error::new_spanned(attr.tokens.clone(), "Expected (<...>)"));
return Err(Error::new_spanned(
attr.tokens.clone(),
"Expected (<...>)",
));
}
}
}
@@ -906,8 +1011,14 @@ mod field_info {
pub fn apply_meta(&mut self, expr: syn::Expr) -> Result<(), Error> {
match expr {
syn::Expr::Assign(assign) => {
let name = expr_to_single_string(&assign.left)
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
let name = expr_to_single_string(&assign.left).ok_or_else(
|| {
Error::new_spanned(
&assign.left,
"Expected identifier",
)
},
)?;
match name.as_str() {
"default" => {
self.default = Some(*assign.right);
@@ -920,13 +1031,23 @@ mod field_info {
}) = *assign.right
{
use std::str::FromStr;
let tokenized_code = TokenStream::from_str(&code.value())?;
let tokenized_code =
TokenStream::from_str(&code.value())?;
self.default = Some(
syn::parse(tokenized_code.into())
.map_err(|e| Error::new_spanned(code, format!("{e}")))?,
syn::parse(tokenized_code.into()).map_err(
|e| {
Error::new_spanned(
code,
format!("{e}"),
)
},
)?,
);
} else {
return Err(Error::new_spanned(assign.right, "Expected string"));
return Err(Error::new_spanned(
assign.right,
"Expected string",
));
}
Ok(())
}
@@ -937,13 +1058,18 @@ mod field_info {
}
}
syn::Expr::Path(path) => {
let name = path_to_single_string(&path.path)
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
let name =
path_to_single_string(&path.path).ok_or_else(|| {
Error::new_spanned(&path, "Expected identifier")
})?;
match name.as_str() {
"default" => {
self.default = Some(
syn::parse(quote!(::core::default::Default::default()).into())
.unwrap(),
syn::parse(
quote!(::core::default::Default::default())
.into(),
)
.unwrap(),
);
Ok(())
}
@@ -954,19 +1080,22 @@ mod field_info {
}
}
syn::Expr::Call(call) => {
let subsetting_name = if let syn::Expr::Path(path) = &*call.func {
path_to_single_string(&path.path)
} else {
None
}
.ok_or_else(|| {
let call_func = &call.func;
let call_func = quote!(#call_func);
Error::new_spanned(
&call.func,
format!("Illegal builder setting group {call_func}"),
)
})?;
let subsetting_name =
if let syn::Expr::Path(path) = &*call.func {
path_to_single_string(&path.path)
} else {
None
}
.ok_or_else(|| {
let call_func = &call.func;
let call_func = quote!(#call_func);
Error::new_spanned(
&call.func,
format!(
"Illegal builder setting group {call_func}"
),
)
})?;
match subsetting_name.as_ref() {
"setter" => {
for arg in call.args {
@@ -976,7 +1105,10 @@ mod field_info {
}
_ => Err(Error::new_spanned(
&call.func,
format!("Illegal builder setting group name {subsetting_name}"),
format!(
"Illegal builder setting group name \
{subsetting_name}"
),
)),
}
}
@@ -987,13 +1119,18 @@ mod field_info {
}) => {
if let syn::Expr::Path(path) = *expr {
let name = path_to_single_string(&path.path)
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
.ok_or_else(|| {
Error::new_spanned(&path, "Expected identifier")
})?;
match name.as_str() {
"default" => {
self.default = None;
Ok(())
}
_ => Err(Error::new_spanned(path, "Unknown setting".to_owned())),
_ => Err(Error::new_spanned(
path,
"Unknown setting".to_owned(),
)),
}
} else {
Err(Error::new_spanned(
@@ -1010,15 +1147,22 @@ mod field_info {
if let (Some(skip), None) = (&self.setter.skip, &self.default) {
return Err(Error::new(
*skip,
"#[builder(skip)] must be accompanied by default or default_code",
"#[builder(skip)] must be accompanied by default or \
default_code",
));
}
if let (Some(strip_option), Some(transform)) =
(&self.setter.strip_option, &self.setter.transform)
{
let mut error = Error::new(transform.span, "transform conflicts with strip_option");
error.combine(Error::new(*strip_option, "strip_option set here"));
let mut error = Error::new(
transform.span,
"transform conflicts with strip_option",
);
error.combine(Error::new(
*strip_option,
"strip_option set here",
));
return Err(error);
}
Ok(())
@@ -1029,8 +1173,14 @@ mod field_info {
fn apply_meta(&mut self, expr: syn::Expr) -> Result<(), Error> {
match expr {
syn::Expr::Assign(assign) => {
let name = expr_to_single_string(&assign.left)
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
let name = expr_to_single_string(&assign.left).ok_or_else(
|| {
Error::new_spanned(
&assign.left,
"Expected identifier",
)
},
)?;
match name.as_str() {
"doc" => {
self.doc = Some(*assign.right);
@@ -1040,8 +1190,10 @@ mod field_info {
// if self.strip_option.is_some() {
// return Err(Error::new(assign.span(), "Illegal setting - transform
// conflicts with strip_option")); }
self.transform =
Some(parse_transform_closure(assign.left.span(), &assign.right)?);
self.transform = Some(parse_transform_closure(
assign.left.span(),
&assign.right,
)?);
Ok(())
}
_ => Err(Error::new_spanned(
@@ -1051,8 +1203,10 @@ mod field_info {
}
}
syn::Expr::Path(path) => {
let name = path_to_single_string(&path.path)
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
let name =
path_to_single_string(&path.path).ok_or_else(|| {
Error::new_spanned(&path, "Expected identifier")
})?;
macro_rules! handle_fields {
( $( $flag:expr, $field:ident, $already:expr, $checks:expr; )* ) => {
match name.as_str() {
@@ -1093,7 +1247,9 @@ mod field_info {
}) => {
if let syn::Expr::Path(path) = *expr {
let name = path_to_single_string(&path.path)
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
.ok_or_else(|| {
Error::new_spanned(&path, "Expected identifier")
})?;
match name.as_str() {
"doc" => {
self.doc = None;
@@ -1111,7 +1267,10 @@ mod field_info {
self.strip_option = None;
Ok(())
}
_ => Err(Error::new_spanned(path, "Unknown setting".to_owned())),
_ => Err(Error::new_spanned(
path,
"Unknown setting".to_owned(),
)),
}
} else {
Err(Error::new_spanned(
@@ -1132,16 +1291,25 @@ mod field_info {
span: Span,
}
fn parse_transform_closure(span: Span, expr: &syn::Expr) -> Result<Transform, Error> {
fn parse_transform_closure(
span: Span,
expr: &syn::Expr,
) -> Result<Transform, Error> {
let closure = match expr {
syn::Expr::Closure(closure) => closure,
_ => return Err(Error::new_spanned(expr, "Expected closure")),
};
if let Some(kw) = &closure.asyncness {
return Err(Error::new(kw.span, "Transform closure cannot be async"));
return Err(Error::new(
kw.span,
"Transform closure cannot be async",
));
}
if let Some(kw) = &closure.capture {
return Err(Error::new(kw.span, "Transform closure cannot be move"));
return Err(Error::new(
kw.span,
"Transform closure cannot be move",
));
}
let params = closure
@@ -1216,7 +1384,9 @@ mod util {
.into()
}
pub fn type_tuple(elems: impl Iterator<Item = syn::Type>) -> syn::TypeTuple {
pub fn type_tuple(
elems: impl Iterator<Item = syn::Type>,
) -> syn::TypeTuple {
let mut result = syn::TypeTuple {
paren_token: Default::default(),
elems: elems.collect(),
@@ -1234,7 +1404,9 @@ mod util {
}
}
pub fn make_punctuated_single<T, P: Default>(value: T) -> syn::punctuated::Punctuated<T, P> {
pub fn make_punctuated_single<T, P: Default>(
value: T,
) -> syn::punctuated::Punctuated<T, P> {
let mut punctuated = syn::punctuated::Punctuated::new();
punctuated.push(value);
punctuated
@@ -1245,17 +1417,21 @@ mod util {
mut mutator: F,
) -> syn::AngleBracketedGenericArguments
where
F: FnMut(&mut syn::punctuated::Punctuated<syn::GenericArgument, syn::token::Comma>),
F: FnMut(
&mut syn::punctuated::Punctuated<
syn::GenericArgument,
syn::token::Comma,
>,
),
{
let mut abga: syn::AngleBracketedGenericArguments =
syn::parse(ty_generics.clone().into_token_stream().into()).unwrap_or_else(|_| {
syn::AngleBracketedGenericArguments {
syn::parse(ty_generics.clone().into_token_stream().into())
.unwrap_or_else(|_| syn::AngleBracketedGenericArguments {
colon2_token: None,
lt_token: Default::default(),
args: Default::default(),
gt_token: Default::default(),
}
});
});
mutator(&mut abga.args);
abga
}

View File

@@ -23,7 +23,10 @@ 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,
@@ -57,17 +60,21 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
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! {
@@ -88,7 +95,9 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
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);
@@ -124,10 +133,14 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
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>");

View File

@@ -1,9 +1,8 @@
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};
use crate::{is_component_node, Mode};
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName, NodeValueExpr};
#[derive(Clone, Copy)]
enum TagType {
@@ -149,32 +148,39 @@ pub(crate) fn render_view(
global_class: Option<&TokenTree>,
) -> TokenStream {
if mode == Mode::Ssr {
if nodes.is_empty() {
let span = Span::call_site();
quote_spanned! {
span => leptos::Unit
match nodes.len() {
0 => {
let span = Span::call_site();
quote_spanned! {
span => leptos::Unit
}
}
} 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)
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.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 {
fragment_to_tokens(
cx,
Span::call_site(),
nodes,
true,
TagType::Unknown,
global_class,
)
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,
),
}
}
}
@@ -184,9 +190,12 @@ 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();
@@ -201,7 +210,9 @@ 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)
}
}
}
@@ -262,9 +273,13 @@ fn root_element_to_tokens_ssr(
};
let tag_name = node.name.to_string();
let typed_element_name = {
let camel_cased =
camel_case_tag_name(&tag_name.replace("svg::", "").replace("math::", ""));
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::", ""),
);
Ident::new(&camel_cased, node.name.span())
};
let typed_element_name = if is_svg_element(&tag_name) {
@@ -274,10 +289,19 @@ fn root_element_to_tokens_ssr(
} else {
quote! { #typed_element_name }
};
let full_name = if is_custom_element {
quote! {
leptos::leptos_dom::Custom::new(#tag_name)
}
} else {
quote! {
leptos::leptos_dom::#typed_element_name::default()
}
};
quote! {
{
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_html(cx, leptos::leptos_dom::#typed_element_name::default(), #template)
::leptos::HtmlElement::from_html(cx, #full_name, #template)
}
}
}
@@ -307,9 +331,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 {
attribute_to_tokens_ssr(cx, attr, template, holes, exprs_for_compiler);
inner_html = attribute_to_tokens_ssr(
cx,
attr,
template,
holes,
exprs_for_compiler,
);
}
}
@@ -339,42 +371,54 @@ 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();
holes.push(quote! {
#value.into_view(#cx).render_to_string(#cx),
})
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),
})
}
}
}
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::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::Fragment(_) => todo!(),
_ => {}
}
}
@@ -398,24 +442,29 @@ fn value_to_string(value: &syn_rsx::NodeValueExpr) -> Option<String> {
}
}
fn attribute_to_tokens_ssr(
// returns `inner_html`
fn attribute_to_tokens_ssr<'a>(
cx: &Ident,
node: &NodeAttribute,
node: &'a 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 == "node_ref" {
if name == "ref" || 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::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);
@@ -440,7 +489,8 @@ fn attribute_to_tokens_ssr(
}
}
}
}
};
None
}
fn set_class_attribute_ssr(
@@ -450,30 +500,32 @@ fn set_class_attribute_ssr(
holes: &mut Vec<TokenStream>,
global_class: Option<&TokenTree>,
) {
let static_global_class = match global_class {
Some(TokenTree::Literal(lit)) => lit.to_string(),
_ => String::new(),
};
let dyn_global_class = match global_class {
None => None,
Some(TokenTree::Literal(_)) => None,
Some(val) => Some(val),
let (static_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_class_attr = node
.attributes
.iter()
.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
.filter_map(|a| match a {
Node::Attribute(attr) if attr.key.to_string() == "class" => {
attr.value.as_ref().and_then(value_to_string)
}
_ => None,
})
.chain(std::iter::once(static_global_class))
.chain(Some(static_global_class))
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
@@ -507,7 +559,9 @@ 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 {
@@ -636,7 +690,9 @@ 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)
}
}
}
@@ -674,7 +730,9 @@ fn element_to_tokens(
}
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;
@@ -718,8 +776,12 @@ 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))
@@ -737,7 +799,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 == "node_ref" {
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
let value = node
.value
.as_ref()
@@ -746,7 +808,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
@@ -926,7 +988,8 @@ 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;
@@ -1020,7 +1083,8 @@ 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
@@ -1059,7 +1123,10 @@ 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(
@@ -1260,7 +1327,8 @@ 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,7 +8,6 @@ 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 }
@@ -33,6 +32,7 @@ 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

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

View File

@@ -1,11 +1,10 @@
#![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).
///
@@ -58,7 +57,8 @@ 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,21 +119,25 @@ 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,9 +1,11 @@
#![forbid(unsafe_code)]
use crate::runtime::{with_runtime, RuntimeId};
use crate::{debug_warn, Runtime, Scope, ScopeProperty};
use crate::{
macros::debug_warn,
runtime::{with_runtime, RuntimeId},
Runtime, Scope, ScopeProperty,
};
use cfg_if::cfg_if;
use std::cell::RefCell;
use std::fmt::Debug;
use std::{cell::RefCell, 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
@@ -114,8 +116,10 @@ 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);
@@ -209,7 +213,13 @@ 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,30 +39,30 @@
//! // 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 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 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);
//!
//! // this effect will run whenever count() changes
//! create_effect(cx, move |_| {
//! println!("Count = {}", count());
//! });
//! // 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());
//! });
//! });
//! ```
@@ -136,20 +136,34 @@ 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>;
}
#[doc(hidden)]
#[macro_export]
macro_rules! debug_warn {
($($x:tt)*) => {
{
#[cfg(debug_assertions)]
mod macros {
macro_rules! debug_warn {
($($x:tt)*) => {
{
log::warn!($($x)*)
#[cfg(debug_assertions)]
{
($crate::console_warn(&format_args!($($x)*).to_string()))
}
#[cfg(not(debug_assertions))]
{
($($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,7 +65,10 @@ 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,
{
@@ -270,9 +273,13 @@ 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,10 +1,12 @@
#![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,
@@ -113,7 +115,9 @@ 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 {
@@ -170,17 +174,18 @@ 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();
/// ```
@@ -234,7 +239,9 @@ 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 {
@@ -300,7 +307,8 @@ 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);
@@ -318,21 +326,25 @@ 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),
@@ -365,7 +377,9 @@ 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()
@@ -380,7 +394,9 @@ 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()
@@ -389,15 +405,22 @@ 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()
})
});
}
@@ -413,7 +436,10 @@ 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
}
}
@@ -673,8 +699,16 @@ 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,8 +1,9 @@
#![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;
@@ -34,7 +35,10 @@ 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"))] {
@@ -88,7 +92,10 @@ 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>(
@@ -109,24 +116,38 @@ 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 {
@@ -150,7 +171,9 @@ 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,
@@ -161,13 +184,21 @@ 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,
{
@@ -187,7 +218,10 @@ 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,
{
@@ -224,13 +258,17 @@ 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,7 +14,10 @@ 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
}
@@ -37,7 +40,10 @@ 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)
}
@@ -116,13 +122,20 @@ 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);
});
@@ -161,7 +174,10 @@ 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",
)
}
}
@@ -191,9 +207,10 @@ 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();
}
@@ -210,14 +227,20 @@ 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);
}
}
@@ -236,12 +259,15 @@ 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());
})
}
@@ -311,18 +337,23 @@ 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,
@@ -342,7 +373,8 @@ 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(());
}
@@ -364,7 +396,9 @@ 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,7 +1,10 @@
#![forbid(unsafe_code)]
use std::{cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, rc::Rc};
use crate::{create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSignal};
use crate::{
create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSignal,
};
use std::{
cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, rc::Rc,
};
/// 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
@@ -16,26 +19,29 @@ use crate::{create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSig
/// # 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()
/// ```
@@ -64,8 +70,9 @@ 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, {
@@ -78,7 +85,9 @@ 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,8 +1,9 @@
#![forbid(unsafe_code)]
use crate::{
debug_warn,
macros::debug_warn,
runtime::{with_runtime, RuntimeId},
Runtime, Scope, ScopeProperty, UntrackedGettableSignal, UntrackedSettableSignal,
Runtime, Scope, ScopeProperty, UntrackedGettableSignal,
UntrackedSettableSignal,
};
use cfg_if::cfg_if;
use futures::Stream;
@@ -59,7 +60,10 @@ 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
@@ -285,8 +289,12 @@ 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),
@@ -453,7 +461,10 @@ 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)
}
}
@@ -511,11 +522,17 @@ 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();
@@ -533,7 +550,10 @@ 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)
}
@@ -793,7 +813,10 @@ 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)
}
}
@@ -885,7 +908,11 @@ 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();
/// ```
@@ -916,11 +943,17 @@ 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();
@@ -938,7 +971,10 @@ 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)
}
@@ -1208,7 +1244,9 @@ 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}");
@@ -1246,15 +1284,25 @@ 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,
{
@@ -1269,14 +1317,19 @@ 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,5 +1,8 @@
#![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>`].
@@ -33,9 +36,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);
@@ -112,7 +115,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);
@@ -139,7 +142,10 @@ 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(),
}
@@ -207,7 +213,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);
@@ -294,7 +300,9 @@ 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)
}
}
}
}
@@ -307,9 +315,13 @@ 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()
}
}
}
}
@@ -322,7 +334,9 @@ 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,
}
}
@@ -377,9 +391,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);
@@ -441,7 +455,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);
@@ -527,7 +541,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,7 +114,10 @@ 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(),
}
@@ -209,7 +212,9 @@ 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,4 +1,6 @@
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,5 +1,8 @@
#![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].
///
@@ -109,7 +112,10 @@ 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)
}
@@ -157,7 +163,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;
use std::rc::Rc;
use std::{cell::RefCell, rc::Rc};
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
@@ -35,8 +35,7 @@ fn effect_runs() {
#[cfg(not(feature = "stable"))]
#[test]
fn effect_tracks_memo() {
use std::cell::RefCell;
use std::rc::Rc;
use std::{cell::RefCell, rc::Rc};
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
@@ -66,8 +65,7 @@ fn effect_tracks_memo() {
#[cfg(not(feature = "stable"))]
#[test]
fn untrack_mutes_effect() {
use std::cell::RefCell;
use std::rc::Rc;
use std::{cell::RefCell, rc::Rc};
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);

View File

@@ -1,5 +1,7 @@
#[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,14 +1,13 @@
//#[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"))]
#[test]
fn untracked_set_doesnt_trigger_effect() {
use std::cell::RefCell;
use std::rc::Rc;
use std::{cell::RefCell, rc::Rc};
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
@@ -39,8 +38,7 @@ fn untracked_set_doesnt_trigger_effect() {
#[test]
fn untracked_get_doesnt_trigger_effect() {
use std::cell::RefCell;
use std::rc::Rc;
use std::{cell::RefCell, rc::Rc};
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
@@ -52,7 +50,8 @@ 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

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

View File

@@ -1,6 +1,7 @@
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};
@@ -65,15 +66,16 @@ 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>>)
@@ -259,15 +261,16 @@ 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>
@@ -306,14 +309,16 @@ 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

@@ -80,7 +80,6 @@
pub use form_urlencoded;
use leptos_reactive::*;
use proc_macro2::{Literal, TokenStream};
use quote::TokenStreamExt;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -95,7 +94,6 @@ mod action;
mod multi_action;
pub use action::*;
pub use multi_action::*;
#[cfg(any(feature = "ssr", doc))]
use std::{
collections::HashMap,
@@ -103,7 +101,10 @@ 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,16 +303,18 @@ 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),
}
@@ -319,7 +322,8 @@ 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
@@ -332,8 +336,9 @@ 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()
))),
@@ -452,6 +457,7 @@ 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,6 +1,7 @@
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};
@@ -46,15 +47,16 @@ 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>>)
@@ -269,18 +271,22 @@ 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,
@@ -313,14 +319,16 @@ 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

@@ -34,19 +34,21 @@ 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)]

View File

@@ -16,23 +16,22 @@
//!
//! #[component]
//! fn MyApp(cx: Scope) -> impl IntoView {
//! let (name, set_name) = create_signal(cx, "Alice".to_string());
//! let (name, set_name) = create_signal(cx, "Alice".to_string());
//!
//! view! { cx,
//! <Title
//! // reactively sets document.title when `name` changes
//! text=name
//! // applies the `formatter` function to the `text` value
//! formatter=|text| format!("“{text}” is your name")
//! />
//! <main>
//! <input
//! prop:value=name
//! on:input=move |ev| set_name(event_target_value(&ev))
//! view! { cx,
//! <Title
//! // reactively sets document.title when `name` changes
//! text=name
//! // applies the `formatter` function to the `text` value
//! formatter=|text| format!("“{text}” is your name")
//! />
//! </main>
//! }
//!
//! <main>
//! <input
//! prop:value=name
//! on:input=move |ev| set_name(event_target_value(&ev))
//! />
//! </main>
//! }
//! }
//! ```
//! # Feature Flags
@@ -46,6 +45,7 @@
//! which mode your app is operating in.
use cfg_if::cfg_if;
use leptos::{leptos_dom::debug_warn, *};
use std::{
cell::{Cell, RefCell},
collections::HashMap,
@@ -53,8 +53,6 @@ use std::{
rc::Rc,
};
use leptos::{leptos_dom::debug_warn, *};
mod body;
mod html;
mod link;
@@ -93,7 +91,14 @@ pub struct MetaContext {
pub struct MetaTagsContext {
next_id: Rc<Cell<MetaTagId>>,
#[allow(clippy::type_complexity)]
els: Rc<RefCell<HashMap<String, (HtmlElement<AnyElement>, Scope, Option<web_sys::Element>)>>>,
els: Rc<
RefCell<
HashMap<
String,
(HtmlElement<AnyElement>, Scope, Option<web_sys::Element>),
>,
>,
>,
}
impl std::fmt::Debug for MetaTagsContext {
@@ -109,12 +114,19 @@ impl MetaTagsContext {
self.els
.borrow()
.iter()
.map(|(_, (builder_el, cx, _))| builder_el.clone().into_view(*cx).render_to_string(*cx))
.map(|(_, (builder_el, cx, _))| {
builder_el.clone().into_view(*cx).render_to_string(*cx)
})
.collect()
}
#[doc(hidden)]
pub fn register(&self, cx: Scope, id: String, builder_el: HtmlElement<AnyElement>) {
pub fn register(
&self,
cx: Scope,
id: String,
builder_el: HtmlElement<AnyElement>,
) {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
use leptos::document;
@@ -190,10 +202,11 @@ pub fn use_head(cx: Scope) -> MetaContext {
match use_context::<MetaContext>(cx) {
None => {
debug_warn!(
"use_head() is being called without a MetaContext being provided. \
We'll automatically create and provide one, but if this is being called in a child \
route it may cause bugs. To be safe, you should provide_meta_context(cx) \
somewhere in the root of the app."
"use_head() is being called without a MetaContext being \
provided. We'll automatically create and provide one, but if \
this is being called in a child route it may cause bugs. To \
be safe, you should provide_meta_context(cx) somewhere in \
the root of the app."
);
let meta = MetaContext::new();
provide_context(cx, meta.clone());

View File

@@ -9,18 +9,18 @@ use leptos::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>
/// <Link rel="preload"
/// href="myFont.woff2"
/// as_="font"
/// type_="font/woff2"
/// crossorigin="anonymous"
/// />
/// </main>
/// }
/// view! { cx,
/// <main>
/// <Link rel="preload"
/// href="myFont.woff2"
/// as_="font"
/// type_="font/woff2"
/// crossorigin="anonymous"
/// />
/// </main>
/// }
/// }
/// ```
#[component(transparent)]

View File

@@ -1,6 +1,5 @@
use leptos::{component, IntoView, Scope};
use crate::{use_head, TextProp};
use leptos::{component, IntoView, Scope};
/// Injects an [HTMLMetaElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMetaElement) into the document
/// head to set metadata

View File

@@ -9,15 +9,15 @@ use leptos::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>
/// <Script>
/// "console.log('Hello, world!');"
/// </Script>
/// </main>
/// }
/// view! { cx,
/// <main>
/// <Script>
/// "console.log('Hello, world!');"
/// </Script>
/// </main>
/// }
/// }
/// ```
#[component(transparent)]
@@ -86,7 +86,9 @@ pub fn Script(
for node in frag.nodes {
match node {
View::Text(text) => script.push_str(&text.content),
_ => leptos::warn!("Only text nodes are supported as children of <Script/>."),
_ => leptos::warn!(
"Only text nodes are supported as children of <Script/>."
),
}
}
builder_el.child(script)

View File

@@ -9,15 +9,15 @@ use leptos::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>
/// <Style>
/// "body { font-weight: bold; }"
/// </Style>
/// </main>
/// }
/// view! { cx,
/// <main>
/// <Style>
/// "body { font-weight: bold; }"
/// </Style>
/// </main>
/// }
/// }
/// ```
#[component(transparent)]
@@ -58,7 +58,9 @@ pub fn Style(
for node in frag.nodes {
match node {
View::Text(text) => style.push_str(&text.content),
_ => leptos::warn!("Only text nodes are supported as children of <Style/>."),
_ => leptos::warn!(
"Only text nodes are supported as children of <Style/>."
),
}
}
builder_el.child(style)

View File

@@ -10,13 +10,13 @@ use leptos::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>
/// <Stylesheet href="/style.css"/>
/// </main>
/// }
/// view! { cx,
/// <main>
/// <Stylesheet href="/style.css"/>
/// </main>
/// }
/// }
/// ```
#[component(transparent)]

View File

@@ -55,33 +55,33 @@ where
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
/// let formatter = |text| format!("{text} — Leptos Online");
/// provide_meta_context(cx);
/// let formatter = |text| format!("{text} — Leptos Online");
///
/// view! { cx,
/// <main>
/// <Title formatter/>
/// // ... routing logic here
/// </main>
/// }
/// view! { cx,
/// <main>
/// <Title formatter/>
/// // ... routing logic here
/// </main>
/// }
/// }
///
/// #[component]
/// fn PageA(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <main>
/// <Title text="Page A"/> // sets title to "Page A — Leptos Online"
/// </main>
/// }
/// view! { cx,
/// <main>
/// <Title text="Page A"/> // sets title to "Page A — Leptos Online"
/// </main>
/// }
/// }
///
/// #[component]
/// fn PageB(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <main>
/// <Title text="Page B"/> // sets title to "Page B — Leptos Online"
/// </main>
/// }
/// view! { cx,
/// <main>
/// <Title text="Page B"/> // sets title to "Page B — Leptos Online"
/// </main>
/// }
/// }
/// ```
#[component(transparent)]

View File

@@ -68,12 +68,16 @@ where
let (form, method, action, enctype) = extract_form_attributes(&ev);
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
let form_data =
web_sys::FormData::new_with_form(&form).unwrap_throw();
if let Some(on_form_data) = on_form_data.clone() {
on_form_data(&form_data);
}
let params =
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
web_sys::UrlSearchParams::new_with_str_sequence_sequence(
&form_data,
)
.unwrap_throw();
let action = use_resolved_path(cx, move || action.clone())
.get()
.unwrap_or_default();
@@ -108,8 +112,13 @@ where
}
if resp.status() == 303 {
if let Some(redirect_url) = resp.headers().get("Location") {
_ = navigate(&redirect_url, Default::default());
if let Some(redirect_url) =
resp.headers().get("Location")
{
_ = navigate(
&redirect_url,
Default::default(),
);
}
}
}
@@ -119,7 +128,9 @@ where
// otherwise, GET
else {
let params = params.to_string().as_string().unwrap_or_default();
if navigate(&format!("{action}?{params}"), Default::default()).is_ok() {
if navigate(&format!("{action}?{params}"), Default::default())
.is_ok()
{
ev.prevent_default();
}
}
@@ -179,7 +190,10 @@ where
let action_url = if let Some(url) = action.url() {
url
} else {
debug_warn!("<ActionForm/> action needs a URL. Either use create_server_action() or Action::using_server_fn().");
debug_warn!(
"<ActionForm/> action needs a URL. Either use \
create_server_action() or Action::using_server_fn()."
);
String::new()
};
let version = action.version();
@@ -200,17 +214,21 @@ where
let on_response = Rc::new(move |resp: &web_sys::Response| {
let resp = resp.clone().expect("couldn't get Response");
spawn_local(async move {
let body =
JsFuture::from(resp.text().expect("couldn't get .text() from Response")).await;
let body = JsFuture::from(
resp.text().expect("couldn't get .text() from Response"),
)
.await;
match body {
Ok(json) => {
match O::from_json(
&json.as_string().expect("couldn't get String from JsString"),
&json
.as_string()
.expect("couldn't get String from JsString"),
) {
Ok(res) => value.set(Some(Ok(res))),
Err(e) => {
value.set(Some(Err(ServerFnError::Deserialization(e.to_string()))))
}
Err(e) => value.set(Some(Err(
ServerFnError::Deserialization(e.to_string()),
))),
}
}
Err(e) => log::error!("{e:?}"),
@@ -258,7 +276,10 @@ where
let action = if let Some(url) = multi_action.url() {
url
} else {
debug_warn!("<MultiActionForm/> action needs a URL. Either use create_server_action() or Action::using_server_fn().");
debug_warn!(
"<MultiActionForm/> action needs a URL. Either use \
create_server_action() or Action::using_server_fn()."
);
String::new()
};
@@ -309,10 +330,14 @@ fn extract_form_attributes(
.unwrap_or_default()
.to_lowercase(),
form.get_attribute("enctype")
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string())
.unwrap_or_else(|| {
"application/x-www-form-urlencoded".to_string()
})
.to_lowercase(),
)
} else if let Some(input) = el.dyn_ref::<web_sys::HtmlInputElement>() {
} else if let Some(input) =
el.dyn_ref::<web_sys::HtmlInputElement>()
{
let form = ev
.target()
.unwrap()
@@ -331,11 +356,15 @@ fn extract_form_attributes(
}),
input.get_attribute("enctype").unwrap_or_else(|| {
form.get_attribute("enctype")
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string())
.unwrap_or_else(|| {
"application/x-www-form-urlencoded".to_string()
})
.to_lowercase()
}),
)
} else if let Some(button) = el.dyn_ref::<web_sys::HtmlButtonElement>() {
} else if let Some(button) =
el.dyn_ref::<web_sys::HtmlButtonElement>()
{
let form = ev
.target()
.unwrap()
@@ -354,18 +383,25 @@ fn extract_form_attributes(
}),
button.get_attribute("enctype").unwrap_or_else(|| {
form.get_attribute("enctype")
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string())
.unwrap_or_else(|| {
"application/x-www-form-urlencoded".to_string()
})
.to_lowercase()
}),
)
} else {
leptos_dom::debug_warn!("<Form/> cannot be submitted from a tag other than <form>, <input>, or <button>");
leptos_dom::debug_warn!(
"<Form/> cannot be submitted from a tag other than \
<form>, <input>, or <button>"
);
panic!()
}
}
None => match ev.target() {
None => {
leptos_dom::debug_warn!("<Form/> SubmitEvent fired without a target.");
leptos_dom::debug_warn!(
"<Form/> SubmitEvent fired without a target."
);
panic!()
}
Some(form) => {
@@ -375,8 +411,9 @@ fn extract_form_attributes(
form.get_attribute("method")
.unwrap_or_else(|| "get".to_string()),
form.get_attribute("action").unwrap_or_default(),
form.get_attribute("enctype")
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string()),
form.get_attribute("enctype").unwrap_or_else(|| {
"application/x-www-form-urlencoded".to_string()
}),
)
}
},
@@ -386,7 +423,9 @@ fn extract_form_attributes(
fn action_input_from_form_data<I: serde::de::DeserializeOwned>(
form_data: &web_sys::FormData,
) -> Result<I, serde_urlencoded::de::Error> {
let data = web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data).unwrap_throw();
let data =
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
.unwrap_throw();
let data = data.to_string().as_string().unwrap_or_default();
serde_urlencoded::from_str::<I>(&data)
}

View File

@@ -1,7 +1,5 @@
use leptos::leptos_dom::IntoView;
use leptos::*;
use crate::{use_location, use_resolved_path, State};
use leptos::{leptos_dom::IntoView, *};
/// Describes a value that is either a static or a reactive URL, i.e.,
/// a [String], a [&str], or a reactive `Fn() -> String`.

View File

@@ -1,7 +1,6 @@
use std::{cell::Cell, rc::Rc};
use crate::use_route;
use leptos::*;
use std::{cell::Cell, rc::Rc};
/// Displays the child route nested in a parent route, allowing you to control exactly where
/// that child route is displayed. Renders nothing if there is no nested child.
@@ -19,16 +18,21 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
}
set_outlet.set(None);
}
(Some(child), Some((is_showing_val, _))) if child.id() == *is_showing_val => {
(Some(child), Some((is_showing_val, _)))
if child.id() == *is_showing_val =>
{
// do nothing: we don't need to rerender the component, because it's the same
}
(Some(child), prev) => {
if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
prev_scope.dispose();
}
is_showing.set(Some((child.id(), child.cx())));
provide_context(cx, child.clone());
set_outlet.set(Some(child.outlet(cx).into_view(cx)))
_ = cx.child_scope(|child_cx| {
provide_context(child_cx, child.clone());
set_outlet
.set(Some(child.outlet(child_cx).into_view(child_cx)));
is_showing.set(Some((child.id(), child_cx)));
});
}
}
});

View File

@@ -1,14 +1,12 @@
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use leptos::*;
use crate::{
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
ParamsMap, RouterContext,
};
use leptos::*;
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
thread_local! {
static ROUTE_ID: Cell<usize> = Cell::new(0);
@@ -161,7 +159,11 @@ impl RouteContext {
self.inner.params
}
pub(crate) fn base(cx: Scope, path: &str, fallback: Option<fn(Scope) -> View>) -> Self {
pub(crate) fn base(
cx: Scope,
path: &str,
fallback: Option<fn(Scope) -> View>,
) -> Self {
Self {
inner: Rc::new(RouteContextInner {
cx,
@@ -171,14 +173,17 @@ impl RouteContext {
path: RefCell::new(path.to_string()),
original_path: path.to_string(),
params: create_memo(cx, |_| ParamsMap::new()),
outlet: Box::new(move |cx| fallback.as_ref().map(move |f| f(cx))),
outlet: Box::new(move |cx| {
fallback.as_ref().map(move |f| f(cx))
}),
}),
}
}
/// Resolves a relative route, relative to the current route's path.
pub fn resolve_path(&self, to: &str) -> Option<String> {
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.borrow())).map(String::from)
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.borrow()))
.map(String::from)
}
/// The nested child route, if any.

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