Compare commits

..

46 Commits

Author SHA1 Message Date
Greg Johnston
b0f745f4a9 fix: escape </script> and other HTML tags in serialized resources 2023-03-28 20:34:54 -04:00
Ben Wishovich
e6b1298915 feat: add property field to Meta component (#759) 2023-03-28 09:10:00 -04:00
Igor Shevchenko
98a9ec8335 chore(docs): fix a few typos (#756) 2023-03-27 20:06:34 -04:00
jquesada2016
5329561687 feat: add is_mounted and dyn_classes (#714) 2023-03-27 19:03:59 -04:00
Greg Johnston
89ca047f2f examples: improve counter_without_macros (#751) 2023-03-27 12:50:01 -04:00
Greg Johnston
a94711fcf0 fix: correct typecast on Memo::get_untracked (closes issue #754) (#755) 2023-03-27 11:28:40 -04:00
Greg Johnston
97d88c65ae docs: warn when reading resource outside <Suspense/> (closes issue #742) (#743) 2023-03-25 14:22:22 -04:00
Jessie Chatham Spencer
e482e3748d docs: document cargo workspace feature resolver footgun (#745)
Due to no rust edition being present in a workspac's Cargo.toml, non
WASM compatible code can end up being built for a WASM target.

This commit documents this error and how to resolve it.
2023-03-25 07:34:28 -04:00
István Donkó
8ab9c08448 docs: fix typo in server_fn docs (#740) 2023-03-24 21:42:27 -04:00
Lachlan Wilger
56de70b714 docs: fix typo (#739)
There was a typo in the section of the docs that pointed towards the hackernews example, so I fixed it by add the word "application."
2023-03-24 21:41:59 -04:00
Greg Johnston
38d97babd8 fix: always run dynamic classes after static classes (closes #735) (#738) 2023-03-24 17:38:34 -04:00
martin frances
4cfecb5d82 chore: bump serde-lite from 0.3 to 0.4. (#737) 2023-03-24 16:54:20 -04:00
Michael Clayton
08b5970b2b check EventSource value for Ok to avoid unwrap panic (#732) 2023-03-23 18:41:18 -04:00
Greg Johnston
af20f80b2b docs: fix typo in router docs (#730) 2023-03-22 20:44:58 -04:00
Andrew Chang-DeWitt
c2fdd2cd70 fix: include missing query params in navigation when <ActionForm/> receives a redirect (#728)
Previous solution in #727 included manually inserted `?` when a leading
`?` is present automatically in `Url.search`.
2023-03-22 20:05:21 -04:00
Greg Johnston
286f3eebe4 fix: relative routing should update when navigating between <Outlet/>s (closes issue #725) (#729)
* clear some cruft out of the navigation code
* fix issue #725 (correctly reactively resolving paths)
2023-03-22 19:59:08 -04:00
Álvaro Mondéjar
509223ab2e chore: Upgrade console_log to stable (#724) 2023-03-22 18:21:53 -04:00
Greg Johnston
665b0b8ed2 chore: make wasm-bindgen dependency optional in leptos_reactive (#723) 2023-03-22 17:56:52 -04:00
Greg Johnston
508ad52582 chore: fix clippy warnings (#721)
* `v0.2.4`

* chore: fix clippy warnings
2023-03-21 18:20:29 -04:00
martin frances
cfd5c98f97 clippy: simplify Box::pin() call. (#718) 2023-03-21 09:06:31 -04:00
Greg Johnston
2e63bb1f50 fix: <Transition/> behavior (#717) 2023-03-21 09:05:41 -04:00
Greg Johnston
982c8f6b5a docs: small fixes (#715) 2023-03-20 20:43:04 -04:00
Greg Johnston
12c4c115f3 docs: make is_odd less pretentious 2023-03-20 20:42:46 -04:00
Carlton Gibson
d4d20ecdb0 Used modulo rather than bitwise & for is_odd check.
The modulo operator is less of a head-scratcher for folks coming through here. The bitwise & is equally correct (clearly) but is likely to cause confusion if folks don't immediately see what's going on.
2023-03-20 20:09:02 +01:00
Greg Johnston
b78919c6ed Merge pull request #712 from leptos-rs/warnings 2023-03-20 10:49:00 -04:00
Greg Johnston
abb9320e31 chore: clear warning and add exports of helpers with handles 2023-03-20 09:36:14 -04:00
Greg Johnston
875d2d5a3a chore: handle unbounded_send warnings 2023-03-20 09:33:58 -04:00
Greg Johnston
42a58855a0 feat: add Scope::batch() (#711) 2023-03-20 08:29:18 -04:00
Greg Johnston
9d142758ec feat: allow manual signal disposal before the scope is disposed (#710) 2023-03-19 21:40:16 -04:00
Greg Johnston
2faddd85cb feat: add set_interval_with_handle and deprecate set_interval (#709) 2023-03-19 16:45:22 -04:00
martin frances
ddd463748d clippy: less .clone() calls, simpler pointer passing. (#707) 2023-03-19 15:30:12 -04:00
Alexis Fontaine
71ee4cd09d fix: view! macro not compiling with a non-default scope name (#704) 2023-03-19 13:14:47 -04:00
Greg Johnston
08c56f7d6c feat: add a debounce helper for event listeners (#691) 2023-03-19 07:10:56 -04:00
Elliot Waite
e1ba26b62c feat: add request_animation_frame_with_handle and request_idle_callback_with_handle (#698) 2023-03-18 19:09:36 -04:00
Greg Johnston
309f0bf826 fix: ignore view markers in DynChild hydration (closes issue #697) (#703) 2023-03-18 19:01:56 -04:00
Greg Johnston
1698ffa7db fix issues in release mode (closes #700) (#701) 2023-03-18 11:04:06 -04:00
Greg Johnston
556955cf1a docs: beginning work on router docs (#682) 2023-03-18 07:34:43 -04:00
Elliot Waite
a9f778459a examples: remove duplicate console_error_panic_hook::set_once() calls (#692) 2023-03-17 16:27:24 -04:00
Greg Johnston
f2ac412253 feat: support diffing inside component children in hot-reload (#690) 2023-03-17 13:53:53 -04:00
Greg Johnston
3bd52fcc9d fix: hydration errors with <Suspense/> inside components in SSR mode (#688) 2023-03-17 12:46:04 -04:00
Vassil "Vasco" Kolarov
b9bd1e103c examples: added example using Tailwind, CSR (only) and Trunk (#666) 2023-03-17 12:45:49 -04:00
Greg Johnston
55f9081465 fix: allow multiple <Suspense/> on same page during in-order or async rendering (#687) 2023-03-17 12:05:36 -04:00
ryndin32
0bac16dba0 docs: typos (#685) 2023-03-15 16:40:57 -04:00
Brett Etter
a8a9c575b5 Added IntoView for ReadSignal and RwSignal in the stable feature. (#677) 2023-03-15 16:40:22 -04:00
Greg Johnston
15ec855db5 Update README.md 2023-03-15 14:34:18 -04:00
Greg Johnston
b8f79a7e56 fix: suppress spurious hydration warnings for tags in leptos_meta (#684) 2023-03-14 14:17:23 -04:00
112 changed files with 2579 additions and 368 deletions

View File

@@ -25,22 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.2.3"
version = "0.2.4"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.2.3" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.3" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.3" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.3" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.3" }
server_fn = { path = "./server_fn", default-features = false, version = "0.2.3" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.3" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.3" }
leptos_router = { path = "./router", version = "0.2.3" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.3" }
leptos = { path = "./leptos", default-features = false, version = "0.2.4" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.4" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.4" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.4" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.4" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.4" }
server_fn = { path = "./server_fn", default-features = false, version = "0.2.4" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.4" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.4" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.4" }
leptos_router = { path = "./router", version = "0.2.4" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.4" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.4" }
[profile.release]
codegen-units = 1

View File

@@ -52,6 +52,7 @@ dependencies = [
{ name = "check", path = "examples/ssr_modes" },
{ name = "check", path = "examples/ssr_modes_axum" },
{ name = "check", path = "examples/tailwind" },
{ name = "check", path = "examples/tailwind_csr_trunk" },
{ name = "check", path = "examples/todo_app_sqlite" },
{ name = "check", path = "examples/todo_app_sqlite_axum" },
{ name = "check", path = "examples/todo_app_sqlite_viz" },

View File

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

View File

@@ -61,3 +61,19 @@ view! {
<input prop:value=a on:input=on_input />
}
```
## Build configuration
### Cargo feature resolution in workspaces
A new [version](https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions) of Cargo's feature resolver was introduced for the 2021 edition of Rust.
For single crate projects it will select a resolver version based on the Rust edition in `Cargo.toml`. As there is no Rust edition present for `Cargo.toml` in a workspace, Cargo will default to the pre 2021 edition resolver.
This can cause issues resulting in non WASM compatible code being built for a WASM target. Seeing `mio` failing to build is often a sign that none WASM compatible code is being included in the build.
The resolver version can be set in the workspace `Cargo.toml` to remedy this issue.
```toml
[workspace]
members = ["member1", "member2"]
resolver = "2"
```

View File

@@ -60,7 +60,7 @@ Your directory structure should now look something like this
leptos_tutorial
├── src
│ └── main.rs
├── Cargo.html
├── Cargo.toml
├── index.html
```

View File

@@ -84,7 +84,7 @@ fn FancyMath(cx: Scope) -> impl IntoView {
This kind of “provide a signal in a parent, consume it in a child” should be familiar
from the chapter on [parent-child interactions](./view/08_parent_child.md). The same
pattern you use to communicate between parents and children works for grandparents and
grandchildren, or any ancestors and descendents: in other words, between “global” state
grandchildren, or any ancestors and descendants: in other words, between “global” state
in the root component of your app and any other components anywhere else in the app.
Because of the fine-grained nature of updates, this is usually all you need. However,

View File

@@ -21,10 +21,11 @@
- [Actions](./async/13_actions.md)
- [Responding to Changes with `create_effect`](./14_create_effect.md)
- [Global State Management](./15_global_state.md)
- [Router]()
- [Fundamentals]()
- [defining `<Routes/>`]()
- [`<A/>`]()
- [Router](./router/README.md)
- [Defining `<Routes/>`](./router/16_routes.md)
- [Nested Routing](./router/17_nested_routing.md)
- [Params and Queries](./router/18_params_and_queries.md)
- [`<A/>`](./router/19_a.md)
- [`<Form/>`]()
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
- [Metadata]()
@@ -33,6 +34,9 @@
- [`cargo-leptos`]()
- [Hydration Footguns]()
- [Request/Response]()
- [Extractors]()
- [Axum]()
- [Actix]()
- [Headers]()
- [Cookies]()
- [Server Functions]()
@@ -41,3 +45,4 @@
- [`<ActionForm/>`s]()
- [Turning off WebAssembly]()
- [Advanced Reactivity]()
- [Appendix: Optimizing WASM Binary Size]()

View File

@@ -69,4 +69,4 @@ Every time one of the resources is reloading, the `"Loading..."` fallback will s
This inversion of the flow of control makes it easier to add or remove individual resources, as you dont need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which well talk about during a later chapter.
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>

View File

@@ -4,6 +4,6 @@ Youll notice in the `<Suspense/>` example that if you keep reloading the data
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data laods. This can be a much better user experience than constantly falling back to a loading message.
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message.
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>

View File

@@ -0,0 +1,101 @@
# Defining Routes
## Getting Started
Its easy to get started with the router.
First things first, make sure youve added the `leptos_router` package to your dependencies.
> Its important that the router is a separate package from `leptos` itself. This means that everything in the router can be defined in user-land code. If you want to create your own router, or use no router, youre completely free to do that!
And import the relevant types from the router, either with something like
```rust
use leptos_router::{Route, RouteProps, Router, RouterProps, Routes, RoutesProps};
```
or simply
```rust
use leptos_router::*;
```
## Providing the `<Router/>`
Routing behavior is provided by the [`<Router/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Router.html) component. This should usually be somewhere near the root of your application, the rest of the app.
> You shouldnt try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which ones decides what to do when the URL changes?
Lets start with a simple `<App/>` component using the router:
```rust
use leptos::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! {
<Router>
<nav>
/* ... */
</nav>
<main>
/* ... */
</main>
</Router>
}
}
```
## Defining `<Routes/>`
The [`<Routes/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Routes.html) component is where you define all the routes to which a user can navigate in your application. Each possible route is defined by a [`<Route/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Route.html) component.
You should place the `<Routes/>` component at the location within your app where you want routes to be rendered. Everything outside `<Routes/>` will be present on every page, so you can leave things like a navigation bar or menu outside the `<Routes/>`.
```rust
use leptos::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! {
<Router>
<nav>
/* ... */
</nav>
<main>
// all our routes will appear inside <main>
<Routes>
/* ... */
</Routes>
</main>
</Router>
}
}
```
Individual routes are defined by providing children to `<Routes/>` with the `<Route/>` component. `<Route/>` takes a `path` and a `view`. When the current location matches `path`, the `view` will be created and displayed.
The `path` can include
- a static path (`/users`),
- dynamic, named parameters beginning with a colon (`/:id`),
- and/or a wildcard beginning with an asterisk (`/user/*any`)
The `view` is a function that takes a `Scope` and returns a view.
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home/> }/>
<Route path="/users" view=|cx| view! { cx, <Users/> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile/> }/>
<Route path="/*any" view=|cx| view! { cx, <NotFound/> }/>
</Routes>
```
> The router scores each route to see how good a match it is, so you can define your routes in any order.
Now if you navigate to `/` or to `/users` youll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` youll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined.
Simple enough?

View File

@@ -0,0 +1,170 @@
# Nested Routing
We just defined the following set of routes:
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home /> }/>
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
</Routes>
```
Theres a certain amount of duplication here: `/users` and `/users/:id`. This is fine for a small app, but you can probably already tell it wont scale well. Wouldnt it be nice if we could nest these routes?
Well... you can!
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home /> }/>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
</Route>
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
</Routes>
```
But wait. Weve just subtly changed what our application does.
The next section is one of the most important in this entire routing section of the guide. Read it carefully, and feel free to ask questions if theres anything you dont understand.
# Nested Routes as Layout
Nested routes are a form of layout, not a method of route definition.
Let me put that another way: The goal of defining nested routes is not primarily to avoid repeating yourself when typing out the paths in your route definitions. It is actually to tell the router to display multiple `<Route/>`s on the page at the same time, side by side.
Lets look back at our practical example.
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
</Routes>
```
This means:
- If I go to `/users`, I get the `<Users/>` component.
- If I go to `/users/3`, I get the `<UserProfile/>` component (with the parameter `id` set to `3`; more on that later)
Lets say I use nested routes instead:
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
</Route>
</Routes>
```
This means:
- If I go to `/users/3`, the path matches two `<Route/>`s: `<Users/>` and `<UserProfile/>`.
- If I go to `/users`, the path is not matched.
I actually need to add a fallback route
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
<Route path="" view=|cx| view! { cx, <NoUser /> }/>
</Route>
</Routes>
```
Now:
- If I go to `/users/3`, the path matches `<Users/>` and `<UserProfile/>`.
- If I go to `/users`, the path matches `<Users/>` and `<NoUser/>`.
When I use nested routes, in other words, each **path** can match multiple **routes**: each URL can render the views provided by multiple `<Route/>` components, at the same time, on the same page.
This may be counter-intuitive, but its very powerful, for reasons youll hopefully see in a few minutes.
## Why Nested Routing?
Why bother with this?
Most web applications contain levels of navigation that correspond to different parts of the layout. For example, in an email app you might have a URL like `/contacts/greg`, which shows a list of contacts on the left of the screen, and contact details for Greg on the right of the screen. The contact list and the contact details should always appear on the screen at the same time. If theres no contact selected, maybe you want to show a little instructional text.
You can easily define this with nested routes
```rust
<Routes>
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }/>
<Route path="" view=|cx| view! { cx,
<p>"Select a contact to view more info."</p>
}/>
</Route>
</Routes>
```
You can go even deeper. Say you want to have tabs for each contacts address, email/phone, and your conversations with them. You can add _another_ set of nested routes inside `:id`:
```rust
<Routes>
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }>
<Route path="" view=|cx| view! { cx, <EmailAndPhone/> }/>
<Route path="address" view=|cx| view! { cx, <Address/> }/>
<Route path="messages" view=|cx| view! { cx, <Messages/> }/>
</Route>
<Route path="" view=|cx| view! { cx,
<p>"Select a contact to view more info."</p>
}/>
</Route>
</Routes>
```
> The main page of the [Remix website](https://remix.run/), a React framework from the creators of React Router, has a great visual example if you scroll down, with three levels of nested routing: Sales > Invoices > an invoice.
## `<Outlet/>`
Parent routes do not automatically render their nested routes. After all, they are just components; they dont know exactly where they should render their children, and “just stick at at the end of the parent component” is not a great answer.
Instead, you tell a parent component where to render any nested components with an `<Outlet/>` component. The `<Outlet/>` simply renders one of two things:
- if there is no nested route that has been matched, it shows nothing
- if there is a nested route that has been matched, it shows its `view`
Thats all! But its important to know and to remember, because its a common source of “Why isnt this working?” frustration. If you dont provide an `<Outlet/>`, the nested route wont be displayed.
```rust
#[component]
pub fn ContactList(cx: Scope) -> impl IntoView {
let contacts = todo!();
view! { cx,
<div style="display: flex">
// the contact list
<For each=contacts
key=|contact| contact.id
view=|cx, contact| todo!()
>
// the nested child, if any
// dont forget this!
<Outlet/>
</div>
}
}
```
## Nested Routing and Performance
All of this is nice, conceptually, but again—whats the big deal?
Performance.
In a fine-grained reactive library like Leptos, its always important to do the least amount of rendering work you can. Because were working with real DOM nodes and not diffing a virtual DOM, we want to “rerender” components as infrequently as possible. Nested routing makes this extremely easy.
Imagine my contact list example. If I navigate from Greg to Alice to Bob and back to Greg, the contact information needs to change on each navigation. But the `<ContactList/>` should never be rerendered. Not only does this save on rendering performance, it also maintains state in the UI. For example, if I have a search bar at the top of `<ContactList/>`, navigating from Greg to Alice to Bob wont clear the search.
In fact, in this case, we dont even need to rerender the `<Contact/>` component when moving between contacts. The router will just reactively update the `:id` parameter as we navigate, allowing us to make fine-grained updates. As we navigate between contacts, well update single text nodes to change the contacts name, address, and so on, without doing _any_ additional rerendering.
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple well cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so dont be surprised if theres anything you dont understand.
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>

View File

@@ -0,0 +1,77 @@
# Params and Queries
Static paths are useful for distinguishing between different pages, but almost every application wants to pass data through the URL at some point.
There are two ways you can do this:
1. named route **params** like `id` in `/users/:id`
2. named route **queries** like `q` in `/search?q=Foo`
Because of the way URLs are built, you can access the query from _any_ `<Route/>` view. You can access route params from the `<Route/>` that defines them or any of its nested children.
Accessing params and queries is pretty simple with a couple of hooks:
- [`use_query`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query.html) or [`use_query_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
- [`use_params`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_params.html) or [`use_params_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
Each of these comes with a typed option (`use_query` and `use_params`) and an untyped option (`use_query_map` and `use_params_map`).
The untyped versions hold a simple key-value map. To use the typed versions, derive the [`Params`](https://docs.rs/leptos_router/0.2.3/leptos_router/trait.Params.html) trait on a struct.
> `Params` is a very lightweight trait to convert a flat key-value map of strings into a struct by applying `FromStr` to each field. Because of the flat structure of route params and URL queries, its significantly less flexible than something like `serde`; it also adds much less weight to your binary.
```rust
use leptos::*;
use leptos_router::*;
#[derive(Params)]
struct ContactParams {
id: usize
}
#[derive(Params)]
struct ContactSearch {
q: String
}
```
> Note: The `Params` derive macro is located at `leptos::Params`, and the `Params` trait is at `leptos_router::Params`. If you avoid using glob imports like `use leptos::*;`, make sure youre importing the right one for the derive macro.
Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`.
The typed versions return `Memo<Result<T>, _>`. Its a Memo so it reacts to changes in the URL. Its a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
```rust
let params = use_params::<ContactParams>(cx);
let query = use_query::<ContactSearch>(cx);
// id: || -> usize
let id = move || {
params.with(|params| {
params
.map(|params| params.id)
.unwrap_or_default()
})
};
```
The untyped versions return `Memo<ParamsMap>`. Again, its memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
```rust
let params = use_params_map(cx);
let query = use_query_map(cx);
// id: || -> Option<String>
let id = move || {
params.with(|params| params.get("id").cloned())
};
```
This can get a little messy: deriving a signal that wraps an `Option<_>` or `Result<_>` can involve a couple steps. But its worth doing this for two reasons:
1. Its correct, i.e., it forces you to consider the cases, “What if the user doesnt pass a value for this query field? What if they pass an invalid value?”
2. Its performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replacing or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we havent explain them all yet.
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>

View File

@@ -0,0 +1,21 @@
# The `<A/>` Component
Client-side navigation works perfectly fine with ordinary HTML `<a>` elements. The router adds a listener that handles every click on a `<a>` element and tries to handle it on the client side, i.e., without doing another round trip to the server to request HTML. This is what enables the snappy “single-page app” navigations youre probably familiar with from most modern web apps.
The router will bail out of handling an `<a>` click under a number of situations
- the click event has had `prevent_default()` called on it
- the <kbd>Meta</kbd>, <kbd>Alt</kbd>, <kbd>Ctrl</kbd>, or <kbd>Shift</kbd> keys were held during click
- the `<a>` has a `target` or `download` attribute, or `rel="external"`
- the link has a different origin from the current location
In other words, the router will only try to do a client-side navigation when its pretty sure it can handle it, and it will upgrade every `<a>` element to get this special behavior.
The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_router/fn.A.html) component, which does two additional things:
1. Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky. For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative route, but `<a href="1">` likely will not (depending on where it appears in your view.) `<A/>` resolves routes relative to the path of the nested route within which it appears.
2. Sets the `aria-current` attribute to `page` if this link is the active link (i.e., its a link to the page youre on). This is helpful for accessibility and for styling. For example, if you want to set the link a different color if its a link to the page youre currently on, you can match this attribute with a CSS selector.
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>

View File

@@ -0,0 +1,23 @@
# Routing
## The Basics
Routing drives most websites. A router is the answer to the question, “Given this URL, what should appear on the page?”
A URL consists of many parts. For example, the URL `https://leptos.dev/blog/search?q=Search#results` consists of
- a _scheme_: `https`
- a _domain_: `leptos.dev`
- a **path**: `/blog/search`
- a **query** (or **search**): `?q=Search`
- a _hash_: `#results`
The Leptos Router works with the path and query (`/blog/search?q=Search`). Given this piece of the URL, what should the app render on the page?
## The Philosophy
In most cases, the path should drive what is displayed on the page. From the users perspective, for most applications, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.
The router handles most of this work for you by mapping the current location to particular components.

View File

@@ -30,7 +30,7 @@ Now lets say Id like to update the list of CSS classes on this element dyn
For example, lets say I want to add the class `red` when the count is odd. I can
do this using the `class:` syntax.
```rust
class:red=move || count() & 1 == 1
class:red=move || count() % 2 == 1
```
`class:` attributes take
1. the class name, following the colon (`red`)

View File

@@ -238,7 +238,7 @@ which allows you to easily pass props with different values.
In this case, its helpful to know about the
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
is a enumerated type that represents any kind of readable reactive signal. It can
is an enumerated type that represents any kind of readable reactive signal. It can
be useful when defining APIs for components youll want to reuse while passing
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
reactive value.

View File

@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
broadcaster = "1"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"

View File

@@ -198,13 +198,13 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
let s = create_signal_from_stream(
cx,
source.subscribe("message").unwrap().map(|value| {
value
.expect("no message event")
.1
.data()
.as_string()
.expect("expected string value")
}),
match value {
Ok(value) => {
value.1.data().as_string().expect("expected string value")
},
Err(_) => "0".to_string(),
}
})
);
on_cleanup(cx, move || source.close());

View File

@@ -10,7 +10,6 @@ cfg_if! {
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["stable"] }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -1,48 +1,44 @@
use leptos::{ev, html::*, *};
pub struct Props {
/// The starting value for the counter
pub initial_value: i32,
/// The change that should be applied each time the button is clicked.
pub step: i32,
}
/// A simple counter view.
pub fn view(cx: Scope, props: Props) -> impl IntoView {
let Props {
initial_value,
step,
} = props;
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);
// elements are created by calling a function with a Scope argument
// the function name is the same as the HTML tag name
div(cx)
.child((
cx,
// children can be added with .child()
// this takes any type that implements IntoView as its argument
// for example, a string or an HtmlElement<_>
.child(
button(cx)
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_value.update(|value| *value = 0))
.child((cx, "Clear")),
))
.child((
cx,
.child("Clear"),
)
.child(
button(cx)
.on(ev::click, move |_| {
set_value.update(|value| *value -= step)
})
.child((cx, "-1")),
))
.child((
cx,
.child("-1"),
)
.child(
span(cx)
.child((cx, "Value: "))
.child("Value: ")
// reactive values are passed to .child() as a tuple
// (Scope, [child function]) so an effect can be created
.child((cx, move || value.get()))
.child((cx, "!")),
))
.child((
cx,
.child("!"),
)
.child(
button(cx)
.on(ev::click, move |_| {
set_value.update(|value| *value += step)
})
.child((cx, "+1")),
))
.child("+1"),
)
}

View File

@@ -1,16 +1,8 @@
use counter_without_macros as counter;
use counter_without_macros::counter;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
counter::view(
cx,
counter::Props {
initial_value: 0,
step: 1,
},
)
})
mount_to_body(|cx| counter(cx, 0, 1))
}

View File

@@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
log = "0.4"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]

View File

@@ -2,7 +2,6 @@ use counters::{Counters, CountersProps};
use leptos::*;
fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <Counters/> })

View File

@@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["stable"] }
log = "0.4"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]

View File

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

View File

@@ -5,6 +5,6 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [

View File

@@ -70,8 +70,8 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
</p>
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
<div>
// note that the error boundries could be placed above in the Router or lower down
// in a particular route. The generated errors on the entire page contribue to the
// note that the error boundaries could be placed above in the Router or lower down
// in a particular route. The generated errors on the entire page contribute to the
// final status code sent by the server when producing ssr pages.
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<ReturnsError/>

View File

@@ -13,7 +13,6 @@ cfg_if! {
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -9,7 +9,7 @@ leptos = { path = "../../leptos" }
reqwasm = "0.5.0"
serde = { version = "1", features = ["derive"] }
log = "0.4"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]

View File

@@ -2,7 +2,6 @@ use fetch::fetch_example;
use leptos::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(fetch_example)

View File

@@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { path = "../../leptos", default-features = false, features = [

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [

View File

@@ -46,7 +46,6 @@ if #[cfg(feature = "ssr")] {
use hackernews_axum::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {

View File

@@ -12,7 +12,7 @@ leptos_router = { version = "0.2.0-alpha2", features = ["stable", "csr"] }
log = "0.4"
console_error_panic_hook = "0.1"
console_log = "0.2"
console_log = "1"
gloo-net = "0.2"
gloo-storage = "0.2"
serde = "1.0"

View File

@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
web-sys = "0.3"

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
console_log = "0.2"
console_log = "1"
log = "0.4"
leptos = { path = "../../leptos" }
leptos_router = { path = "../../router", features = ["csr"] }

View File

@@ -2,7 +2,6 @@ use leptos::*;
use router::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <RouterExample/> })

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "0.2.0"
console_log = "1.0.0"
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
console_error_panic_hook = "0.1.7"
futures = "0.3.25"

View File

@@ -15,7 +15,6 @@ cfg_if! {
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
console_log = "0.2"
console_log = "1"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", default-features = false, features = [

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
console_error_panic_hook = "0.1"
console_log = "0.2"
console_log = "1"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", default-features = false, features = [

View File

@@ -22,7 +22,7 @@ cfg-if = "1.0"
# dependecies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2", optional = true }
console_log = { version = "0.2", optional = true }
console_log = { version = "1", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
# dependecies for server (enable when ssr set)

View File

@@ -9,14 +9,14 @@ cfg_if! {
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log!("hydrate mode - hydrating");
log!("hydrate mode - hydrating");
leptos::mount_to_body(|cx| {
view! { cx, <App/> }
});
leptos::mount_to_body(|cx| {
view! { cx, <App/> }
});
}
}
else if #[cfg(feature = "csr")] {
@@ -35,5 +35,5 @@ cfg_if! {
view! { cx, <App /> }
});
}
}
}
}

10
examples/tailwind_csr_trunk/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

View File

@@ -0,0 +1,21 @@
[package]
name = "tailwind-csr-trunk"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { version = "0.2", features = [
"serde",
"csr",
] }
leptos_meta = { version = "0.2", features = ["csr"] }
leptos_router = { version = "0.2", features = ["csr"] }
log = "0.4"
gloo-net = { version = "0.2", features = ["http"] }
# dependecies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2" }
console_log = { version = "1"}
console_error_panic_hook = { version = "0.1"}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 henrik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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,70 @@
# Leptos Starter Template
This is a template demonstrating how to integrate [TailwindCSS](https://tailwindcss.com/) with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [trunk](https://github.com/thedodd/trunk) tool.
Install Tailwind and build the CSS:
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
Install trunk to client side render this bundle.
`cargo install trunk`
Then the site can be served with `trunk serve --open`
The browser will automatically open [http://127.0.0.1:8080//](http://127.0.0.1:8080//)
You can begin editing your app at `src/app.rs`.
## Installing Tailwind
You can install Tailwind using `npm`:
```bash
npm install -D tailwindcss
```
If you'd rather not use `npm`, you can install the Tailwind binary [here](https://github.com/tailwindlabs/tailwindcss/releases).
## Setting up with VS Code and Additional Tools
If you're using VS Code, add the following to your `settings.json`
```json
"emmet.includeLanguages": {
"rust": "html",
"*.rs": "html"
},
"tailwindCSS.includeLanguages": {
"rust": "html",
"*.rs": "html"
},
"files.associations": {
"*.rs": "rust"
},
"editor.quickSuggestions": {
"other": "on",
"comments": "on",
"strings": true
},
"css.validate": false,
```
Install [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss).
Install "VS Browser" extension, a browser at the right window.
Allow vscode Ports forward: 3000, 3001.
## Notes about Tooling
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
2. `rustup default nightly` - setup nightly as default, or you can use rust-toolchain file later on
3. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
4. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
5. `npm install -g sass` - install `dart-sass` (should be optional in future
## Attribution
This is based on the original Tailwind example (../examples/tailwind)

View File

@@ -0,0 +1,74 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
},
"node_modules/@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.28.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"node_modules/playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true,
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
}
},
"dependencies": {
"@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"requires": {
"@types/node": "*",
"playwright-core": "1.28.0"
}
},
"@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true
}
}
}

View File

@@ -0,0 +1,13 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
}

View File

@@ -0,0 +1,107 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
export default config;

View File

@@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
test("homepage has title and links to intro page", async ({ page }) => {
await page.goto("http://localhost:8080/");
await expect(page).toHaveTitle("Leptos • Counter with Tailwind");
await expect(page.locator("h2")).toHaveText("Welcome to Leptos with Tailwind");
});

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link data-trunk rel="rust" data-wasm-opt="z" />
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico" />
<link data-trunk rel="css" href="/style/output.css" />
<title>Leptos • Counter with Tailwind</title>
</head>
<body></body>
</html>

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,43 @@
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
view! {
cx,
<Stylesheet id="leptos" href="/pkg/tailwind.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Router>
<Routes>
<Route path="" view= move |cx| view! { cx, <Home/> }/>
</Routes>
</Router>
}
}
#[component]
fn Home(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div class="my-0 mx-auto max-w-3xl text-center">
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
<button
class="bg-amber-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
on:click=move |_| set_count.update(|count| *count += 1)
>
"Something's here | "
{move || if count() == 0 {
"Click me!".to_string()
} else {
count().to_string()
}}
" | Some more text"
</button>
</div>
}
}

View File

@@ -0,0 +1,15 @@
mod app;
use app::*;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log!("csr mode - mounting to body");
mount_to_body(|cx| {
view! { cx, <App /> }
});
}

View File

@@ -0,0 +1,583 @@
/*
! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
*/
html {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.my-0 {
margin-top: 0px;
margin-bottom: 0px;
}
.max-w-3xl {
max-width: 48rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.bg-amber-600 {
--tw-bg-opacity: 1;
background-color: rgb(217 119 6 / var(--tw-bg-opacity));
}
.p-6 {
padding: 1.5rem;
}
.px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
.px-5 {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.pb-10 {
padding-bottom: 2.5rem;
}
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:bg-sky-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
}

View File

@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["*.html", "./src/**/*.rs"],
},
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -11,7 +11,7 @@ actix-files = { version = "0.6.2", optional = true }
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
anyhow = "1.0.68"
broadcaster = "1.0.0"
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
serde = { version = "1.0.152", features = ["derive"] }
futures = "0.3.25"

View File

@@ -4,6 +4,15 @@ args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
clear = true
dependencies = ["check-debug", "check-release"]
[tasks.check-debug]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-release]
command = "cargo"
args = ["+nightly", "check-all-features", "--release"]
install_crate = "cargo-all-features"

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"

View File

@@ -13,7 +13,6 @@ cfg_if! {
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"

View File

@@ -13,7 +13,6 @@ cfg_if! {
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", default-features = false }
log = "0.4"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1.7"
uuid = { version = "1", features = ["v4", "js", "serde"] }
serde = { version = "1", features = ["derive"] }

View File

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

View File

@@ -94,7 +94,7 @@ impl ResponseOptions {
}
}
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead.
pub fn redirect(cx: leptos::Scope, path: &str) {
@@ -340,7 +340,7 @@ where
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before

View File

@@ -95,7 +95,7 @@ impl ResponseOptions {
}
}
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
pub fn redirect(cx: leptos::Scope, path: &str) {
@@ -128,7 +128,7 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
/// Decomposes an HTTP request into its parts, allowing you to read its headers
/// and other data without consuming the body. Creates a new Request from the
/// original parts for further processsing
/// original parts for further processing
pub async fn generate_request_and_parts(
req: Request<Body>,
) -> (Request<Body>, RequestParts) {
@@ -148,7 +148,7 @@ pub async fn generate_request_and_parts(
}
/// A struct to hold the http::request::Request and allow users to take ownership of it
/// Requred by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
/// Required by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
#[derive(Debug, Default)]
pub struct LeptosRequest<B>(Arc<RwLock<Option<Request<B>>>>);
@@ -198,8 +198,8 @@ impl<B> LeptosRequest<B> {
}
}
/// Generate a wrapper for the http::Request::Request type that allows one to
/// processs it, access the body, and use axum Extractors on it.
/// Requred by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
/// process it, access the body, and use axum Extractors on it.
/// Required by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
pub async fn generate_leptos_request<B>(req: Request<B>) -> LeptosRequest<B>
where
B: Default + std::fmt::Debug,
@@ -495,7 +495,7 @@ where
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
@@ -735,7 +735,7 @@ async fn forward_stream(
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network

View File

@@ -10,7 +10,7 @@ pub fn html_parts(
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to maintain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();

View File

@@ -90,7 +90,7 @@ impl ResponseOptions {
}
}
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
pub fn redirect(cx: leptos::Scope, path: &str) {
@@ -385,7 +385,7 @@ where
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
@@ -617,7 +617,7 @@ async fn forward_stream(
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This version allows us to pass Viz State/Extractor or other infro from Viz or network

View File

@@ -56,7 +56,7 @@
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional that works as intended even before WASM has loaded and begun to run.
//! a fully-functional application that works as intended even before WASM has loaded and begun to run.
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite),
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum), and
//! [`todo_app_sqlite_viz`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_viz)
@@ -150,12 +150,15 @@ pub use leptos_config::{self, get_configuration, LeptosOptions};
pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
#[allow(deprecated)]
pub use leptos_dom::{
self, create_node_ref, debug_warn, document, error, ev,
helpers::{
event_target, event_target_checked, event_target_value,
request_animation_frame, request_idle_callback, set_interval,
set_timeout, window_event_listener,
request_animation_frame, request_animation_frame_with_handle,
request_idle_callback, request_idle_callback_with_handle, set_interval,
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener,
},
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,

View File

@@ -126,7 +126,7 @@ where
}
);
// return the fallback for now, wrapped in fragment identifer
// return the fallback for now, wrapped in fragment identifier
fallback().into_view(cx)
}
};

View File

@@ -116,9 +116,12 @@ where
let suspense_context = use_context::<SuspenseContext>(cx)
.expect("there to be a SuspenseContext");
if is_first_run(&first_run, &suspense_context) {
let has_local_only = suspense_context.has_local_only();
if cfg!(feature = "hydrate") || !first_run.get() {
*prev_children.borrow_mut() = Some(frag.nodes.clone());
}
if is_first_run(&first_run, &suspense_context) {
let has_local_only = suspense_context.has_local_only()
|| cfg!(feature = "csr");
if !has_local_only || child_runs.get() > 0 {
first_run.set(false);
}
@@ -138,17 +141,21 @@ fn is_first_run(
first_run: &Rc<Cell<bool>>,
suspense_context: &SuspenseContext,
) -> bool {
match (
first_run.get(),
cfg!(feature = "hydrate"),
suspense_context.has_local_only(),
) {
(false, _, _) => false,
// is in hydrate mode, and has non-local resources (so, has streamed)
(_, false, false) => false,
// is in hydrate mode, but with only local resources (so, has not streamed)
(_, false, true) => true,
// either SSR or client mode: it's the first run
(_, true, _) => true,
if cfg!(feature = "csr") {
false
} else {
match (
first_run.get(),
cfg!(feature = "hydrate"),
suspense_context.has_local_only(),
) {
(false, _, _) => false,
// SSR and has non-local resources (so, has streamed)
(_, false, false) => false,
// SSR but with only local resources (so, has not streamed)
(_, false, true) => true,
// hydrate: it's the first run
(_, true, _) => true,
}
}
}

View File

@@ -34,6 +34,11 @@ leptos = { path = "../leptos" }
[dependencies.web-sys]
version = "0.3"
features = [
"DocumentFragment",
"Element",
"HtmlTemplateElement",
"NodeList",
"Window",
"console",
"Comment",
"Document",

View File

@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -207,7 +207,10 @@ where
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none()
&& child.get_closing_node().next_sibling().as_ref()
&& child
.get_closing_node()
.next_non_view_marker_sibling()
.as_ref()
!= Some(&closing);
// If the previous child was a text node, we would like to
@@ -241,7 +244,7 @@ where
if !was_child_moved && child != new_child {
// Remove the text
closing
.previous_sibling()
.previous_non_view_marker_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
@@ -300,7 +303,7 @@ where
&& new_child.get_text().is_some()
{
let t = closing
.previous_sibling()
.previous_non_view_marker_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>();
@@ -364,3 +367,49 @@ where
View::CoreComponent(crate::CoreComponent::DynChild(component))
}
}
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use web_sys::Node;
trait NonViewMarkerSibling {
fn next_non_view_marker_sibling(&self) -> Option<Node>;
fn previous_non_view_marker_sibling(&self) -> Option<Node>;
}
impl NonViewMarkerSibling for web_sys::Node {
fn next_non_view_marker_sibling(&self) -> Option<Node> {
cfg_if! {
if #[cfg(debug_assertions)] {
self.next_sibling().and_then(|node| {
if node.text_content().unwrap_or_default().trim().starts_with("leptos-view") {
node.next_sibling()
} else {
Some(node)
}
})
} else {
self.next_sibling()
}
}
}
fn previous_non_view_marker_sibling(&self) -> Option<Node> {
cfg_if! {
if #[cfg(debug_assertions)] {
self.previous_sibling().and_then(|node| {
if node.text_content().unwrap_or_default().trim().starts_with("leptos-view") {
node.previous_sibling()
} else {
Some(node)
}
})
} else {
self.previous_sibling()
}
}
}
}
}
}

View File

@@ -1,6 +1,7 @@
//! A variety of DOM utility functions.
use crate::{is_server, window};
use leptos_reactive::{on_cleanup, Scope};
use std::time::Duration;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
@@ -80,10 +81,33 @@ pub fn event_target_checked(ev: &web_sys::Event) -> bool {
.checked()
}
/// Runs the given function between the next repaint
/// using [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
/// Handle that is generated by [request_animation_frame_with_handle] and can
/// be used to cancel the animation frame request.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct AnimationFrameRequestHandle(i32);
impl AnimationFrameRequestHandle {
/// Cancels the animation frame request to which this refers.
/// See [`cancelAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame)
pub fn cancel(&self) {
_ = window().cancel_animation_frame(self.0);
}
}
/// 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) {
_ = request_animation_frame_with_handle(cb);
}
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame),
/// returning a cancelable handle.
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_animation_frame_with_handle(
cb: impl FnOnce() + 'static,
) -> Result<AnimationFrameRequestHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
@@ -95,13 +119,38 @@ pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
}
let cb = Closure::once_into_js(cb);
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
window()
.request_animation_frame(cb.as_ref().unchecked_ref())
.map(AnimationFrameRequestHandle)
}
/// Queues the given function during an idle period
/// using [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
/// Handle that is generated by [request_idle_callback_with_handle] and can be
/// used to cancel the idle callback.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IdleCallbackHandle(u32);
impl IdleCallbackHandle {
/// Cancels the idle callback to which this refers.
/// See [`cancelAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelIdleCallback)
pub fn cancel(&self) {
window().cancel_idle_callback(self.0);
}
}
/// 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) {
_ = request_idle_callback_with_handle(cb);
}
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback),
/// returning a cancelable handle.
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_idle_callback_with_handle(
cb: impl Fn() + 'static,
) -> Result<IdleCallbackHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
@@ -113,7 +162,21 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
window()
.request_idle_callback(cb.as_ref().unchecked_ref())
.map(IdleCallbackHandle)
}
/// Handle that is generated by [set_timeout_with_handle] and can be used to clear the timeout.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TimeoutHandle(i32);
impl TimeoutHandle {
/// Cancels the timeout to which this refers.
/// See [`clearTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout)
pub fn clear(&self) {
window().clear_timeout_with_handle(self.0);
}
}
/// Executes the given function after the given duration of time has passed.
@@ -123,6 +186,19 @@ 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) {
_ = set_timeout_with_handle(cb, duration);
}
/// Executes the given function after the given duration of time has passed, returning a cancelable handle.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_timeout_with_handle(
cb: impl FnOnce() + 'static,
duration: Duration,
) -> Result<TimeoutHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
@@ -134,10 +210,83 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
}
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(),
);
window()
.set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)
.map(TimeoutHandle)
}
/// "Debounce" a callback function. This will cause it to wait for a period of `delay`
/// after it is called. If it is called again during that period, it will wait
/// `delay` before running, and so on. This can be used, for example, to wrap event
/// listeners to prevent them from firing constantly as you type.
///
/// ```
/// use leptos::{leptos_dom::helpers::debounce, *};
///
/// #[component]
/// fn DebouncedButton(cx: Scope) -> impl IntoView {
/// let delay = std::time::Duration::from_millis(250);
/// let on_click = debounce(cx, delay, move |_| {
/// log!("...so many clicks!");
/// });
///
/// view! { cx,
/// <button on:click=on_click>"Click me"</button>
/// }
/// }
/// ```
pub fn debounce<T: 'static>(
cx: Scope,
delay: Duration,
mut cb: impl FnMut(T) + 'static,
) -> impl FnMut(T) {
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |value| {
let _guard = span.enter();
cb(value);
};
}
}
let cb = Rc::new(RefCell::new(cb));
let timer = Rc::new(Cell::new(None::<TimeoutHandle>));
on_cleanup(cx, {
let timer = Rc::clone(&timer);
move || {
if let Some(timer) = timer.take() {
timer.clear();
}
}
});
move |arg| {
if let Some(timer) = timer.take() {
timer.clear();
}
let handle = set_timeout_with_handle(
{
let cb = Rc::clone(&cb);
move || {
cb.borrow_mut()(arg);
}
},
delay,
);
if let Ok(handle) = handle {
timer.set(Some(handle));
}
}
}
/// Handle that is generated by [set_interval] and can be used to clear the interval.
@@ -152,12 +301,16 @@ impl IntervalHandle {
}
}
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[deprecated = "use set_interval_with_handle() instead. In the future, \
set_interval() will no longer return a handle, for consistency \
with other timer helper functions."]
pub fn set_interval(
cb: impl Fn() + 'static,
duration: Duration,
@@ -181,6 +334,36 @@ pub fn set_interval(
Ok(IntervalHandle(handle))
}
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_interval_with_handle(
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();
};
}
}
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`.
#[cfg_attr(
debug_assertions,

View File

@@ -74,13 +74,13 @@ pub trait ElementDescriptor: ElementDescriptorBounds {
/// The name of the element, i.e., `div`, `p`, `custom-element`.
fn name(&self) -> Cow<'static, str>;
/// Determains if the tag is void, i.e., `<input>` and `<br>`.
/// Determines if the tag is void, i.e., `<input>` and `<br>`.
fn is_void(&self) -> bool {
false
}
/// A unique `id` that should be generated for each new instance of
/// this element, and be consistant for both SSR and CSR.
/// this element, and be consistent for both SSR and CSR.
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
fn hydration_id(&self) -> &HydrationKey;
}
@@ -297,16 +297,40 @@ cfg_if! {
pub struct HtmlElement<El: ElementDescriptor> {
pub(crate) cx: Scope,
pub(crate) element: El,
#[educe(Debug(ignore))]
pub(crate) attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
#[educe(Debug(ignore))]
#[allow(clippy::type_complexity)]
pub(crate) children: SmallVec<[View; 4]>,
#[educe(Debug(ignore))]
pub(crate) prerendered: Option<Cow<'static, str>>,
pub(crate) children: ElementChildren,
#[cfg(debug_assertions)]
pub(crate) view_marker: Option<String>
}
#[derive(Clone, educe::Educe, PartialEq, Eq)]
#[educe(Default)]
pub(crate) enum ElementChildren {
#[educe(Default)]
Empty,
Children(Vec<View>),
InnerHtml(Cow<'static, str>),
Chunks(Vec<StringOrView>)
}
#[doc(hidden)]
#[derive(Clone)]
pub enum StringOrView {
String(Cow<'static, str>),
View(std::rc::Rc<dyn Fn() -> View>)
}
impl PartialEq for StringOrView {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(StringOrView::String(a), StringOrView::String(b)) => a == b,
_ => false
}
}
}
impl Eq for StringOrView {}
}
}
@@ -341,9 +365,8 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
Self {
cx,
attrs: smallvec![],
children: smallvec![],
children: Default::default(),
element,
prerendered: None,
#[cfg(debug_assertions)]
view_marker: None
}
@@ -353,6 +376,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
#[deprecated = "Use HtmlElement::from_chunks() instead."]
pub fn from_html(
cx: Scope,
element: El,
@@ -361,9 +385,27 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
Self {
cx,
attrs: smallvec![],
children: smallvec![],
children: ElementChildren::Chunks(vec![StringOrView::String(
html.into(),
)]),
element,
#[cfg(debug_assertions)]
view_marker: None,
}
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub fn from_chunks(
cx: Scope,
element: El,
chunks: impl IntoIterator<Item = StringOrView>,
) -> Self {
Self {
cx,
attrs: smallvec![],
children: ElementChildren::Chunks(chunks.into_iter().collect()),
element,
prerendered: Some(html.into()),
#[cfg(debug_assertions)]
view_marker: None,
}
@@ -407,7 +449,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
attrs,
children,
element,
prerendered,
#[cfg(debug_assertions)]
view_marker
} = self;
@@ -416,7 +457,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
cx,
attrs,
children,
prerendered,
element: AnyElement {
name: element.name(),
is_void: element.is_void(),
@@ -533,6 +573,23 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
self
}
/// Checks to see if this element is mounted to the DOM as a child
/// of `body`.
///
/// This method will always return [`None`] on non-wasm CSR targets.
pub fn is_mounted(&self) -> bool {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
crate::document()
.body()
.unwrap()
.contains(Some(self.element.as_ref()))
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
false
}
/// Adds an attribute to this element.
#[track_caller]
pub fn attr(
@@ -639,6 +696,108 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
this
}
/// Sets the class on the element as the class signal changes.
#[track_caller]
pub fn dyn_classes<I, C>(
self,
classes_signal: impl Fn() -> I + 'static,
) -> Self
where
I: IntoIterator<Item = C>,
C: Into<Cow<'static, str>>,
{
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
use smallvec::SmallVec;
let class_list = self.element.as_ref().class_list();
leptos_reactive::create_effect(
self.cx,
move |prev_classes: Option<
SmallVec<[Cow<'static, str>; 4]>,
>| {
let classes = classes_signal()
.into_iter()
.map(Into::into)
.collect::<SmallVec<[Cow<'static, str>; 4]>>(
);
let mut new_classes = classes
.iter()
.flat_map(|classes| classes.split_whitespace());
if let Some(prev_classes) = prev_classes {
let mut old_classes = prev_classes
.iter()
.flat_map(|classes| classes.split_whitespace());
// Remove old classes
for prev_class in old_classes.clone() {
if !new_classes.any(|c| c == prev_class) {
class_list.remove_1(prev_class).unwrap_or_else(
|err| {
panic!(
"failed to add class \
`{prev_class}`, error: {err:#?}"
)
},
);
}
}
// Add new classes
for class in new_classes {
if !old_classes.any(|c| c == class) {
class_list.add_1(class).unwrap_or_else(|err| {
panic!(
"failed to remove class `{class}`, \
error: {err:#?}"
)
});
}
}
} else {
let new_classes = new_classes
.map(ToOwned::to_owned)
.collect::<SmallVec<[_; 4]>>();
for class in &new_classes {
class_list.add_1(class).unwrap_or_else(|err| {
panic!(
"failed to add class `{class}`, error: \
{err:#?}"
)
});
}
}
classes
},
);
self
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let mut this = self;
let this = classes_signal()
.into_iter()
.map(Into::into)
.flat_map(|classes| {
classes
.split_whitespace()
.map(ToString::to_string)
.collect::<SmallVec<[_; 4]>>()
})
.fold(this, |this, class| this.class(class, true));
this
}
}
/// Sets a property on an element.
#[track_caller]
pub fn prop(
@@ -736,8 +895,23 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let mut this = self;
let children = &mut this.children;
this.children.push(child);
match children {
ElementChildren::Empty => {
*children = ElementChildren::Children(vec![child]);
}
ElementChildren::Children(ref mut children) => {
children.push(child);
}
_ => {
crate::debug_warn!(
"Dont call .child() on an HtmlElement if youve \
already called .inner_html() or \
HtmlElement::from_chunks()."
);
}
}
this
}
@@ -764,16 +938,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
{
let mut this = self;
let child = HtmlElement::from_html(
this.cx,
Custom {
name: "inner-html".into(),
id: Default::default(),
},
html,
);
this.children = smallvec![child.into_view(this.cx)];
this.children = ElementChildren::InnerHtml(html);
this
}
@@ -793,7 +958,6 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
element,
mut attrs,
children,
prerendered,
#[cfg(debug_assertions)]
view_marker,
..
@@ -811,8 +975,7 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
}
element.attrs = attrs;
element.children.extend(children);
element.prerendered = prerendered;
element.children = children;
#[cfg(debug_assertions)]
{

View File

@@ -33,6 +33,10 @@ pub use html::HtmlElement;
use html::{AnyElement, ElementDescriptor};
pub use hydration::{HydrationCtx, HydrationKey};
use leptos_reactive::Scope;
#[cfg(feature = "stable")]
use leptos_reactive::{
MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
pub use logging::*;
pub use macro_helpers::*;
pub use node_ref::*;
@@ -138,6 +142,72 @@ where
}
}
#[cfg(feature = "stable")]
impl<T> IntoView for ReadSignal<T>
where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "ReadSignal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
impl<T> IntoView for RwSignal<T>
where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "RwSignal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
impl<T> IntoView for Memo<T>
where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "Memo<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
impl<T> IntoView for Signal<T>
where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "Signal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
impl<T> IntoView for MaybeSignal<T>
where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "MaybeSignal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
DynChild::new(move || self.get()).into_view(cx)
}
}
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
/// HTML element.
@@ -161,14 +231,15 @@ cfg_if! {
}
}
} else {
use crate::html::ElementChildren;
/// HTML element.
#[derive(Clone, PartialEq, Eq)]
pub struct Element {
name: Cow<'static, str>,
is_void: bool,
attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
children: Vec<View>,
prerendered: Option<Cow<'static, str>>,
children: ElementChildren,
id: HydrationKey,
#[cfg(debug_assertions)]
/// Optional marker for the view macro source, in debug mode.
@@ -189,8 +260,10 @@ cfg_if! {
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
for child in &self.children {
writeln!(pad_adapter, "{child:#?}")?;
if let ElementChildren::Children(children) = &self.children {
for child in children {
writeln!(pad_adapter, "{child:#?}")?;
}
}
write!(f, "</{}>", self.name)
@@ -239,7 +312,6 @@ impl Element {
attrs,
children,
id,
prerendered,
#[cfg(debug_assertions)]
view_marker,
} = self;
@@ -250,8 +322,7 @@ impl Element {
cx,
element,
attrs,
children: children.into_iter().collect(),
prerendered,
children,
#[cfg(debug_assertions)]
view_marker,
}
@@ -286,7 +357,6 @@ impl Element {
attrs: Default::default(),
children: Default::default(),
id: el.hydration_id().clone(),
prerendered: None,
#[cfg(debug_assertions)]
view_marker: None
}

View File

@@ -2,7 +2,10 @@
//! Server-side HTML rendering utilities.
use crate::{CoreComponent, HydrationCtx, IntoView, View};
use crate::{
html::{ElementChildren, StringOrView},
CoreComponent, HydrationCtx, IntoView, View,
};
use cfg_if::cfg_if;
use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
use itertools::Itertools;
@@ -385,8 +388,19 @@ impl View {
}
}
View::Element(el) => {
let el_html = if let Some(prerendered) = el.prerendered {
prerendered
let el_html = if let ElementChildren::Chunks(chunks) =
el.children
{
chunks
.into_iter()
.map(|chunk| match chunk {
StringOrView::String(string) => string,
StringOrView::View(view) => {
view().render_to_string_helper()
}
})
.join("")
.into()
} else {
let tag_name = el.name;
@@ -421,11 +435,17 @@ impl View {
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>")
.into()
} else {
let children = el
.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("");
let children = match el.children {
ElementChildren::Empty => "".into(),
ElementChildren::Children(c) => c
.into_iter()
.map(View::render_to_string_helper)
.join("")
.into(),
ElementChildren::InnerHtml(h) => h,
// already handled this case above
ElementChildren::Chunks(_) => unreachable!(),
};
format!("<{tag_name}{attrs}>{children}</{tag_name}>")
.into()
@@ -489,12 +509,14 @@ pub(crate) fn render_serializers(
) -> impl Stream<Item = String> {
serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
let json = json.replace('<', "\\u003c");
format!(
r#"<script>
var val = {json:?};
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)
}} else {{
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
__LEPTOS_RESOLVED_RESOURCES.set({id}, val);
}}
</script>"#,
)

View File

@@ -2,10 +2,14 @@
//! Server-side HTML rendering utilities for in-order streaming and async rendering.
use crate::{ssr::render_serializers, CoreComponent, HydrationCtx, View};
use crate::{
html::{ElementChildren, StringOrView},
ssr::render_serializers,
CoreComponent, HydrationCtx, View,
};
use async_recursion::async_recursion;
use cfg_if::cfg_if;
use futures::{channel::mpsc::Sender, Stream, StreamExt};
use futures::{channel::mpsc::UnboundedSender, Stream, StreamExt};
use itertools::Itertools;
use leptos_reactive::{
create_runtime, run_scope_undisposed, suspense::StreamChunk, RuntimeId,
@@ -93,7 +97,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
)
});
let (tx, rx) = futures::channel::mpsc::channel(1);
let (tx, rx) = futures::channel::mpsc::unbounded();
leptos_reactive::spawn_local(async move {
handle_chunks(tx, chunks).await;
});
@@ -122,14 +126,15 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
}
#[async_recursion(?Send)]
async fn handle_chunks(mut tx: Sender<String>, chunks: Vec<StreamChunk>) {
async fn handle_chunks(tx: UnboundedSender<String>, chunks: Vec<StreamChunk>) {
let mut buffer = String::new();
for chunk in chunks {
match chunk {
StreamChunk::Sync(sync) => buffer.push_str(&sync),
StreamChunk::Async(suspended) => {
// add static HTML before the Suspense and stream it down
_ = tx.try_send(std::mem::take(&mut buffer));
tx.unbounded_send(std::mem::take(&mut buffer))
.expect("failed to send async HTML chunk");
// send the inner stream
let suspended = suspended.await;
@@ -138,7 +143,8 @@ async fn handle_chunks(mut tx: Sender<String>, chunks: Vec<StreamChunk>) {
}
}
// send final sync chunk
_ = tx.try_send(std::mem::take(&mut buffer));
tx.unbounded_send(std::mem::take(&mut buffer))
.expect("failed to send final HTML chunk");
}
impl View {
@@ -186,8 +192,17 @@ impl View {
format!("<!--leptos-view|{id}|open-->").into(),
));
}
if let Some(prerendered) = el.prerendered {
chunks.push(StreamChunk::Sync(prerendered))
if let ElementChildren::Chunks(el_chunks) = el.children {
for chunk in el_chunks {
match chunk {
StringOrView::String(string) => {
chunks.push(StreamChunk::Sync(string))
}
StringOrView::View(view) => {
view().into_stream_chunks_helper(cx, chunks);
}
}
}
} else {
let tag_name = el.name;
@@ -231,8 +246,19 @@ impl View {
chunks.push(StreamChunk::Sync(
format!("<{tag_name}{attrs}>").into(),
));
for child in el.children {
child.into_stream_chunks_helper(cx, chunks);
match el.children {
ElementChildren::Empty => {}
ElementChildren::Children(children) => {
for child in children {
child.into_stream_chunks_helper(cx, chunks);
}
}
ElementChildren::InnerHtml(inner_html) => {
chunks.push(StreamChunk::Sync(inner_html));
}
// handled above
ElementChildren::Chunks(_) => unreachable!(),
}
chunks.push(StreamChunk::Sync(

View File

@@ -2,10 +2,6 @@ use crate::node::{LAttributeValue, LNode};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
// TODO: insertion and removal code are still somewhat broken
// namely, it will tend to remove and move or mutate nodes,
// which causes a bit of a problem for DynChild etc.
#[derive(Debug, Default)]
struct OldChildren(IndexMap<LNode, Vec<usize>>);
@@ -58,7 +54,7 @@ impl LNode {
.collect(),
},
LNode::Text(_)
| LNode::Component(_, _)
| LNode::Component { .. }
| LNode::DynChild(_) => ReplacementNode::Html(self.to_html()),
},
}
@@ -80,10 +76,19 @@ impl LNode {
child.add_old_children(new_path, positions);
}
}
// only need to insert dynamic content, as these might change
LNode::Component(_, _) | LNode::DynChild(_) => {
// need to insert dynamic content and children, as these might change
LNode::DynChild(_) => {
positions.0.insert(self.clone(), path);
}
LNode::Component { children, .. } => {
positions.0.insert(self.clone(), path.clone());
for (idx, child) in children.iter().enumerate() {
let mut new_path = path.clone();
new_path.push(idx);
child.add_old_children(new_path, positions);
}
}
// can just create text nodes, whatever
LNode::Text(_) => {}
}
@@ -148,6 +153,28 @@ impl LNode {
.collect()
}
// components + dynamic context: no patches
(
LNode::Component {
name: old_name,
children: old_children,
..
},
LNode::Component {
name: new_name,
children: new_children,
..
},
) if old_name == new_name => {
let mut path = path.to_vec();
path.push(0);
path.push(0);
LNode::diff_children(
&path,
old_children,
new_children,
orig_children,
)
}
_ => vec![],
}
}

View File

@@ -20,7 +20,11 @@ pub enum LNode {
},
// don't need anything; skipped during patching because it should
// contain its own view macros
Component(String, Vec<(String, String)>),
Component {
name: String,
props: Vec<(String, String)>,
children: Vec<LNode>,
},
DynChild(String),
}
@@ -71,9 +75,14 @@ impl LNode {
}
Node::Element(el) => {
if is_component_node(&el) {
views.push(LNode::Component(
el.name.to_string(),
el.attributes
let mut children = Vec::new();
for child in el.children {
LNode::parse_node(child, &mut children)?;
}
views.push(LNode::Component {
name: el.name.to_string(),
props: el
.attributes
.into_iter()
.filter_map(|attr| match attr {
Node::Attribute(attr) => Some((
@@ -83,7 +92,8 @@ impl LNode {
_ => None,
})
.collect(),
));
children,
});
} else {
let name = el.name.to_string();
let mut attrs = Vec::new();
@@ -125,7 +135,7 @@ impl LNode {
match self {
LNode::Fragment(frag) => frag.iter().map(LNode::to_html).collect(),
LNode::Text(text) => text.to_owned(),
LNode::Component(name, _) => format!(
LNode::Component { name, .. } => format!(
"<!--<{name}>--><pre>&lt;{name}/&gt; will load once Rust code \
has been compiled.</pre><!--</{name}>-->"
),

View File

@@ -2,7 +2,8 @@ console.log("[HOT RELOADING] Connected to server.");
function patch(json) {
try {
const views = JSON.parse(json);
for (const [id, patches] of views) {
for (const [id, patches] of views) {
console.log("[HOT RELOAD]", patches);
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
open = `leptos-view|${id}|open`,
close = `leptos-view|${id}|close`;
@@ -250,7 +251,33 @@ function patch(json) {
node: walker.currentNode
});
} else if (walker.currentNode.nodeType == Node.COMMENT_NODE) {
if (walker.currentNode.textContent.trim().startsWith("leptos-view")) {
if (walker.currentNode.textContent.trim().startsWith("leptos-view")) {
if (walker.currentNode.textContent.trim().endsWith("-children|open")) {
const startingName = walker.currentNode.textContent.trim();
const componentName = startingName.replace("-children|open").replace("leptos-view|");
const endingName = `leptos-view|${componentName}-children|close`;
let start = walker.currentNode;
let depth = 1;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == endingName) {
depth--;
} else if (walker.currentNode.textContent.trim() == startingName) {
depth++;
}
if(depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "fragment",
start: start.nextSibling,
end: end.previousSibling,
children: childrenFromRange(start.parentElement, start.nextSibling, end.previousSibling)
});
}
} else if (walker.currentNode.textContent.trim() == "<() />") {
actualChildren.push({
type: "unit",
@@ -326,7 +353,7 @@ function patch(json) {
return actualChildren;
}
function childAtPath(element, path) {
function childAtPath(element, path) {
if (path.length == 0) {
return element;
} else if (element.children) {

View File

@@ -130,6 +130,7 @@ impl ToTokens for Model {
let mut body = body.to_owned();
body.sig.ident = format_ident!("__{}", body.sig.ident);
#[allow(clippy::redundant_clone)] // false positive
let body_name = body.sig.ident.clone();
let (_, generics, where_clause) = body.sig.generics.split_for_impl();
@@ -153,6 +154,7 @@ impl ToTokens for Model {
if cfg!(feature = "tracing") {
(
quote! {
#[allow(clippy::let_with_type_underscore)]
#[cfg_attr(
debug_assertions,
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)

View File

@@ -267,32 +267,55 @@ fn root_element_to_tokens_ssr(
if is_component_node(node) {
component_to_tokens(cx, node, global_class)
} else {
let mut template = String::new();
let mut holes = Vec::<TokenStream>::new();
let mut exprs_for_compiler = Vec::<TokenStream>::new();
let mut template = String::new();
let mut holes = Vec::new();
let mut chunks = Vec::new();
element_to_tokens_ssr(
cx,
node,
&mut template,
&mut holes,
&mut chunks,
&mut exprs_for_compiler,
true,
global_class,
);
let template = if holes.is_empty() {
quote! {
#template
// push final chunk
if !template.is_empty() {
chunks.push(SsrElementChunks::String { template, holes })
}
let chunks = chunks.into_iter().map(|chunk| match chunk {
SsrElementChunks::String { template, holes } => {
if holes.is_empty() {
quote! {
leptos::leptos_dom::html::StringOrView::String(#template.into())
}
} else {
quote! {
leptos::leptos_dom::html::StringOrView::String(
format!(
#template,
#(#holes),*
)
.into()
)
}
}
}
} else {
quote! {
format!(
#template,
#(#holes)*
)
}
};
SsrElementChunks::View(view) => {
quote! {
#[allow(unused_braces)]
{
let view = #view;
leptos::leptos_dom::html::StringOrView::View(std::rc::Rc::new(move || view.clone()))
}
}
},
});
let tag_name = node.name.to_string();
let is_custom_element = is_custom_element(&tag_name);
@@ -328,27 +351,42 @@ fn root_element_to_tokens_ssr(
quote! {
{
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_html(cx, #full_name, #template)#view_marker
::leptos::HtmlElement::from_chunks(#cx, #full_name, [#(#chunks),*])#view_marker
}
}
}
}
enum SsrElementChunks {
String {
template: String,
holes: Vec<TokenStream>,
},
View(TokenStream),
}
#[allow(clippy::too_many_arguments)]
fn element_to_tokens_ssr(
cx: &Ident,
node: &NodeElement,
template: &mut String,
holes: &mut Vec<TokenStream>,
chunks: &mut Vec<SsrElementChunks>,
exprs_for_compiler: &mut Vec<TokenStream>,
is_root: bool,
global_class: Option<&TokenTree>,
) {
if is_component_node(node) {
template.push_str("{}");
let component = component_to_tokens(cx, node, global_class);
holes.push(quote! {
{#component}.into_view(cx).render_to_string(cx),
})
if !template.is_empty() {
chunks.push(SsrElementChunks::String {
template: std::mem::take(template),
holes: std::mem::take(holes),
})
}
chunks.push(SsrElementChunks::View(quote! {
{#component}.into_view(#cx)
}));
} else {
let tag_name = node
.name
@@ -374,9 +412,9 @@ fn element_to_tokens_ssr(
// insert hydration ID
let hydration_id = if is_root {
quote! { leptos::leptos_dom::HydrationCtx::peek(), }
quote! { leptos::leptos_dom::HydrationCtx::peek() }
} else {
quote! { leptos::leptos_dom::HydrationCtx::id(), }
quote! { leptos::leptos_dom::HydrationCtx::id() }
};
match node
.attributes
@@ -404,20 +442,23 @@ fn element_to_tokens_ssr(
let value = inner_html.as_ref();
holes.push(quote! {
(#value).into_attribute(cx).as_nameless_value_string().unwrap_or_default(),
(#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::Element(child) => {
element_to_tokens_ssr(
cx,
child,
template,
holes,
chunks,
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(
@@ -428,7 +469,7 @@ fn element_to_tokens_ssr(
let value = text.value.as_ref();
holes.push(quote! {
#value.into_view(#cx).render_to_string(#cx),
#value.into_view(#cx).render_to_string(#cx)
})
}
}
@@ -436,14 +477,23 @@ fn element_to_tokens_ssr(
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),
})
if !template.is_empty() {
chunks.push(SsrElementChunks::String {
template: std::mem::take(template),
holes: std::mem::take(holes),
})
}
chunks.push(SsrElementChunks::View(quote! {
{#value}.into_view(#cx)
}));
}
}
Node::Fragment(_) => todo!(),
Node::Fragment(_) => abort!(
Span::call_site(),
"You can't nest a fragment inside an element."
),
_ => {}
}
}
@@ -498,7 +548,7 @@ fn attribute_to_tokens_ssr<'a>(
&{#value}.into_attribute(#cx)
.as_nameless_value_string()
.map(|a| format!("{}=\"{}\"", #name, leptos::leptos_dom::ssr::escape_attr(&a)))
.unwrap_or_default(),
.unwrap_or_default()
})
}
} else {
@@ -618,9 +668,9 @@ fn set_class_attribute_ssr(
template.push_str(" {}");
let value = value.as_ref();
holes.push(quote! {
&(cx, #value).into_attribute(#cx).as_nameless_value_string()
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
.unwrap_or_default(),
.unwrap_or_default()
});
}
}
@@ -628,13 +678,13 @@ fn set_class_attribute_ssr(
for (_span, name, value) in &class_attrs {
template.push_str(" {}");
holes.push(quote! {
(cx, #value).into_class(#cx).as_value_string(#name),
(#cx, #value).into_class(#cx).as_value_string(#name)
});
}
if let Some(dyn_global_class) = dyn_global_class {
template.push_str(" {}");
holes.push(quote! { #dyn_global_class, });
holes.push(quote! { #dyn_global_class });
}
template.push('"');
@@ -768,7 +818,22 @@ fn element_to_tokens(
};
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
Some(attribute_to_tokens(cx, node))
if node.key.to_string().trim().starts_with("class:") {
None
} else {
Some(attribute_to_tokens(cx, node))
}
} else {
None
}
});
let class_attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
if node.key.to_string().trim().starts_with("class:") {
Some(attribute_to_tokens(cx, node))
} else {
None
}
} else {
None
}
@@ -826,6 +891,7 @@ fn element_to_tokens(
quote! {
#name
#(#attrs)*
#(#class_attrs)*
#global_class_expr
#(#children)*
#view_marker
@@ -1053,6 +1119,15 @@ pub(crate) fn component_to_tokens(
let children = if node.children.is_empty() {
quote! {}
} else {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let marker = format!("<{component_name}/>-children");
let view_marker = quote! { .with_view_marker(#marker) };
} else {
let view_marker = quote! {};
}
}
let children = fragment_to_tokens(
cx,
span,
@@ -1071,7 +1146,7 @@ pub(crate) fn component_to_tokens(
.children({
#(#clonables)*
Box::new(move |#cx| #children)
Box::new(move |#cx| #children #view_marker)
})
}
};

View File

@@ -20,7 +20,7 @@ fn Component(
#[test]
fn component() {
let cp = ComponentProps::builder().into("").strip_option(9).build();
assert_eq!(cp.optional, false);
assert!(!cp.optional);
assert_eq!(cp.optional_no_strip, None);
assert_eq!(cp.strip_option, Some(9));
assert_eq!(cp.default, NonZeroUsize::new(10).unwrap());

View File

@@ -10,9 +10,9 @@ description = "Reactive system for the Leptos web framework."
[dependencies]
slotmap = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde-lite = { version = "0.3", optional = true }
serde-lite = { version = "0.4", optional = true }
futures = { version = "0.3" }
js-sys = "0.3"
js-sys = { version = "0.3", optional = true }
miniserde = { version = "0.1", optional = true }
rkyv = { version = "0.7.39", features = [
"validation",
@@ -31,17 +31,17 @@ base64 = "0.21"
thiserror = "1"
tokio = { version = "1", features = ["rt"], optional = true }
tracing = "0.1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = [
wasm-bindgen = { version = "0.2", optional = true }
wasm-bindgen-futures = { version = "0.4", optional = true }
web-sys = { version = "0.3", optional = true, features = [
"DocumentFragment",
"Element",
"HtmlTemplateElement",
"NodeList",
"Window",
] }
cfg-if = "1.0.0"
indexmap = "1.9.2"
cfg-if = "1"
indexmap = "1"
[dev-dependencies]
log = "0.4"
@@ -50,8 +50,18 @@ leptos = { path = "../leptos" }
[features]
default = []
csr = []
hydrate = []
csr = [
"dep:js-sys",
"dep:wasm-bindgen",
"dep:wasm-bindgen-futures",
"dep:web-sys",
]
hydrate = [
"dep:js-sys",
"dep:wasm-bindgen",
"dep:wasm-bindgen-futures",
"dep:web-sys",
]
ssr = ["dep:tokio"]
stable = []
serde = []

View File

@@ -1,8 +1,8 @@
#![forbid(unsafe_code)]
use crate::{
create_effect, node::NodeId, on_cleanup, with_runtime, AnyComputation,
RuntimeId, Scope, SignalGet, SignalGetUntracked, SignalStream, SignalWith,
SignalWithUntracked,
RuntimeId, Scope, SignalDispose, SignalGet, SignalGetUntracked,
SignalStream, SignalWith, SignalWithUntracked,
};
use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
@@ -191,7 +191,8 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
)]
fn get_untracked(&self) -> T {
with_runtime(self.runtime, move |runtime| {
match self.id.try_with_no_subscription(runtime, T::clone) {
let f = move |maybe_value: &Option<T>| maybe_value.clone().unwrap();
match self.id.try_with_no_subscription(runtime, f) {
Ok(t) => t,
Err(_) => panic_getting_dead_memo(
#[cfg(debug_assertions)]
@@ -412,6 +413,12 @@ impl<T: Clone> SignalStream<T> for Memo<T> {
}
}
impl<T> SignalDispose for Memo<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl_get_fn_traits![Memo];
pub(crate) struct MemoState<T, F>

View File

@@ -15,6 +15,7 @@ use std::{
fmt::Debug,
future::Future,
marker::PhantomData,
panic::Location,
pin::Pin,
rc::Rc,
};
@@ -377,13 +378,15 @@ where
///
/// If you want to get the value without cloning it, use [Resource::with].
/// (`value.read(cx)` is equivalent to `value.with(cx, T::clone)`.)
#[track_caller]
pub fn read(&self, cx: Scope) -> Option<T>
where
T: Clone,
{
let location = std::panic::Location::caller();
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.read(cx)
resource.read(cx, location)
})
})
.ok()
@@ -397,10 +400,12 @@ where
///
/// If you want to get the value by cloning it, you can use
/// [Resource::read].
#[track_caller]
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
let location = std::panic::Location::caller();
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.with(cx, f)
resource.with(cx, f, location)
})
})
.ok()
@@ -563,14 +568,25 @@ where
S: Clone + 'static,
T: 'static,
{
pub fn read(&self, cx: Scope) -> Option<T>
#[track_caller]
pub fn read(
&self,
cx: Scope,
location: &'static Location<'static>,
) -> Option<T>
where
T: Clone,
{
self.with(cx, T::clone)
self.with(cx, T::clone, location)
}
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
#[track_caller]
pub fn with<U>(
&self,
cx: Scope,
f: impl FnOnce(&T) -> U,
location: &'static Location<'static>,
) -> Option<U> {
let suspense_cx = use_context::<SuspenseContext>(cx);
let v = self
@@ -587,6 +603,19 @@ where
if serializable {
suspense_cx.has_local_only.set_value(false);
}
} else {
#[cfg(all(feature = "hydrate", debug_assertions))]
crate::macros::debug_warn!(
"At {location}, you are reading a resource in `hydrate` mode \
outside a <Suspense/> or <Transition/>. This can cause \
hydration mismatch errors and loses out on a significant \
performance optimization. To fix this issue, you can either: \
\n1. Wrap the place where you read the resource in a \
<Suspense/> or <Transition/> component, or \n2. Switch to \
using create_local_resource(), which will wait to load the \
resource until the app is hydrated on the client side. (This \
will have worse performance in most cases.)",
);
}
let increment = move |_: Option<()>| {

View File

@@ -62,6 +62,7 @@ pub(crate) struct Runtime {
RefCell<SecondaryMap<NodeId, RefCell<FxIndexSet<NodeId>>>>,
pub pending_effects: RefCell<Vec<NodeId>>,
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
pub batching: Cell<bool>,
}
// This core Runtime impl block handles all the work of marking and updating
@@ -186,12 +187,12 @@ impl Runtime {
let current_observer = self.observer.get();
// mark self dirty
if let Some(mut current_node) = nodes.get_mut(node) {
if let Some(current_node) = nodes.get_mut(node) {
Runtime::mark(
node,
&mut current_node,
current_node,
ReactiveNodeState::Dirty,
&mut *pending_effects,
&mut pending_effects,
current_observer,
);
@@ -200,10 +201,10 @@ impl Runtime {
let mut descendants = Default::default();
Runtime::gather_descendants(&subscribers, node, &mut descendants);
for descendant in descendants {
if let Some(mut node) = nodes.get_mut(descendant) {
if let Some(node) = nodes.get_mut(descendant) {
Runtime::mark(
descendant,
&mut node,
node,
ReactiveNodeState::Check,
&mut pending_effects,
current_observer,
@@ -248,12 +249,22 @@ impl Runtime {
pub(crate) fn run_effects(runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
let effects = runtime.pending_effects.take();
for effect_id in effects {
runtime.update_if_necessary(effect_id);
}
runtime.run_your_effects();
});
}
pub(crate) fn run_your_effects(&self) {
let effects = self.pending_effects.take();
for effect_id in effects {
self.update_if_necessary(effect_id);
}
}
pub(crate) fn dispose_node(&self, node: NodeId) {
self.node_sources.borrow_mut().remove(node);
self.node_subscribers.borrow_mut().remove(node);
self.nodes.borrow_mut().remove(node);
}
}
impl Debug for Runtime {

View File

@@ -414,7 +414,7 @@ impl Scope {
/// The set of all HTML fragments currently pending.
///
/// The keys are hydration IDs. Valeus are tuples of two pinned
/// The keys are hydration IDs. Values are tuples of two pinned
/// `Future`s that return content for out-of-order and in-order streaming, respectively.
pub fn pending_fragments(
&self,
@@ -442,6 +442,25 @@ impl Scope {
.ok()
.flatten()
}
/// Batches any reactive updates, preventing effects from running until the whole
/// function has run. This allows you to prevent rerunning effects if multiple
/// signal updates might cause the same effect to run.
///
/// # Panics
/// Panics if the runtime this scope belongs to has already been disposed.
pub fn batch<T>(&self, f: impl FnOnce() -> T) -> T {
with_runtime(self.runtime, move |runtime| {
runtime.batching.set(true);
let val = f();
runtime.batching.set(false);
runtime.run_your_effects();
val
})
.expect(
"tried to run a batched update in a runtime that has been disposed",
)
}
}
impl fmt::Debug for ScopeDisposer {

View File

@@ -274,6 +274,16 @@ pub trait SignalStream<T> {
fn to_stream(&self, cx: Scope) -> Pin<Box<dyn Stream<Item = T>>>;
}
/// This trait allows disposing a signal before its [Scope] has been disposed.
pub trait SignalDispose {
/// Disposes of the signal. This:
/// 1. Detaches the signal from the reactive graph, preventing it from triggering
/// further updates; and
/// 2. Drops the value contained in the signal.
#[track_caller]
fn dispose(self);
}
/// Creates a signal, the basic reactive primitive.
///
/// A signal is a piece of data that may change over time,
@@ -725,6 +735,12 @@ impl<T: Clone> SignalStream<T> for ReadSignal<T> {
}
}
impl<T> SignalDispose for ReadSignal<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl<T> ReadSignal<T>
where
T: 'static,
@@ -1025,6 +1041,12 @@ impl<T> SignalSet<T> for WriteSignal<T> {
}
}
impl<T> SignalDispose for WriteSignal<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl<T> Clone for WriteSignal<T> {
fn clone(&self) -> Self {
Self {
@@ -1598,6 +1620,12 @@ impl<T: Clone> SignalStream<T> for RwSignal<T> {
}
}
impl<T> SignalDispose for RwSignal<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl<T> RwSignal<T> {
/// Returns a read-only handle to the signal.
///
@@ -1884,7 +1912,7 @@ impl NodeId {
runtime.mark_dirty(*self);
// notify subscribers
if updated.is_some() {
if updated.is_some() && !runtime.batching.get() {
Runtime::run_effects(runtime_id);
};
updated

View File

@@ -1,7 +1,7 @@
#[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_rw_signal,
create_scope, create_signal, SignalSet,
};
#[cfg(not(feature = "stable"))]
@@ -91,3 +91,45 @@ fn untrack_mutes_effect() {
})
.dispose()
}
#[cfg(not(feature = "stable"))]
#[test]
fn batching_actually_batches() {
use std::{cell::Cell, rc::Rc};
create_scope(create_runtime(), |cx| {
let first_name = create_rw_signal(cx, "Greg".to_string());
let last_name = create_rw_signal(cx, "Johnston".to_string());
// simulate an arbitrary side effect
let count = Rc::new(Cell::new(0));
create_isomorphic_effect(cx, {
let count = count.clone();
move |_| {
_ = first_name();
_ = last_name();
count.set(count.get() + 1);
}
});
// runs once initially
assert_eq!(count.get(), 1);
// individual updates run effect once each
first_name.set("Alice".to_string());
assert_eq!(count.get(), 2);
last_name.set("Smith".to_string());
assert_eq!(count.get(), 3);
// batched effect only runs twice
cx.batch(move || {
first_name.set("Bob".to_string());
last_name.set("Williams".to_string());
});
assert_eq!(count.get(), 4);
})
.dispose()
}

View File

@@ -286,7 +286,7 @@ where
let pending = create_rw_signal(cx, false);
let action_fn = Rc::new(move |input: &I| {
let fut = action_fn(input);
Box::pin(async move { fut.await }) as Pin<Box<dyn Future<Output = O>>>
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
});
Action(store_value(

View File

@@ -48,7 +48,7 @@
//! # run_scope(create_runtime(), |cx| {
//! spawn_local(async {
//! let posts = read_posts(3, "my search".to_string()).await;
//! log::debug!("posts = {posts{:#?}");
//! log::debug!("posts = {posts:#?}");
//! })
//! # });
//!

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