mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 09:02:37 -05:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ae3f6f7f0 | ||
|
|
73f67d9a55 | ||
|
|
8643126d09 | ||
|
|
99d28ed045 |
48
.github/workflows/check-examples.yml
vendored
48
.github/workflows/check-examples.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all examples
|
||||
run: cargo make check-examples
|
||||
48
.github/workflows/check.yml
vendored
48
.github/workflows/check.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all libraries
|
||||
run: cargo make check
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -45,4 +45,4 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make test
|
||||
run: cargo make ci
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -4,13 +4,9 @@ members = [
|
||||
"leptos",
|
||||
"leptos_dom",
|
||||
"leptos_config",
|
||||
"leptos_hot_reload",
|
||||
"leptos_macro",
|
||||
"leptos_reactive",
|
||||
"leptos_server",
|
||||
"server_fn",
|
||||
"server_fn_macro",
|
||||
"server_fn/server_fn_macro_default",
|
||||
|
||||
# integrations
|
||||
"integrations/actix",
|
||||
@@ -25,22 +21,18 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.2"
|
||||
version = "0.2.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.2" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.2" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.2" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.2" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.2" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.2" }
|
||||
leptos_router = { path = "./router", version = "0.2.2" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.2" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.0" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.0" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.0" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.0" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.0" }
|
||||
leptos_router = { path = "./router", version = "0.2.0" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.0" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -8,20 +8,20 @@
|
||||
default_to_workspace = false
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["check", "check-examples", "test"]
|
||||
dependencies = ["build", "check-examples", "test"]
|
||||
|
||||
[tasks.check]
|
||||
[tasks.build]
|
||||
clear = true
|
||||
dependencies = ["check-all", "check-wasm"]
|
||||
dependencies = ["build-all", "build-wasm"]
|
||||
|
||||
[tasks.check-all]
|
||||
[tasks.build-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-wasm]
|
||||
[tasks.build-wasm]
|
||||
clear = true
|
||||
dependencies = [{ name = "check-wasm", path = "leptos" }]
|
||||
dependencies = [{ name = "build-wasm", path = "leptos" }]
|
||||
|
||||
[tasks.check-examples]
|
||||
clear = true
|
||||
@@ -31,17 +31,12 @@ dependencies = [
|
||||
{ name = "check", path = "examples/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
{ name = "check", path = "examples/error_boundary" },
|
||||
{ name = "check", path = "examples/errors_axum" },
|
||||
{ name = "check", path = "examples/fetch" },
|
||||
{ name = "check", path = "examples/hackernews" },
|
||||
{ name = "check", path = "examples/hackernews_axum" },
|
||||
{ name = "check", path = "examples/login_with_token_csr_only" },
|
||||
{ name = "check", path = "examples/parent_child" },
|
||||
{ name = "check", path = "examples/router" },
|
||||
{ name = "check", path = "examples/session_auth_axum" },
|
||||
{ name = "check", path = "examples/ssr_modes" },
|
||||
{ name = "check", path = "examples/ssr_modes_axum" },
|
||||
{ name = "check", path = "examples/tailwind" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_axum" },
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Getting Started
|
||||
|
||||
There are two basic paths to getting started with Leptos:
|
||||
|
||||
1. Client-side rendering with [Trunk](https://trunkrs.dev/)
|
||||
2. Full-stack rendering with [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos)
|
||||
|
||||
For the early examples, it will be easiest to begin with Trunk. We’ll introduce
|
||||
`cargo-leptos` a little later in this series.
|
||||
|
||||
|
||||
If you don’t already have it installed, you can install Trunk by running
|
||||
|
||||
```bash
|
||||
@@ -20,22 +20,12 @@ Create a basic Rust binary project
|
||||
cargo init leptos-tutorial
|
||||
```
|
||||
|
||||
> We recommend using `nightly` Rust, as it enables [a few nice features](https://github.com/leptos-rs/leptos#nightly-note). To use `nightly` Rust with WebAssembly, you can run
|
||||
>
|
||||
> ```bash
|
||||
> rustup toolchain install nightly
|
||||
> rustup default nightly
|
||||
> rustup target add wasm32-unknown-unknown
|
||||
> ```
|
||||
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
```bash
|
||||
cargo add leptos
|
||||
```
|
||||
|
||||
Create a simple `index.html` in the root of the `leptos-tutorial` directory
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -45,26 +35,14 @@ Create a simple `index.html` in the root of the `leptos-tutorial` directory
|
||||
```
|
||||
|
||||
And add a simple “Hello, world!” to your `main.rs`
|
||||
|
||||
```rust
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
}
|
||||
```
|
||||
|
||||
Your directory structure should now look something like this
|
||||
|
||||
```
|
||||
leptos_tutorial
|
||||
├── src
|
||||
│ └── main.rs
|
||||
├── Cargo.html
|
||||
├── index.html
|
||||
```
|
||||
|
||||
Now run `trunk serve --open` from the root of the `leptos-tutorial` directory.
|
||||
Trunk should automatically compile your app and open it in your default browser.
|
||||
If you make edits to `main.rs`, Trunk will recompile your source code and
|
||||
live-reload the page.
|
||||
Now run `trunk serve --open`. Trunk should automatically compile your app and
|
||||
open it in your default browser. If you make edits to `main.rs`, Trunk will
|
||||
recompile your source code and live-reload the page.
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
# Responding to Changes with `create_effect`
|
||||
|
||||
Believe it or not, we’ve made it this far without having mentioned half of the reactive system: effects.
|
||||
|
||||
Leptos is built on a fine-grained reactive system, which means that individual reactive values (“signals,” sometimes known as observables) trigger rerunning the code that reacts to them (“effects,” sometimes known as observers). These two halves of the reactive system are inter-dependent. Without effects, signals can change within the reactive system but never be observed in a way that interacts with the outside world. Without signals, effects run once but never again, as there’s no observable value to subscribe to.
|
||||
|
||||
[`create_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_effect.html) takes a function as its argument. It immediately runs the function. If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal with the reactive runtime. Whenever one of the signals that the effect depends on changes, the effect runs again.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, 0);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
// immediately prints "Value: 0" and subscribes to `a`
|
||||
log::debug!("Value: {}", a());
|
||||
});
|
||||
```
|
||||
|
||||
The effect function is called with an argument containing whatever value it returned the last time it ran. On the initial run, this is `None`.
|
||||
|
||||
By default, effects **do not run on the server**. This means you can call browser-specific APIs within the effect function without causing issues. If you need an effect to run on the server, use [`create_isomorphic_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_isomorphic_effect.html).
|
||||
|
||||
## Autotracking and Dynamic Dependencies
|
||||
|
||||
If you’re familiar with a framework like React, you might notice one key difference. React and similar frameworks typically require you to pass a “dependency array,” an explicit set of variables that determine when the effect should rerun.
|
||||
|
||||
Because Leptos comes from the tradition of synchronous reactive programming, we don’t need this explicit dependency list. Instead, we automatically track dependencies depending on which signals are accessed within the effect.
|
||||
|
||||
This has two effects (no pun intended). Dependencies are
|
||||
|
||||
1. **Automatic**: You don’t need to maintain a dependency list, or worry about what should or shouldn’t be included. The framework simply tracks which signals might cause the effect to rerun, and handles it for you.
|
||||
2. **Dynamic**: The dependency list is cleared and updated every time the effect runs. If your effect contains a conditional (for example), only signals that are used in the current branch are tracked. This means that effects rerun the absolute minimum number of times.
|
||||
|
||||
> If this sounds like magic, and if you want a deep dive into how automatic dependency tracking works, [check out this video](https://www.youtube.com/watch?v=GWB3vTWeLd4). (Apologies for the low volume!)
|
||||
|
||||
## Effects as Zero-Cost-ish Abstraction
|
||||
|
||||
While they’re not a “zero-cost abstraction” in the most technical sense—they require some additional memory use, exist at runtime, etc.—at a higher level, from the perspective of whatever expensive API calls or other work you’re doing within them, effects are a zero-cost abstraction. They rerun the absolute minimum number of times necessary, given how you’ve described them.
|
||||
|
||||
Imagine that I’m creating some kind of chat software, and I want people to be able to display their full name, or just their first name, and to notify the server whenever their name changes:
|
||||
|
||||
```rust
|
||||
let (first, set_first) = create_signal(cx, String::new());
|
||||
let (last, set_last) = create_signal(cx, String::new());
|
||||
let (use_last, set_use_last) = create_signal(cx, true);
|
||||
|
||||
// this will add the name to the log
|
||||
// any time one of the source signals changes
|
||||
create_effect(cx, move |_| {
|
||||
log(
|
||||
cx,
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
first()
|
||||
},
|
||||
)
|
||||
});
|
||||
```
|
||||
|
||||
If `use_last` is `true`, effect should rerun whenever `first`, `last`, or `use_last` changes. But if I toggle `use_last` to `false`, a change in `last` will never cause the full name to change. In fact, `last` will be removed from the dependency list until `use_last` toggles again. This saves us from sending multiple unnecessary requests to the API if I change `last` multiple times while `use_last` is still `false`.
|
||||
|
||||
## To `create_effect`, or not to `create_effect`?
|
||||
|
||||
Effects are intended to run _side-effects_ of the system, not to synchronize state _within_ the system. In other words: don’t write to signals within effects.
|
||||
|
||||
If you need to define a signal that depends on the value of other signals, use a derived signal or [`create_memo`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_memo.html).
|
||||
|
||||
If you need to synchronize some reactive value with the non-reactive world outside—like a web API, the console, the filesystem, or the DOM—create an effect.
|
||||
|
||||
> If you’re curious for more information about when you should and shouldn’t use `create_effect`, [check out this video](https://www.youtube.com/watch?v=aQOFJQ2JkvQ) for a more in-depth consideration!
|
||||
|
||||
## Effects and Rendering
|
||||
|
||||
We’ve managed to get this far without mentioning effects because they’re built into the Leptos DOM renderer. We’ve seen that you can create a signal and pass it into the `view` macro, and it will update the relevant DOM node whenever the signal changes:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<p>{count}</p>
|
||||
}
|
||||
```
|
||||
|
||||
This works because the framework essentially creates an effect wrapping this update. You can imagine Leptos translating this view into something like this:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
// create a DOM element
|
||||
let p = create_element("p");
|
||||
|
||||
// create an effect to reactively update the text
|
||||
create_effect(cx, move |prev_value| {
|
||||
// first, access the signal’s value and convert it to a string
|
||||
let text = count().to_string();
|
||||
|
||||
// if this is different from the previous value, update the node
|
||||
if prev_value != Some(text) {
|
||||
p.set_text_content(&text);
|
||||
}
|
||||
|
||||
// return this value so we can memoize the next update
|
||||
text
|
||||
});
|
||||
```
|
||||
|
||||
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
@@ -1,171 +0,0 @@
|
||||
# Global State Management
|
||||
|
||||
So far, we've only been working with local state in components
|
||||
We've only seen how to communicate between parent and child components
|
||||
But there are also more general ways to manage global state
|
||||
|
||||
The three best approaches to global state are
|
||||
|
||||
1. Using the router to drive global state via the URL
|
||||
2. Passing signals through context
|
||||
3. Creating a global state struct and creating lenses into it with `create_slice`
|
||||
|
||||
## Option #1: URL as Global State
|
||||
|
||||
The next few sections of the tutorial will be about the router.
|
||||
So for now, we'll just look at options #2 and #3.
|
||||
|
||||
## Option #2: Passing Signals through Context
|
||||
|
||||
In virtual DOM libraries like React, using the Context API to manage global
|
||||
state is a bad idea: because the entire app exists in a tree, changing
|
||||
some value provided high up in the tree can cause the whole app to render.
|
||||
|
||||
In fine-grained reactive libraries like Leptos, this is simply not the case.
|
||||
You can create a signal in the root of your app and pass it down to other
|
||||
components using provide_context(). Changing it will only cause rerendering
|
||||
in the specific places it is actually used, not the whole app.
|
||||
|
||||
We start by creating a signal in the root of the app and providing it to
|
||||
all its children and descendants using `provide_context`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
// here we create a signal in the root that can be consumed
|
||||
// anywhere in the app.
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
// we'll pass the setter to specific components,
|
||||
// but provide the count itself to the whole app via context
|
||||
provide_context(cx, count);
|
||||
|
||||
view! { cx,
|
||||
// SetterButton is allowed to modify the count
|
||||
<SetterButton set_count/>
|
||||
// These consumers can only read from it
|
||||
// But we could give them write access by passing `set_count` if we wanted
|
||||
<FancyMath/>
|
||||
<ListItems/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`<SetterButton/>` is the kind of counter we’ve written several times now.
|
||||
(See the sandbox below if you don’t understand what I mean.)
|
||||
|
||||
`<FancyMath/>` and `<ListItems/>` both consume the signal we’re providing via
|
||||
`use_context` and do something with it.
|
||||
|
||||
```rust
|
||||
/// A component that does some "fancy" math with the global count
|
||||
#[component]
|
||||
fn FancyMath(cx: Scope) -> impl IntoView {
|
||||
// here we consume the global count signal with `use_context`
|
||||
let count = use_context::<ReadSignal<u32>>(cx)
|
||||
// we know we just provided this in the parent component
|
||||
.expect("there to be a `count` signal provided");
|
||||
let is_even = move || count() & 1 == 0;
|
||||
|
||||
view! { cx,
|
||||
<div class="consumer blue">
|
||||
"The number "
|
||||
<strong>{count}</strong>
|
||||
{move || if is_even() {
|
||||
" is"
|
||||
} else {
|
||||
" is not"
|
||||
}}
|
||||
" even."
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
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,
|
||||
in some cases with more complex state changes, you may want to use a slightly more
|
||||
structured approach to global state.
|
||||
|
||||
## Option #3: Create a Global State Struct
|
||||
|
||||
You can use this approach to build a single global data structure
|
||||
that holds the state for your whole app, and then access it by
|
||||
taking fine-grained slices using
|
||||
[`create_slice`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html)
|
||||
or [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html),
|
||||
so that changing one part of the state doesn't cause parts of your
|
||||
app that depend on other parts of the state to change.
|
||||
|
||||
You can begin by defining a simple state struct:
|
||||
|
||||
```rust
|
||||
#[derive(Default, Clone, Debug)]
|
||||
struct GlobalState {
|
||||
count: u32,
|
||||
name: String,
|
||||
}
|
||||
```
|
||||
|
||||
Provide it in the root of your app so it’s available everywhere.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
// we'll provide a single signal that holds the whole state
|
||||
// each component will be responsible for creating its own "lens" into it
|
||||
let state = create_rw_signal(cx, GlobalState::default());
|
||||
provide_context(cx, state);
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
Then child components can access “slices” of that state with fine-grained
|
||||
updates via `create_slice`. Each slice signal only updates when the particular
|
||||
piece of the larger struct it accesses updates. This means you can create a single
|
||||
root signal, and then take independent, fine-grained slices of it in different
|
||||
components, each of which can update without notifying the others of changes.
|
||||
|
||||
```rust
|
||||
/// A component that updates the count in the global state.
|
||||
#[component]
|
||||
fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|
||||
let state = use_context::<RwSignal<GlobalState>>(cx).expect("state to have been provided");
|
||||
|
||||
// `create_slice` lets us create a "lens" into the data
|
||||
let (count, set_count) = create_slice(
|
||||
cx,
|
||||
// we take a slice *from* `state`
|
||||
state,
|
||||
// our getter returns a "slice" of the data
|
||||
|state| state.count,
|
||||
// our setter describes how to mutate that slice, given a new value
|
||||
|state, n| state.count = n,
|
||||
);
|
||||
|
||||
view! { cx,
|
||||
<div class="consumer blue">
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count(count() + 1);
|
||||
}
|
||||
>
|
||||
"Increment Global Count"
|
||||
</button>
|
||||
<br/>
|
||||
<span>"Count is: " {count}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clicking this button only updates `state.count`, so if we create another slice
|
||||
somewhere else that only takes `state.name`, clicking the button won’t cause
|
||||
that other slice to update. This allows you to combine the benefits of a top-down
|
||||
data flow and of fine-grained reactive updates.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px">
|
||||
@@ -17,16 +17,15 @@
|
||||
- [Async](./async/README.md)
|
||||
- [Loading Data with Resources](./async/10_resources.md)
|
||||
- [Suspense](./async/11_suspense.md)
|
||||
- [Transition](./async/12_transition.md)
|
||||
- [Actions](./async/13_actions.md)
|
||||
- [Responding to Changes with `create_effect`](./14_create_effect.md)
|
||||
- [Global State Management](./15_global_state.md)
|
||||
- [Transition]()
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [State Management]()
|
||||
- [Interlude: Advanced Reactivity]()
|
||||
- [Router]()
|
||||
- [Fundamentals]()
|
||||
- [defining `<Routes/>`]()
|
||||
- [`<A/>`]()
|
||||
- [`<Form/>`]()
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Metadata]()
|
||||
- [SSR]()
|
||||
- [Models of SSR]()
|
||||
@@ -40,4 +39,3 @@
|
||||
- [Forms]()
|
||||
- [`<ActionForm/>`s]()
|
||||
- [Turning off WebAssembly]()
|
||||
- [Advanced Reactivity]()
|
||||
|
||||
@@ -15,12 +15,12 @@ let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
// our resource
|
||||
let async_data = create_resource(cx,
|
||||
count,
|
||||
// every time `count` changes, this will run
|
||||
|value| async move {
|
||||
log!("loading data from API");
|
||||
load_data(value).await
|
||||
},
|
||||
count,
|
||||
// every time `count` changes, this will run
|
||||
|value| async move {
|
||||
log!("loading data from API");
|
||||
load_data(value).await
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@@ -40,14 +40,14 @@ So, you can show the current state of a resource in your view:
|
||||
```rust
|
||||
let once = create_resource(cx, || (), |_| async move { load_data().await });
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
}}
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
Resources also provide a `refetch()` method that allows you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
|
||||
Resources also provide a `refetch()` method that allow you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
|
||||
|
||||
<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>
|
||||
|
||||
@@ -7,11 +7,11 @@ let (count, set_count) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
}}
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -24,14 +24,14 @@ let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match (a.read(cx), b.read(cx)) {
|
||||
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
(Some(a), Some(b)) => view! { cx,
|
||||
<ShowA a/>
|
||||
<ShowA b/>
|
||||
}.into_view(cx)
|
||||
}}
|
||||
<h1>"My Data"</h1>
|
||||
{move || match (a.read(cx), b.read(cx)) {
|
||||
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
(Some(a), Some(b)) => view! { cx,
|
||||
<ShowA a/>
|
||||
<ShowA b/>
|
||||
}.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -46,22 +46,22 @@ let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
<Suspense
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
>
|
||||
<h2>"My Data"</h2>
|
||||
<h3>"A"</h3>
|
||||
{move || {
|
||||
a.read(cx)
|
||||
.map(|a| view! { cx, <ShowA a/> })
|
||||
}}
|
||||
<h3>"B"</h3>
|
||||
{move || {
|
||||
b.read(cx)
|
||||
.map(|b| view! { cx, <ShowB b/> })
|
||||
}}
|
||||
</Suspense>
|
||||
<h1>"My Data"</h1>
|
||||
<Suspense
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
>
|
||||
<h2>"My Data"</h2>
|
||||
<h3>"A"</h3>
|
||||
{move || {
|
||||
a.read(cx)
|
||||
.map(|a| view! { cx, <ShowA a/> })
|
||||
}}
|
||||
<h3>"B"</h3>
|
||||
{move || {
|
||||
b.read(cx)
|
||||
.map(|b| view! { cx, <ShowB b/> })
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# Mutating Data with Actions
|
||||
|
||||
We’ve talked about how to load `async` data with resources. Resources immediately load data and work closely with `<Suspense/>` and `<Transition/>` components to show whether data is loading in your app. But what if you just want to call some arbitrary `async` function and keep track of what it’s doing?
|
||||
|
||||
Well, you could always use [`spawn_local`](https://docs.rs/leptos/latest/leptos/fn.spawn_local.html). This allows you to just spawn an `async` task in a synchronous environment by handing the `Future` off to the browser (or, on the server, Tokio or whatever other runtime you’re using). But how do you know if it’s still pending? Well, you could just set a signal to show whether it’s loading, and another one to show the result...
|
||||
|
||||
All of this is true. Or you could use the final `async` primitive: [`create_action`](https://docs.rs/leptos/latest/leptos/fn.create_action.html).
|
||||
|
||||
Actions and resources seem similar, but they represent fundamentally different things. If you’re trying to load data by running an `async` function, either once or when some other value changes, you probably want to use `create_resource`. If you’re trying to occasionally run an `async` function in response to something like a user clicking a button, you probably want to use `create_action`.
|
||||
|
||||
Say we have some `async` function we want to run.
|
||||
|
||||
```rust
|
||||
async fn add_todo(new_title: &str) -> Uuid {
|
||||
/* do some stuff on the server to add a new todo */
|
||||
}
|
||||
```
|
||||
|
||||
`create_action` takes a reactive `Scope` and an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
|
||||
|
||||
> The input is always a single type. If you want to pass in multiple arguments, you can do it with a struct or tuple.
|
||||
>
|
||||
> ```rust
|
||||
> // if there's a single argument, just use that
|
||||
> let action1 = create_action(cx, |input: &String| {
|
||||
> let input = input.clone();
|
||||
> async move { todo!() }
|
||||
> });
|
||||
>
|
||||
> // if there are no arguments, use the unit type `()`
|
||||
> let action2 = create_action(cx, |input: &()| async { todo!() });
|
||||
>
|
||||
> // if there are multiple arguments, use a tuple
|
||||
> let action3 = create_action(cx,
|
||||
> |input: &(usize, String)| async { todo!() }
|
||||
> );
|
||||
> ```
|
||||
>
|
||||
> Because the action function takes a reference but the `Future` needs to have a `'static` lifetime, you’ll usually need to clone the value to pass it into the `Future`. This is admittedly awkward but it unlocks some powerful features like optimistic UI. We’ll see a little more about that in future chapters.
|
||||
|
||||
So in this case, all we need to do to create an action is
|
||||
|
||||
```rust
|
||||
let add_todo = create_action(cx, |input: &String| {
|
||||
let input = input.to_owned();
|
||||
async move { add_todo(&input).await }
|
||||
});
|
||||
```
|
||||
|
||||
Rather than calling `add_todo` directly, we’ll call it with `.dispatch()`, as in
|
||||
|
||||
```rust
|
||||
add_todo.dispatch("Some value".to_string());
|
||||
```
|
||||
|
||||
You can do this from an event listener, a timeout, or anywhere; because `.dispatch()` isn’t an `async` function, it can be called from a synchronous context.
|
||||
|
||||
Actions provide access to a few signals that synchronize between the asynchronous action you’re calling and the synchronous reactive system:
|
||||
|
||||
```rust
|
||||
let submitted = add_todo.input(); // RwSignal<Option<String>>
|
||||
let pending = add_todo.pending(); // ReadSignal<bool>
|
||||
let todo_id = add_todo.value(); // RwSignal<Option<Uuid>>
|
||||
```
|
||||
|
||||
This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed.
|
||||
|
||||
```rust
|
||||
let input_ref = create_node_ref::<Input>(cx);
|
||||
|
||||
view! { cx,
|
||||
<form
|
||||
on:submit=move |ev| {
|
||||
ev.prevent_default(); // don't reload the page...
|
||||
let input = input_ref.get().expect("input to exist");
|
||||
add_todo.dispatch(input.value());
|
||||
}
|
||||
>
|
||||
<label>
|
||||
"What do you need to do?"
|
||||
<input type="text"
|
||||
node_ref=input_ref
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">"Add Todo"</button>
|
||||
</form>
|
||||
// use our loading state
|
||||
<p>{move || pending().then("Loading...")}</p>
|
||||
}
|
||||
```
|
||||
|
||||
Now, there’s a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, you’ll actually most often use actions alongside server functions, [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), and the [`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Don’t worry! Maybe it will make sense later. (Or check out our [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) example now.)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
@@ -12,19 +12,19 @@ let (count, set_count) = create_signal(cx, 0);
|
||||
let double_count = move || count() * 2;
|
||||
let count_is_odd = move || count() & 1 == 1;
|
||||
let text = move || if count_is_odd() {
|
||||
"odd"
|
||||
"odd"
|
||||
} else {
|
||||
"even"
|
||||
"even"
|
||||
};
|
||||
|
||||
// an effect automatically tracks the signals it depends on
|
||||
// and reruns when they change
|
||||
// and re-runs when they change
|
||||
create_effect(cx, move |_| {
|
||||
log!("text = {}", text());
|
||||
log!("text = {}", text());
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<p>{move || text().to_uppercase()}</p>
|
||||
<p>{move || text().to_uppercase()}</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -45,7 +45,7 @@ The key phrase here is “runs some kind of code.” The natural way to “run s
|
||||
|
||||
1. virtual DOM (VDOM) frameworks like React, Yew, or Dioxus rerun a component or render function over and over, to generate a virtual DOM tree that can be reconciled with the previous result to patch the DOM
|
||||
2. compiled frameworks like Angular and Svelte divide your component templates into “create” and “update” functions, rerunning the update function when they detect a change to the component’s state
|
||||
3. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, _you_ define the functions that rerun
|
||||
3. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, _you_ define the functions that re-run
|
||||
|
||||
That’s what all our components are doing.
|
||||
|
||||
@@ -59,16 +59,16 @@ pub fn SimpleCounter(cx: Scope) -> impl IntoView {
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
view! { cx,
|
||||
<button on:click=increment>
|
||||
{value}
|
||||
</button>
|
||||
<button on:click=increment>
|
||||
{value}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `SimpleCounter` function itself runs once. The `value` signal is created once. The framework hands off the `increment` function to the browser as an event listener. When you click the button, the browser calls `increment`, which updates `value` via `set_value`. And that updates the single text node represented in our view by `{value}`.
|
||||
|
||||
Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in responsive to a change.
|
||||
Closures are key to reactivity. They provide the framework with the ability to re-run the smallest possible unit of your application in responsive to a change.
|
||||
|
||||
So remember two things:
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@ For example, instead of embedding logic in a component directly like this:
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
|
||||
// ⚠️ this is hard to test because it's embedded in the component
|
||||
let num_remaining = move || todos.with(|todos| {
|
||||
todos.iter().filter(|todo| !todo.completed).sum()
|
||||
});
|
||||
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
|
||||
// ⚠️ this is hard to test because it's embedded in the component
|
||||
let maximum = move || todos.with(|todos| {
|
||||
todos.iter().filter(|todo| todo.completed).sum()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -29,24 +29,24 @@ You could pull that logic out into a separate data structure and test it:
|
||||
pub struct Todos(Vec<Todo>);
|
||||
|
||||
impl Todos {
|
||||
pub fn num_remaining(&self) -> usize {
|
||||
todos.iter().filter(|todo| !todo.completed).sum()
|
||||
}
|
||||
pub fn remaining(&self) -> usize {
|
||||
todos.iter().filter(|todo| todo.completed).sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_remaining {
|
||||
// ...
|
||||
}
|
||||
#[test]
|
||||
fn test_remaining {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
|
||||
// ✅ this has a test associated with it
|
||||
let num_remaining = move || todos.with(Todos::num_remaining);
|
||||
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
|
||||
// ✅ this has a test associated with it
|
||||
let maximum = move || todos.with(Todos::remaining);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
>
|
||||
"Click me: "
|
||||
"Click me"
|
||||
{move || count.get()}
|
||||
</button>
|
||||
}
|
||||
@@ -61,7 +61,7 @@ Every component is a function with the following characteristics
|
||||
|
||||
## The Component Body
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
render function that reruns multiple times. You’ll typically use it to create a
|
||||
render function that re-runs multiple times. You’ll typically use it to create a
|
||||
few reactive variables, define any side effects that run in response to those values
|
||||
changing, and describe the user interface.
|
||||
|
||||
@@ -110,7 +110,7 @@ than they’ve ever used in their lives. And fair enough. Basically, passing a f
|
||||
into the view tells the framework: “Hey, this is something that might change.”
|
||||
|
||||
When we click the button and call `set_count`, the `count` signal is updated. This
|
||||
`move || count.get()` closure, whose value depends on the value of `count`, reruns,
|
||||
`move || count.get()` closure, whose value depends on the value of `count`, re-runs,
|
||||
and the framework makes a targeted update to that one specific text node, touching
|
||||
nothing else in your application. This is what allows for extremely efficient updates
|
||||
to the DOM.
|
||||
|
||||
@@ -18,7 +18,7 @@ let double_count = move || count() * 2;
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
value=count
|
||||
value=progress
|
||||
/>
|
||||
<progress
|
||||
max="50"
|
||||
@@ -212,7 +212,7 @@ fn ProgressBar<F>(
|
||||
progress: F
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> i32 + 'static,
|
||||
F: Fn() -> i32
|
||||
{
|
||||
view! { cx,
|
||||
<progress
|
||||
@@ -227,14 +227,14 @@ This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
any value that implements this `Fn()` trait.
|
||||
|
||||
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
|
||||
or as `progress: impl Fn() -> i32 + 'static,`, in part because they’re actually used to generate
|
||||
or as `progress: impl Fn() -> i32`, in part because they’re actually used to generate
|
||||
a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
|
||||
### `into` Props
|
||||
|
||||
There’s one more way we could implement this, and it would be to use `#[prop(into)]`.
|
||||
This attribute automatically calls `.into()` on the values you pass as props,
|
||||
which allows you to easily pass props with different values.
|
||||
This attribute automatically calls `.into()` on the values you pass as proprs,
|
||||
which allows you to pass props of different values easily.
|
||||
|
||||
In this case, it’s helpful to know about the
|
||||
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
|
||||
|
||||
@@ -13,7 +13,7 @@ Leptos supports to two different patterns for iterating over items:
|
||||
|
||||
Sometimes you need to show an item repeatedly, but the list you’re drawing from
|
||||
does not often change. In this case, it’s important to know that you can insert
|
||||
any `Vec<IV> where IV: IntoView` into your view. In other words, if you can render
|
||||
any `Vec<IV> where IV: IntoView` into your view. In other views, if you can render
|
||||
`T`, you can render `Vec<T>`.
|
||||
|
||||
```rust
|
||||
@@ -85,4 +85,4 @@ it is generated, and using that as an ID for the key function.
|
||||
|
||||
Check out the `<DynamicList/>` component below for an example.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="1000px"></iframe>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="100px"></iframe>
|
||||
|
||||
@@ -54,7 +54,7 @@ event.
|
||||
```rust
|
||||
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
|
||||
|
||||
let input_element: NodeRef<Input> = create_node_ref(cx);
|
||||
let input_element: NodeRef<HtmlElement<Input>> = NodeRef::new(cx);
|
||||
```
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
underlying DOM node. Its value will be set when the element is rendered.
|
||||
|
||||
@@ -26,7 +26,7 @@ things:
|
||||
all of which can be rendered. Spending time in the `Option` and `Result` docs in particular
|
||||
is one of the best ways to level up your Rust game.
|
||||
4. And always remember: to be reactive, values must be functions. You’ll see me constantly
|
||||
wrap things in a `move ||` closure, below. This is to ensure that they actually rerun
|
||||
wrap things in a `move ||` closure, below. This is to ensure that they actually re-run
|
||||
when the signal they depend on changes, keeping the UI reactive.
|
||||
|
||||
## So What?
|
||||
@@ -55,13 +55,13 @@ if it’s even. Well, how about this?
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<p>
|
||||
{move || if is_odd() {
|
||||
"Odd"
|
||||
} else {
|
||||
"Even"
|
||||
}}
|
||||
</p>
|
||||
<p>
|
||||
{move || if is_odd() {
|
||||
"Odd"
|
||||
} else {
|
||||
"Even"
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -74,15 +74,15 @@ Let’s say we want to render some text if it’s odd, and nothing if it’s eve
|
||||
|
||||
```rust
|
||||
let message = move || {
|
||||
if is_odd() {
|
||||
Some("Ding ding ding!")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if is_odd() {
|
||||
Some("Ding ding ding!")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<p>{message}</p>
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -91,7 +91,7 @@ This works fine. We can make it a little shorter if we’d like, using `bool::th
|
||||
```rust
|
||||
let message = move || is_odd().then(|| "Ding ding ding!");
|
||||
view! { cx,
|
||||
<p>{message}</p>
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -105,15 +105,15 @@ pattern matching at your disposal.
|
||||
|
||||
```rust
|
||||
let message = move || {
|
||||
match value() {
|
||||
0 => "Zero",
|
||||
1 => "One",
|
||||
n if is_odd() => "Odd",
|
||||
_ => "Even"
|
||||
}
|
||||
match value() {
|
||||
0 => "Zero",
|
||||
1 => "One",
|
||||
n if is_odd() => "Odd",
|
||||
_ => "Even"
|
||||
}
|
||||
};
|
||||
view! { cx,
|
||||
<p>{message}</p>
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -134,13 +134,13 @@ But consider the following example:
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
let message = move || if value() > 5 {
|
||||
"Big"
|
||||
"Big"
|
||||
} else {
|
||||
"Small"
|
||||
"Small"
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<p>{message}</p>
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -148,11 +148,11 @@ This _works_, for sure. But if you added a log, you might be surprised
|
||||
|
||||
```rust
|
||||
let message = move || if value() > 5 {
|
||||
log!("{}: rendering Big", value());
|
||||
"Big"
|
||||
log!("{}: rendering Big", value());
|
||||
"Big"
|
||||
} else {
|
||||
log!("{}: rendering Small", value());
|
||||
"Small"
|
||||
log!("{}: rendering Small", value());
|
||||
"Small"
|
||||
};
|
||||
```
|
||||
|
||||
@@ -177,9 +177,9 @@ like this:
|
||||
|
||||
```rust
|
||||
let message = move || if value() > 5 {
|
||||
<Big/>
|
||||
<Big/>
|
||||
} else {
|
||||
<Small/>
|
||||
<Small/>
|
||||
};
|
||||
```
|
||||
|
||||
@@ -228,20 +228,20 @@ different branches of a conditional:
|
||||
|
||||
```rust,compile_error
|
||||
view! { cx,
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }
|
||||
}}
|
||||
</main>
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }
|
||||
}}
|
||||
</main>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -265,20 +265,20 @@ Here’s the same example, with the conversion added:
|
||||
|
||||
```rust,compile_error
|
||||
view! { cx,
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }.into_any()
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }.into_any()
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
|
||||
}}
|
||||
</main>
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }.into_any()
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }.into_any()
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
|
||||
}}
|
||||
</main>
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ Let’s add an `<ErrorBoundary/>` to this example.
|
||||
fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, Ok(0));
|
||||
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
|
||||
view! { cx,
|
||||
<h1>"Error Handling"</h1>
|
||||
@@ -77,7 +77,9 @@ fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
<p>"Not a number! Errors: "</p>
|
||||
// we can render a list of errors as strings, if we'd like
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
{move || errors.unwrap()
|
||||
.get()
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
@@ -30,11 +30,11 @@ it in the child. This lets you manipulate the state of the parent from the child
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonA setter=set_toggled/>
|
||||
}
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonA setter=set_toggled/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -63,11 +63,11 @@ Another approach would be to pass a callback to the child: say, `on_click`.
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,13 +106,13 @@ in your `view` macro in `<App/>`.
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
// note the on:click instead of on_click
|
||||
// this is the same syntax as an HTML element event listener
|
||||
<ButtonC on:click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
// note the on:click instead of on_click
|
||||
// this is the same syntax as an HTML element event listener
|
||||
<ButtonC on:click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,32 +142,31 @@ tree:
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Layout(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Content/>
|
||||
</main>
|
||||
}
|
||||
view! { cx,
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
<main>
|
||||
<Content/>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Content(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<div class="content">
|
||||
<ButtonD/>
|
||||
</div>
|
||||
}
|
||||
view! { cx,
|
||||
<div class="content">
|
||||
<ButtonD/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -183,32 +182,31 @@ pass your `WriteSignal` to its props. You could do what’s sometimes called
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout set_toggled/>
|
||||
}
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout set_toggled/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Content set_toggled/>
|
||||
</main>
|
||||
}
|
||||
view! { cx,
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
<main>
|
||||
<Content set_toggled/>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
<div class="content">
|
||||
<ButtonD set_toggled/>
|
||||
</div>
|
||||
}
|
||||
view! { cx,
|
||||
<div class="content">
|
||||
<ButtonD set_toggled/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -238,26 +236,26 @@ unnecessary prop drilling.
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
|
||||
// share `set_toggled` with all children of this component
|
||||
provide_context(cx, set_toggled);
|
||||
// share `set_toggled` with all children of this component
|
||||
provide_context(cx, set_toggled);
|
||||
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
}
|
||||
|
||||
// <Layout/> and <Content/> omitted
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD(cx: Scope) -> impl IntoView {
|
||||
// use_context searches up the context tree, hoping to
|
||||
// find a `WriteSignal<bool>`
|
||||
// in this case, I .expect() because I know I provided it
|
||||
let setter = use_context::<WriteSignal<bool>>(cx)
|
||||
.expect("to have found the setter provided");
|
||||
// use_context searches up the context tree, hoping to
|
||||
// find a `WriteSignal<bool>`
|
||||
// in this case, I .expect() because I know I provided it
|
||||
let setter = use_context::<WriteSignal<bool>>(cx)
|
||||
.expect("to have found the setter provided");
|
||||
|
||||
view! { cx,
|
||||
<button
|
||||
|
||||
@@ -6,15 +6,15 @@ that enhances an HTML `<form>`. I need some way to pass all its inputs.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<Form>
|
||||
<fieldset>
|
||||
<label>
|
||||
"Some Input"
|
||||
<input type="text" name="something"/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<button>"Submit"</button>
|
||||
</Form>
|
||||
<Form>
|
||||
<fieldset>
|
||||
<label>
|
||||
"Some Input"
|
||||
<input type="text" name="something"/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<button>"Submit"</button>
|
||||
</Form>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,13 +30,13 @@ In fact, you’ve already seen these both in action in the [`<Show/>`](/view/06_
|
||||
```rust
|
||||
view! { cx,
|
||||
<Show
|
||||
// `when` is a normal prop
|
||||
// `when` is a normal prop
|
||||
when=move || value() > 5
|
||||
// `fallback` is a "render prop": a function that returns a view
|
||||
// `fallback` is a "render prop": a function that returns a view
|
||||
fallback=|cx| view! { cx, <Small/> }
|
||||
>
|
||||
// `<Big/>` (and anything else here)
|
||||
// will be given to the `children` prop
|
||||
// `<Big/>` (and anything else here)
|
||||
// will be given to the `children` prop
|
||||
<Big/>
|
||||
</Show>
|
||||
}
|
||||
@@ -62,7 +62,7 @@ where
|
||||
<h2>"Render Prop"</h2>
|
||||
{render_prop()}
|
||||
|
||||
<h2>"Children"</h2>
|
||||
<h2>"Children"</h2>
|
||||
{children(cx)}
|
||||
}
|
||||
}
|
||||
@@ -79,11 +79,11 @@ We can use the component like this:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
|
||||
// these get passed to `children`
|
||||
"Some text"
|
||||
<span>"A span"</span>
|
||||
</TakesChildren>
|
||||
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
|
||||
// these get passed to `children`
|
||||
"Some text"
|
||||
<span>"A span"</span>
|
||||
</TakesChildren>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -115,11 +115,11 @@ Calling it like this will create a list:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<WrappedChildren>
|
||||
"A"
|
||||
"B"
|
||||
"C"
|
||||
</WrappedChildren>
|
||||
<WrappedChildren>
|
||||
"A"
|
||||
"B"
|
||||
"C"
|
||||
</WrappedChildren>
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -10,7 +10,5 @@ log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
web-sys ="0.3"
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use counter::*;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counter::*;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn clear() {
|
||||
@@ -84,22 +84,22 @@ fn inc() {
|
||||
let clear = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<web_sys::HtmlElement>()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let dec = clear
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<web_sys::HtmlElement>()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let text = dec
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<web_sys::HtmlElement>()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let inc = text
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<web_sys::HtmlElement>()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
inc.click();
|
||||
|
||||
@@ -24,6 +24,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// as strings, if we'd like
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
@@ -13,7 +13,7 @@ console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
"serde",
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
@@ -27,7 +27,7 @@ gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
thiserror = "1.0.38"
|
||||
@@ -39,14 +39,14 @@ default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
@@ -54,12 +54,12 @@ denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "errors_axum"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
|
||||
@@ -34,7 +34,7 @@ where
|
||||
abort_controller.abort()
|
||||
}
|
||||
});
|
||||
T::de(&json).ok()
|
||||
T::from_json(&json).ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -49,7 +49,7 @@ where
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
T::de(&json).map_err(|e| log::error!("{e}")).ok()
|
||||
T::from_json(&json).map_err(|e| log::error!("{e}")).ok()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
|
||||
@@ -13,7 +13,7 @@ console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
@@ -26,7 +26,7 @@ gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8", optional = true }
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
@@ -38,15 +38,15 @@ default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:http",
|
||||
"leptos/ssr",
|
||||
"leptos_axum",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:http",
|
||||
"leptos/ssr",
|
||||
"leptos_axum",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
@@ -54,27 +54,27 @@ denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "hackernews_axum"
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "hackernews_axum"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
@@ -95,4 +95,4 @@ lib-features = ["hydrate"]
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
lib-default-features = false
|
||||
@@ -34,7 +34,7 @@ where
|
||||
abort_controller.abort()
|
||||
}
|
||||
});
|
||||
T::de(&json).ok()
|
||||
T::from_json(&json).ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -49,7 +49,7 @@ where
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
T::de(&json).map_err(|e| log::error!("{e}")).ok()
|
||||
T::from_json(&json).map_err(|e| log::error!("{e}")).ok()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -1,7 +1,7 @@
|
||||
# Leptos Login Example
|
||||
|
||||
This example demonstrates a scenario of a client-side rendered application
|
||||
that uses an existing API that you cannot or do not want to change.
|
||||
that uses uses an existing API that you cannot or do not want to change.
|
||||
The authentications of this example are done using an API token.
|
||||
|
||||
## Run
|
||||
|
||||
@@ -14,5 +14,5 @@ mailparse = "0.14"
|
||||
pwhash = "1.0"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.25", features = ["macros", "rt-multi-thread"] }
|
||||
tower-http = { version = "0.4", features = ["cors"] }
|
||||
tower-http = { version = "0.3", features = ["cors"] }
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
use flake
|
||||
@@ -1,113 +0,0 @@
|
||||
[package]
|
||||
name = "session_auth_axum"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { version = "0.2.0", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { version = "0.2.0", default-features = false }
|
||||
leptos_axum = { version = "0.2.0", optional = true }
|
||||
leptos_router = { version = "0.2.0", default-features = false }
|
||||
leptos_reactive = { version = "0.2.0", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_sessions_auth = { version = "7.0.0", features = [ "sqlite-rustls" ], optional = true }
|
||||
axum_database_sessions = { version = "7.0.0", features = [ "sqlite-rustls" ], optional = true }
|
||||
bcrypt = { version = "0.14", optional = true }
|
||||
async-trait = {version = "0.1.64", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:axum_sessions_auth",
|
||||
"dep:axum_database_sessions",
|
||||
"dep:async-trait",
|
||||
"dep:sqlx",
|
||||
"dep:bcrypt",
|
||||
"dep:rand",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "session_auth_axum"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Greg Johnston
|
||||
|
||||
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.
|
||||
@@ -1,9 +0,0 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -1,42 +0,0 @@
|
||||
# Leptos Authenticated Todo App Sqlite with Axum
|
||||
|
||||
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server. It lets you login, signup, and submit todos as different users, or a guest.
|
||||
|
||||
## Client Side Rendering
|
||||
This example cannot be built as a trunk standalone CSR-only app. Only the server may directly connect to the database.
|
||||
|
||||
## Server Side Rendering with cargo-leptos
|
||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||
|
||||
1. Install cargo-leptos
|
||||
```bash
|
||||
cargo install --locked cargo-leptos
|
||||
```
|
||||
2. Build the site in watch mode, recompiling on file changes
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
```
|
||||
|
||||
## Server Side Rendering without cargo-leptos
|
||||
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
|
||||
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||
1. Install wasm-pack
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
```
|
||||
2. Build the Webassembly used to hydrate the HTML from the server
|
||||
```bash
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
3. Run the server to serve the Webassembly, JS, and HTML
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
Binary file not shown.
80
examples/session_auth_axum/flake.lock
generated
80
examples/session_auth_axum/flake.lock
generated
@@ -1,80 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1676283394,
|
||||
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1672580127,
|
||||
"narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0874168639713f547c05947c76124f78441ea46c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-22.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1677292251,
|
||||
"narHash": "sha256-D+6q5Z2MQn3UFJtqsM5/AvVHi3NXKZTIMZt1JGq/spA=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "34cdbf6ad480ce13a6a526f57d8b9e609f3d65dc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.05";
|
||||
inputs.rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ (import rust-overlay) ];
|
||||
};
|
||||
in
|
||||
with pkgs; rec {
|
||||
devShells.default = mkShell {
|
||||
|
||||
shellHook = ''
|
||||
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig";
|
||||
'';
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
];
|
||||
buildInputs = [
|
||||
trunk
|
||||
sqlite
|
||||
sass
|
||||
openssl
|
||||
(rust-bin.nightly.latest.default.override {
|
||||
extensions = [ "rust-src" ];
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
})
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||
user_id INTEGER NOT NULL,
|
||||
token TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- INSERT INTO users (id, anonymous, username, password)
|
||||
-- SELECT 0, true, 'Guest', ''
|
||||
-- ON CONFLICT(id) DO UPDATE SET
|
||||
-- anonymous = EXCLUDED.anonymous,
|
||||
-- username = EXCLUDED.username;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
completed BOOLEAN,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
-- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,231 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::SqlitePool;
|
||||
use axum_sessions_auth::{SessionSqlitePool, Authentication, HasPermission};
|
||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
use crate::todo::{pool, auth};
|
||||
pub type AuthSession = axum_sessions_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
}}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub permissions: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
let permissions = HashSet::new();
|
||||
|
||||
Self {
|
||||
id: -1,
|
||||
username: "Guest".into(),
|
||||
password: "".into(),
|
||||
permissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use async_trait::async_trait;
|
||||
|
||||
impl User {
|
||||
pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = ?;",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
}
|
||||
|
||||
pub async fn get_from_username(name: String, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE username = ?")
|
||||
.bind(name)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = ?;",
|
||||
)
|
||||
.bind(sqluser.id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlPermissionTokens {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Authentication<User, i64, SqlitePool> for User {
|
||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||
let pool = pool.unwrap();
|
||||
|
||||
User::get(userid, pool)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("Cannot get user"))
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_anonymous(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HasPermission<SqlitePool> for User {
|
||||
async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool {
|
||||
self.permissions.contains(perm)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlUser {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl SqlUser {
|
||||
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
|
||||
User {
|
||||
id: self.id,
|
||||
username: self.username,
|
||||
password: self.password,
|
||||
permissions: if let Some(user_perms) = sql_user_perms {
|
||||
user_perms
|
||||
.into_iter()
|
||||
.map(|x| x.token)
|
||||
.collect::<HashSet<String>>()
|
||||
} else {
|
||||
HashSet::<String>::new()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(Foo, "/api")]
|
||||
pub async fn foo() -> Result<String, ServerFnError> {
|
||||
Ok(String::from("Bar!"))
|
||||
}
|
||||
|
||||
#[server(GetUser, "/api")]
|
||||
pub async fn get_user(cx: Scope) -> Result<Option<User>, ServerFnError> {
|
||||
let auth = auth(cx)?;
|
||||
|
||||
Ok(auth.current_user)
|
||||
}
|
||||
|
||||
#[server(Login, "/api")]
|
||||
pub async fn login(
|
||||
cx: Scope,
|
||||
username: String,
|
||||
password: String,
|
||||
remember: Option<String>,
|
||||
) -> Result<(), ServerFnError> {
|
||||
let pool = pool(cx)?;
|
||||
let auth = auth(cx)?;
|
||||
|
||||
let user: User = User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or("User does not exist.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
|
||||
match verify(password, &user.password).map_err(|e| ServerFnError::ServerError(e.to_string()))? {
|
||||
true => {
|
||||
auth.login_user(user.id);
|
||||
auth.remember_user(remember.is_some());
|
||||
leptos_axum::redirect(cx, "/");
|
||||
Ok(())
|
||||
}
|
||||
false => Err(ServerFnError::ServerError(
|
||||
"Password does not match.".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[server(Signup, "/api")]
|
||||
pub async fn signup(
|
||||
cx: Scope,
|
||||
username: String,
|
||||
password: String,
|
||||
password_confirmation: String,
|
||||
remember: Option<String>,
|
||||
) -> Result<(), ServerFnError> {
|
||||
let pool = pool(cx)?;
|
||||
let auth = auth(cx)?;
|
||||
|
||||
if password != password_confirmation {
|
||||
return Err(ServerFnError::ServerError(
|
||||
"Passwords did not match.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let password_hashed = hash(password, DEFAULT_COST).unwrap();
|
||||
|
||||
sqlx::query("INSERT INTO users (username, password) VALUES (?,?)")
|
||||
.bind(username.clone())
|
||||
.bind(password_hashed)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
|
||||
let user = User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or("Signup failed: User does not exist.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
|
||||
auth.login_user(user.id);
|
||||
auth.remember_user(remember.is_some());
|
||||
|
||||
leptos_axum::redirect(cx, "/");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(Logout, "/api")]
|
||||
pub async fn logout(cx: Scope) -> Result<(), ServerFnError> {
|
||||
let auth = auth(cx)?;
|
||||
|
||||
auth.logout_user();
|
||||
leptos_axum::redirect(cx, "/");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
use crate::errors::TodoAppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||
// here than just displaying them
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
cx: Scope,
|
||||
#[prop(optional)] outside_errors: Option<Errors>,
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => create_rw_signal(cx, e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
},
|
||||
};
|
||||
|
||||
// Get Errors from Signal
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<TodoAppError> = errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().cloned())
|
||||
.collect();
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
cfg_if! {
|
||||
if #[cfg(feature="ssr")]{
|
||||
let response = use_context::<ResponseOptions>(cx);
|
||||
if let Some(response) = response{
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view! {cx,
|
||||
<h1>"Errors"</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
view= move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
cx,
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use http::status::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum TodoAppError {
|
||||
#[error("Not Found")]
|
||||
NotFound,
|
||||
#[error("Internal Server Error")]
|
||||
InternalServerError,
|
||||
}
|
||||
|
||||
impl TodoAppError {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
TodoAppError::NotFound => StatusCode::NOT_FOUND,
|
||||
TodoAppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||
use crate::errors::TodoAppError;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view!{cx, <ErrorTemplate outside_errors=errors.clone()/>});
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
pub mod auth;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
pub mod fallback;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
use leptos::view;
|
||||
|
||||
#[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();
|
||||
|
||||
leptos::mount_to_body(|cx| {
|
||||
view! { cx, <TodoApp/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{Response, IntoResponse},
|
||||
routing::{post, get},
|
||||
extract::{Path, Extension},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
};
|
||||
use session_auth_axum::todo::*;
|
||||
use session_auth_axum::auth::*;
|
||||
use session_auth_axum::*;
|
||||
use session_auth_axum::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
|
||||
use leptos::{log, view, provide_context, LeptosOptions, get_configuration, ServerFnError};
|
||||
use std::sync::Arc;
|
||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
||||
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_sessions_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
|
||||
async fn server_fn_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, request: Request<AxumBody>) -> impl IntoResponse {
|
||||
|
||||
log!("{:?}", path);
|
||||
|
||||
handle_server_fns_with_context(path, headers, move |cx| {
|
||||
provide_context(cx, auth_session.clone());
|
||||
provide_context(cx, pool.clone());
|
||||
}, request).await
|
||||
}
|
||||
|
||||
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, auth_session.clone());
|
||||
provide_context(cx, pool.clone());
|
||||
},
|
||||
|cx| view! { cx, <TodoApp/> }
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging");
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect("sqlite:Todos.db")
|
||||
.await
|
||||
.expect("Could not make pool.");
|
||||
|
||||
// Auth section
|
||||
let session_config = SessionConfig::default().with_table_name("axum_sessions");
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config);
|
||||
session_store.initiate().await.unwrap();
|
||||
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
crate::todo::register_server_functions();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(server_fn_handler))
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
|
||||
.with_config(auth_config))
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.layer(Extension(Arc::new(leptos_options)))
|
||||
.layer(Extension(pool));
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// Only the server may directly connect to the database.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
use crate::auth::*;
|
||||
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Todo {
|
||||
id: u32,
|
||||
user: Option<User>,
|
||||
title: String,
|
||||
created_at: String,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
pub fn pool(cx: Scope) -> Result<SqlitePool, ServerFnError> {
|
||||
Ok(use_context::<SqlitePool>(cx)
|
||||
.ok_or("Pool missing.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn auth(cx: Scope) -> Result<AuthSession, ServerFnError> {
|
||||
Ok(use_context::<AuthSession>(cx)
|
||||
.ok_or("Auth session missing.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
_ = GetTodos::register();
|
||||
_ = AddTodo::register();
|
||||
_ = DeleteTodo::register();
|
||||
_ = Login::register();
|
||||
_ = Logout::register();
|
||||
_ = Signup::register();
|
||||
_ = GetUser::register();
|
||||
_ = Foo::register();
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlTodo {
|
||||
id: u32,
|
||||
user_id: i64,
|
||||
title: String,
|
||||
created_at: String,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
impl SqlTodo {
|
||||
pub async fn into_todo(self, pool: &SqlitePool) -> Todo {
|
||||
Todo {
|
||||
id: self.id,
|
||||
user: User::get(self.user_id, pool).await,
|
||||
title: self.title,
|
||||
created_at: self.created_at,
|
||||
completed: self.completed,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(GetTodos, "/api")]
|
||||
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let pool = pool(cx)?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
|
||||
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
// why can't we just have async closures?
|
||||
// let mut rows: Vec<Todo> = rows.iter().map(|t| async { t }).collect();
|
||||
|
||||
let mut converted_todos = Vec::with_capacity(todos.len());
|
||||
|
||||
for t in todos {
|
||||
let todo = t.into_todo(&pool).await;
|
||||
converted_todos.push(todo);
|
||||
}
|
||||
|
||||
let todos: Vec<Todo> = converted_todos;
|
||||
|
||||
Ok(todos)
|
||||
}
|
||||
|
||||
#[server(AddTodo, "/api")]
|
||||
pub async fn add_todo(cx: Scope, title: String) -> Result<(), ServerFnError> {
|
||||
let user = get_user(cx).await?;
|
||||
let pool = pool(cx)?;
|
||||
|
||||
let id = match user {
|
||||
Some(user) => user.id,
|
||||
None => -1,
|
||||
};
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
|
||||
match sqlx::query("INSERT INTO todos (title, user_id, completed) VALUES (?, ?, false)")
|
||||
.bind(title)
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(_row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[server(DeleteTodo, "/api")]
|
||||
pub async fn delete_todo(cx: Scope, id: u16) -> Result<(), ServerFnError> {
|
||||
let pool = pool(cx)?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let login = create_server_action::<Login>(cx);
|
||||
let logout = create_server_action::<Logout>(cx);
|
||||
let signup = create_server_action::<Signup>(cx);
|
||||
|
||||
let user = create_resource(
|
||||
cx,
|
||||
move || {
|
||||
(
|
||||
login.version().get(),
|
||||
signup.version().get(),
|
||||
logout.version().get(),
|
||||
)
|
||||
},
|
||||
move |_| get_user(cx),
|
||||
);
|
||||
provide_meta_context(cx);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/session_auth_axum.css"/>
|
||||
<Router>
|
||||
<header>
|
||||
<A href="/"><h1>"My Tasks"</h1></A>
|
||||
<Transition
|
||||
fallback=move || view! {cx, <span>"Loading..."</span>}
|
||||
>
|
||||
{move || {
|
||||
user.read(cx).map(|user| match user {
|
||||
Err(e) => view! {cx,
|
||||
<A href="/signup">"Signup"</A>", "
|
||||
<A href="/login">"Login"</A>", "
|
||||
<span>{format!("Login error: {}", e.to_string())}</span>
|
||||
}.into_view(cx),
|
||||
Ok(None) => view! {cx,
|
||||
<A href="/signup">"Signup"</A>", "
|
||||
<A href="/login">"Login"</A>", "
|
||||
<span>"Logged out."</span>
|
||||
}.into_view(cx),
|
||||
Ok(Some(user)) => view! {cx,
|
||||
<A href="/settings">"Settings"</A>", "
|
||||
<span>{format!("Logged in as: {} ({})", user.username, user.id)}</span>
|
||||
}.into_view(cx)
|
||||
})
|
||||
}}
|
||||
</Transition>
|
||||
</header>
|
||||
<hr/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<Todos/>
|
||||
</ErrorBoundary>
|
||||
}/> //Route
|
||||
<Route path="signup" view=move |cx| view! {
|
||||
cx,
|
||||
<Signup action=signup/>
|
||||
}/>
|
||||
<Route path="login" view=move |cx| view! {
|
||||
cx,
|
||||
<Login action=login />
|
||||
}/>
|
||||
<Route path="settings" view=move |cx| view! {
|
||||
cx,
|
||||
<h1>"Settings"</h1>
|
||||
<Logout action=logout />
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
let add_todo = create_server_multi_action::<AddTodo>(cx);
|
||||
let delete_todo = create_server_action::<DeleteTodo>(cx);
|
||||
let submissions = add_todo.submissions();
|
||||
|
||||
// list of todos is loaded from the server in reaction to changes
|
||||
let todos = create_resource(
|
||||
cx,
|
||||
move || (add_todo.version().get(), delete_todo.version().get()),
|
||||
move |_| get_todos(cx),
|
||||
);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>
|
||||
"Add a Todo"
|
||||
<input type="text" name="title"/>
|
||||
</label>
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
": Created at "
|
||||
{todo.created_at}
|
||||
" by "
|
||||
{
|
||||
todo.user.unwrap_or_default().username
|
||||
}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
.into_any()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Login(cx: Scope, action: Action<Login, Result<(), ServerFnError>>) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<ActionForm action=action>
|
||||
<h1>"Log In"</h1>
|
||||
<label>
|
||||
"User ID:"
|
||||
<input type="text" placeholder="User ID" maxlength="32" name="username" class="auth-input" />
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
"Password:"
|
||||
<input type="password" placeholder="Password" name="password" class="auth-input" />
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
<input type="checkbox" name="remember" class="auth-input" />
|
||||
"Remember me?"
|
||||
</label>
|
||||
<br/>
|
||||
<button type="submit" class="button">"Log In"</button>
|
||||
</ActionForm>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Signup(cx: Scope, action: Action<Signup, Result<(), ServerFnError>>) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<ActionForm action=action>
|
||||
<h1>"Sign Up"</h1>
|
||||
<label>
|
||||
"User ID:"
|
||||
<input type="text" placeholder="User ID" maxlength="32" name="username" class="auth-input" />
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
"Password:"
|
||||
<input type="password" placeholder="Password" name="password" class="auth-input" />
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
"Confirm Password:"
|
||||
<input type="password" placeholder="Password again" name="password_confirmation" class="auth-input" />
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
"Remember me?"
|
||||
<input type="checkbox" name="remember" class="auth-input" />
|
||||
</label>
|
||||
|
||||
<br/>
|
||||
<button type="submit" class="button">"Sign Up"</button>
|
||||
</ActionForm>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Logout(cx: Scope, action: Action<Logout, Result<(), ServerFnError>>) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<div id="loginbox">
|
||||
<ActionForm action=action>
|
||||
<button type="submit" class="button">"Log Out"</button>
|
||||
</ActionForm>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
|
||||
a {
|
||||
color: black;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -23,7 +23,7 @@ simple_logger = "4"
|
||||
thiserror = "1"
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["time"], optional = true}
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
@@ -41,12 +41,12 @@ ssr = [
|
||||
]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "ssr_modes"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style/main.scss"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -6,38 +6,32 @@ use leptos_router::*;
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
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/> }/>
|
||||
<Route path="" view= move |cx| view! {
|
||||
cx,
|
||||
<main 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-sky-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
|
||||
on:click=move |_| set_count.update(|count| *count += 1)
|
||||
>
|
||||
{move || if count() == 0 {
|
||||
"Click me!".to_string()
|
||||
} else {
|
||||
count().to_string()
|
||||
}}
|
||||
</button>
|
||||
</main>
|
||||
}/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<main 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>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
|
||||
! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -30,7 +30,6 @@
|
||||
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 {
|
||||
@@ -45,8 +44,6 @@ html {
|
||||
/* 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 */
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -413,13 +410,54 @@ video {
|
||||
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: ;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
::-webkit-backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
@@ -531,39 +569,11 @@ video {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-sm {
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.bg-amber-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(217 119 6 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(2 132 199 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(125 211 252 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(14 165 233 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(245 158 11 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
@@ -595,10 +605,6 @@ video {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
@@ -609,41 +615,6 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-200 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 202 202 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(14 165 233 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(125 211 252 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(3 105 161 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(7 89 133 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(153 27 27 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-sky-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Leptos Todo App Sqlite
|
||||
|
||||
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||
|
||||
## Client Side Rendering
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
|
||||
@@ -129,20 +129,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<MultiActionForm
|
||||
// we can handle client-side validation in the on:submit event
|
||||
// leptos_router implements a `FromFormData` trait that lets you
|
||||
// parse deserializable types from form data and check them
|
||||
on:submit=move |ev| {
|
||||
let data = AddTodo::from_event(&ev).expect("to parse form data");
|
||||
// silly example of validation: if the todo is "nope!", nope it
|
||||
if data.title == "nope!" {
|
||||
// ev.prevent_default() will prevent form submission
|
||||
ev.prevent_default();
|
||||
}
|
||||
}
|
||||
action=add_todo
|
||||
>
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>
|
||||
"Add a Todo"
|
||||
<input type="text" name="title"/>
|
||||
|
||||
@@ -27,7 +27,7 @@ gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
|
||||
});
|
||||
|
||||
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
|
||||
let input_ref = create_node_ref::<Input>(cx);
|
||||
let input_ref = NodeRef::<Input>::new(cx);
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let input = input_ref.get().unwrap();
|
||||
ev.stop_propagation();
|
||||
@@ -273,8 +273,8 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
|
||||
// this will be filled by node_ref=input below
|
||||
let todo_input = create_node_ref::<Input>(cx);
|
||||
// this will be filled by _ref=input below
|
||||
let todo_input = NodeRef::<Input>::new(cx);
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
@@ -294,7 +294,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
>
|
||||
<div class="view">
|
||||
<input
|
||||
node_ref=todo_input
|
||||
_ref=todo_input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={move || (todo.completed)()}
|
||||
|
||||
@@ -111,8 +111,6 @@ pub fn redirect(cx: leptos::Scope, path: &str) {
|
||||
|
||||
/// Decomposes an HTTP request into its parts, allowing you to read its headers
|
||||
/// and other data without consuming the body.
|
||||
#[deprecated(note = "Replaced with generate_request_and_parts() to allow for \
|
||||
putting LeptosRequest in the Context")]
|
||||
pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
// provide request headers as context in server scope
|
||||
let (parts, body) = req.into_parts();
|
||||
@@ -126,88 +124,6 @@ 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
|
||||
pub async fn generate_request_and_parts(
|
||||
req: Request<Body>,
|
||||
) -> (Request<Body>, RequestParts) {
|
||||
// provide request headers as context in server scope
|
||||
let (parts, body) = req.into_parts();
|
||||
let body = body::to_bytes(body).await.unwrap_or_default();
|
||||
let request_parts = RequestParts {
|
||||
method: parts.method.clone(),
|
||||
uri: parts.uri.clone(),
|
||||
headers: parts.headers.clone(),
|
||||
version: parts.version,
|
||||
body: body.clone(),
|
||||
};
|
||||
let request = Request::from_parts(parts, body.into());
|
||||
|
||||
(request, request_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
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LeptosRequest<B>(Arc<RwLock<Option<Request<B>>>>);
|
||||
|
||||
impl<B> Clone for LeptosRequest<B> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
impl<B> LeptosRequest<B> {
|
||||
/// Overwrite the contents of a LeptosRequest with a new Request<B>
|
||||
pub fn overwrite(&self, req: Option<Request<B>>) {
|
||||
let mut writable = self.0.write();
|
||||
*writable = req
|
||||
}
|
||||
/// Consume the inner Request<B> inside the LeptosRequest and return it
|
||||
///```rust, ignore
|
||||
/// use axum::{
|
||||
/// RequestPartsExt,
|
||||
/// headers::Host
|
||||
/// };
|
||||
/// #[server(GetHost, "/api")]
|
||||
/// pub async fn get_host(cx: Scope) -> Result((), ServerFnError){
|
||||
/// let req = use_context::<leptos_axum::LeptosRequest<axum::body::Body>>(cx);
|
||||
/// if let Some(req) = req{
|
||||
/// let owned_req = req.take_request().unwrap();
|
||||
/// let (mut parts, _body) = owned_req.into_parts();
|
||||
/// let host: TypedHeader<Host> = parts.extract().await().unwrap();
|
||||
/// println!("Host: {host:#?}");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn take_request(&self) -> Option<Request<B>> {
|
||||
let mut writable = self.0.write();
|
||||
writable.take()
|
||||
}
|
||||
/// Can be used to get immutable access to the interior fields of Request
|
||||
/// and do something with them
|
||||
pub fn with(&self, with_fn: impl Fn(Option<&Request<B>>)) {
|
||||
let readable = self.0.read();
|
||||
with_fn(readable.as_ref());
|
||||
}
|
||||
|
||||
/// Can be used to mutate the fields of the Request
|
||||
pub fn update(&self, update_fn: impl Fn(Option<&mut Request<B>>)) {
|
||||
let mut writable = self.0.write();
|
||||
update_fn(writable.as_mut());
|
||||
}
|
||||
}
|
||||
/// 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
|
||||
pub async fn generate_leptos_request<B>(req: Request<B>) -> LeptosRequest<B>
|
||||
where
|
||||
B: Default + std::fmt::Debug,
|
||||
{
|
||||
let leptos_request = LeptosRequest::default();
|
||||
leptos_request.overwrite(Some(req));
|
||||
leptos_request
|
||||
}
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
/// run the server function if found, and return the resulting [Response].
|
||||
///
|
||||
@@ -302,11 +218,9 @@ async fn handle_server_fns_inner(
|
||||
|
||||
additional_context(cx);
|
||||
|
||||
let (req, req_parts) =
|
||||
generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await; // Add this so we can get details about the Request
|
||||
let req_parts = generate_request_parts(req).await;
|
||||
// Add this so we can get details about the Request
|
||||
provide_context(cx, req_parts.clone());
|
||||
provide_context(cx, leptos_req);
|
||||
// Add this so that we can set headers and status of the response
|
||||
provide_context(cx, ResponseOptions::default());
|
||||
|
||||
@@ -642,10 +556,9 @@ where
|
||||
.run_until(async {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
let req_parts = generate_request_parts(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
provide_contexts(cx, full_path, req_parts, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
@@ -811,10 +724,9 @@ where
|
||||
.run_until(async {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
let req_parts = generate_request_parts(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
provide_contexts(cx, full_path, req_parts, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
@@ -840,18 +752,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn provide_contexts<B: 'static + std::fmt::Debug + std::default::Default>(
|
||||
fn provide_contexts(
|
||||
cx: Scope,
|
||||
path: String,
|
||||
req_parts: RequestParts,
|
||||
leptos_req: LeptosRequest<B>,
|
||||
default_res_options: ResponseOptions,
|
||||
) {
|
||||
let integration = ServerIntegration { path };
|
||||
provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
provide_context(cx, MetaContext::new());
|
||||
provide_context(cx, req_parts);
|
||||
provide_context(cx, leptos_req);
|
||||
provide_context(cx, default_res_options);
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
}
|
||||
@@ -994,10 +904,9 @@ where
|
||||
.run_until(async {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
let req_parts = generate_request_parts(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
provide_contexts(cx, full_path, req_parts, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ description = "Utilities to help build server integrations for the Leptos web fr
|
||||
[dependencies]
|
||||
futures = "0.3"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_hot_reload = { workspace = true }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_config = { workspace = true }
|
||||
|
||||
@@ -25,7 +25,6 @@ pub fn html_parts(
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
{}
|
||||
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
let msg = JSON.parse(ev.data);
|
||||
@@ -41,15 +40,11 @@ pub fn html_parts(
|
||||
}});
|
||||
if (!found) console.warn(`CSS hot-reload: Could not find a <link href=/\"${{msg.css}}\"> element`);
|
||||
}};
|
||||
if(msg.view) {{
|
||||
patch(msg.view);
|
||||
}}
|
||||
}};
|
||||
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
|
||||
}})()
|
||||
</script>
|
||||
"#,
|
||||
leptos_hot_reload::HOT_RELOAD_JS
|
||||
"#
|
||||
),
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
@@ -16,8 +16,7 @@ leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.14"
|
||||
server_fn = { workspace = true }
|
||||
typed-builder = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = ".", default-features = false }
|
||||
@@ -51,7 +50,6 @@ stable = [
|
||||
serde = ["leptos_reactive/serde"]
|
||||
serde-lite = ["leptos_reactive/serde-lite"]
|
||||
miniserde = ["leptos_reactive/miniserde"]
|
||||
rkyv = ["leptos_reactive/rkyv"]
|
||||
tracing = ["leptos_macro/tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
@@ -81,16 +79,4 @@ skip_feature_sets = [
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"rkyv",
|
||||
],
|
||||
[
|
||||
"miniserde",
|
||||
"rkyv",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"rkyv",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
[tasks.check-wasm]
|
||||
[tasks.build-wasm]
|
||||
clear = true
|
||||
dependencies = ["check-hydrate", "check-csr"]
|
||||
dependencies = ["build-hydrate", "build-csr"]
|
||||
|
||||
[tasks.check-hydrate]
|
||||
[tasks.build-hydrate]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"check",
|
||||
"build",
|
||||
"--no-default-features",
|
||||
"--features=hydrate",
|
||||
"--target=wasm32-unknown-unknown",
|
||||
]
|
||||
|
||||
[tasks.check-csr]
|
||||
[tasks.build-csr]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"check",
|
||||
"build",
|
||||
"--no-default-features",
|
||||
"--features=csr",
|
||||
"--target=wasm32-unknown-unknown",
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
//! `Result` types to handle errors.
|
||||
//! - [`parent_child`](https://github.com/leptos-rs/leptos/tree/main/examples/parent_child) shows four different
|
||||
//! ways a parent component can communicate with a child, including passing a closure, context, and more
|
||||
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) implements the classic to-do
|
||||
//! app in Leptos. This is a good example of a complete, simple app. In particular, you might want to
|
||||
//! see how we use [create_effect] to [serialize JSON to `localStorage`](https://github.com/leptos-rs/leptos/blob/16f084a71268ac325fbc4a5e50c260df185eadb6/examples/todomvc/src/lib.rs#L164)
|
||||
//! and [reactively call DOM methods](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L291)
|
||||
//! on [references to elements](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L254).
|
||||
//! - [`fetch`](https://github.com/leptos-rs/leptos/tree/main/examples/fetch) introduces
|
||||
//! [Resource](leptos_reactive::Resource)s, which allow you to integrate arbitrary `async` code like an
|
||||
//! HTTP request within your reactive code.
|
||||
@@ -49,10 +54,6 @@
|
||||
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) shows the basics of building an
|
||||
//! isomorphic web app. Both the server and the client import the same app code from the `todomvc` example.
|
||||
//! The server renders the app directly to an HTML string, and the client hydrates that HTML to make it interactive.
|
||||
//! You might also want to
|
||||
//! see how we use [create_effect] to [serialize JSON to `localStorage`](https://github.com/leptos-rs/leptos/blob/16f084a71268ac325fbc4a5e50c260df185eadb6/examples/todomvc/src/lib.rs#L164)
|
||||
//! and [reactively call DOM methods](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L291)
|
||||
//! on [references to elements](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L254).
|
||||
//! - [`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
|
||||
@@ -167,7 +168,6 @@ pub use leptos_server::{
|
||||
self, create_action, create_multi_action, create_server_action,
|
||||
create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError,
|
||||
};
|
||||
pub use server_fn::{self, ServerFn as _};
|
||||
pub use typed_builder;
|
||||
mod error_boundary;
|
||||
pub use error_boundary::*;
|
||||
|
||||
@@ -119,7 +119,9 @@ where
|
||||
if is_first_run(&first_run, &suspense_context) {
|
||||
let has_local_only = suspense_context.has_local_only();
|
||||
*prev_children.borrow_mut() = Some(frag.nodes.clone());
|
||||
if !has_local_only || child_runs.get() > 0 {
|
||||
if (has_local_only && child_runs.get() > 0)
|
||||
|| !has_local_only
|
||||
{
|
||||
first_run.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@ fn simple_ssr_test() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-8|open--><div \
|
||||
id=\"_0-1\"><button id=\"_0-2\">-1</button><span \
|
||||
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span \
|
||||
id=\"_0-3\">Value: \
|
||||
<!--hk=_0-4o|leptos-dyn-child-start-->0<!\
|
||||
--hk=_0-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-8|close-->"
|
||||
id=\"_0-5\">+1</button></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -56,25 +54,21 @@ fn ssr_test_with_components() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-49|open--><div id=\"_0-1\" \
|
||||
class=\"counters\"><!--hk=_0-1-0o|leptos-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-38|open--><div \
|
||||
"<div class=\"counters\" \
|
||||
id=\"_0-1\"><!--hk=_0-1-0o|leptos-counter-start--><div \
|
||||
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
id=\"_0-1-3\">Value: \
|
||||
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
|
||||
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-38|close--><!--hk=_0-1-0c|leptos-counter-end--><!\
|
||||
--hk=_0-1-5-0o|leptos-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-38|open--><div \
|
||||
id=\"_0-1-5\">+1</button></div><!\
|
||||
--hk=_0-1-0c|leptos-counter-end--><!\
|
||||
--hk=_0-1-5-0o|leptos-counter-start--><div \
|
||||
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
|
||||
id=\"_0-1-5-3\">Value: \
|
||||
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
|
||||
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5-5\">+1</button></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-38|close--><!\
|
||||
--hk=_0-1-5-0c|leptos-counter-end--></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-49|close-->"
|
||||
--hk=_0-1-5-0c|leptos-counter-end--></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -108,26 +102,22 @@ fn ssr_test_with_snake_case_components() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-101|open--><div id=\"_0-1\" \
|
||||
class=\"counters\"><!\
|
||||
--hk=_0-1-0o|leptos-snake-case-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-90|open--><div \
|
||||
"<div class=\"counters\" \
|
||||
id=\"_0-1\"><!\
|
||||
--hk=_0-1-0o|leptos-snake-case-counter-start--><div \
|
||||
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
id=\"_0-1-3\">Value: \
|
||||
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
|
||||
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-90|close--><!--hk=_0-1-0c|leptos-snake-case-counter-end--><!\
|
||||
--hk=_0-1-5-0o|leptos-snake-case-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-90|open--><div \
|
||||
id=\"_0-1-5\">+1</button></div><!\
|
||||
--hk=_0-1-0c|leptos-snake-case-counter-end--><!\
|
||||
--hk=_0-1-5-0o|leptos-snake-case-counter-start--><div \
|
||||
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
|
||||
id=\"_0-1-5-3\">Value: \
|
||||
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
|
||||
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5-5\">+1</button></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-90|close--><!\
|
||||
--hk=_0-1-5-0c|leptos-snake-case-counter-end--></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-101|close-->"
|
||||
--hk=_0-1-5-0c|leptos-snake-case-counter-end--></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -146,9 +136,7 @@ fn test_classes() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-142|open--><div id=\"_0-1\" \
|
||||
class=\"my big red \
|
||||
car\"></div><!--leptos-view|leptos-tests-ssr.rs-142|close-->"
|
||||
"<div class=\"my big red car\" id=\"_0-1\"></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -170,10 +158,8 @@ fn ssr_with_styles() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-164|open--><div id=\"_0-1\" \
|
||||
class=\" myclass\"><button id=\"_0-2\" class=\"btn \
|
||||
myclass\">-1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-164|close-->"
|
||||
"<div class=\"myclass\" id=\"_0-1\"><button class=\"btn myclass\" \
|
||||
id=\"_0-2\">-1</button></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -192,9 +178,7 @@ fn ssr_option() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-188|open--><option \
|
||||
id=\"_0-1\"></option><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-188|close-->"
|
||||
"<option id=\"_0-1\"></option>"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,8 +14,4 @@ fs = "0.0.5"
|
||||
regex = "1.7.0"
|
||||
serde = { version = "1.0.151", features = ["derive"] }
|
||||
thiserror = "1.0.38"
|
||||
typed-builder = "0.14"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
tempfile = "3"
|
||||
typed-builder = "0.12"
|
||||
|
||||
@@ -5,10 +5,7 @@ pub mod errors;
|
||||
use crate::errors::LeptosConfigError;
|
||||
use config::{Config, File, FileFormat};
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
convert::TryFrom, env::VarError, fs, net::SocketAddr, path::Path,
|
||||
str::FromStr,
|
||||
};
|
||||
use std::{convert::TryFrom, env::VarError, fs, net::SocketAddr, str::FromStr};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
@@ -120,7 +117,6 @@ impl From<&str> for Env {
|
||||
from_str(str).unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Result<String, VarError>> for Env {
|
||||
fn from(input: &Result<String, VarError>) -> Self {
|
||||
match input {
|
||||
@@ -138,67 +134,48 @@ impl TryFrom<String> for Env {
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads [LeptosOptions] from a Cargo.toml text content with layered overrides.
|
||||
/// If an env var is specified, like `LEPTOS_ENV`, it will override a setting in the file.
|
||||
pub fn get_config_from_str(text: &str) -> Result<ConfFile, LeptosConfigError> {
|
||||
let re: Regex = Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
|
||||
let start = match re.find(text) {
|
||||
Some(found) => found.start(),
|
||||
None => return Err(LeptosConfigError::ConfigSectionNotFound),
|
||||
};
|
||||
|
||||
// so that serde error messages have right line number
|
||||
let newlines = text[..start].matches('\n').count();
|
||||
let input = "\n".repeat(newlines) + &text[start..];
|
||||
let toml = input
|
||||
.replace("[package.metadata.leptos]", "[leptos_options]")
|
||||
.replace('-', "_");
|
||||
let settings = Config::builder()
|
||||
// Read the "default" configuration file
|
||||
.add_source(File::from_str(&toml, FileFormat::Toml))
|
||||
// Layer on the environment-specific values.
|
||||
// Add in settings from environment variables (with a prefix of LEPTOS and '_' as separator)
|
||||
// E.g. `LEPTOS_RELOAD_PORT=5001 would set `LeptosOptions.reload_port`
|
||||
.add_source(config::Environment::with_prefix("LEPTOS").separator("_"))
|
||||
.build()?;
|
||||
|
||||
settings
|
||||
.try_deserialize()
|
||||
.map_err(|e| LeptosConfigError::ConfigError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Loads [LeptosOptions] from a Cargo.toml with layered overrides. If an env var is specified, like `LEPTOS_ENV`,
|
||||
/// it will override a setting in the file. It takes in an optional path to a Cargo.toml file. If None is provided,
|
||||
/// you'll need to set the options as environment variables or rely on the defaults. This is the preferred
|
||||
/// approach for cargo-leptos. If Some("./Cargo.toml") is provided, Leptos will read in the settings itself. This
|
||||
/// option currently does not allow dashes in file or folder names, as all dashes become underscores
|
||||
/// option currently does not allow dashes in file or foldernames, as all dashes become underscores
|
||||
pub async fn get_configuration(
|
||||
path: Option<&str>,
|
||||
) -> Result<ConfFile, LeptosConfigError> {
|
||||
if let Some(path) = path {
|
||||
get_config_from_file(&path).await
|
||||
let text = fs::read_to_string(path)
|
||||
.map_err(|_| LeptosConfigError::ConfigNotFound)?;
|
||||
|
||||
let re: Regex =
|
||||
Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
|
||||
let start = match re.find(&text) {
|
||||
Some(found) => found.start(),
|
||||
None => return Err(LeptosConfigError::ConfigSectionNotFound),
|
||||
};
|
||||
|
||||
// so that serde error messages have right line number
|
||||
let newlines = text[..start].matches('\n').count();
|
||||
let input = "\n".repeat(newlines) + &text[start..];
|
||||
let toml = input
|
||||
.replace("[package.metadata.leptos]", "[leptos_options]")
|
||||
.replace('-', "_");
|
||||
let settings = Config::builder()
|
||||
// Read the "default" configuration file
|
||||
.add_source(File::from_str(&toml, FileFormat::Toml))
|
||||
// Layer on the environment-specific values.
|
||||
// Add in settings from environment variables (with a prefix of LEPTOS and '_' as separator)
|
||||
// E.g. `LEPTOS_RELOAD_PORT=5001 would set `LeptosOptions.reload_port`
|
||||
.add_source(
|
||||
config::Environment::with_prefix("LEPTOS").separator("_"),
|
||||
)
|
||||
.build()?;
|
||||
|
||||
settings
|
||||
.try_deserialize()
|
||||
.map_err(|e| LeptosConfigError::ConfigError(e.to_string()))
|
||||
} else {
|
||||
get_config_from_env()
|
||||
Ok(ConfFile {
|
||||
leptos_options: LeptosOptions::try_from_env()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads [LeptosOptions] from a Cargo.toml with layered overrides. Leptos will read in the settings itself. This
|
||||
/// option currently does not allow dashes in file or folder names, as all dashes become underscores
|
||||
pub async fn get_config_from_file<P: AsRef<Path>>(
|
||||
path: P,
|
||||
) -> Result<ConfFile, LeptosConfigError> {
|
||||
let text = fs::read_to_string(path)
|
||||
.map_err(|_| LeptosConfigError::ConfigNotFound)?;
|
||||
get_config_from_str(&text)
|
||||
}
|
||||
|
||||
/// Loads [LeptosOptions] from environment variables or rely on the defaults
|
||||
pub fn get_config_from_env() -> Result<ConfFile, LeptosConfigError> {
|
||||
Ok(ConfFile {
|
||||
leptos_options: LeptosOptions::try_from_env()?,
|
||||
})
|
||||
}
|
||||
|
||||
#[path = "tests.rs"]
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
use crate::{env_w_default, from_str, Env, LeptosOptions};
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
#[test]
|
||||
fn from_str_env() {
|
||||
assert!(matches!(from_str("dev").unwrap(), Env::DEV));
|
||||
assert!(matches!(from_str("development").unwrap(), Env::DEV));
|
||||
assert!(matches!(from_str("DEV").unwrap(), Env::DEV));
|
||||
assert!(matches!(from_str("DEVELOPMENT").unwrap(), Env::DEV));
|
||||
assert!(matches!(from_str("prod").unwrap(), Env::PROD));
|
||||
assert!(matches!(from_str("production").unwrap(), Env::PROD));
|
||||
assert!(matches!(from_str("PROD").unwrap(), Env::PROD));
|
||||
assert!(matches!(from_str("PRODUCTION").unwrap(), Env::PROD));
|
||||
assert!(from_str("TEST").is_err());
|
||||
assert!(from_str("?").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_w_default_test() {
|
||||
std::env::set_var("LEPTOS_CONFIG_ENV_TEST", "custom");
|
||||
assert_eq!(
|
||||
env_w_default("LEPTOS_CONFIG_ENV_TEST", "default").unwrap(),
|
||||
String::from("custom")
|
||||
);
|
||||
std::env::remove_var("LEPTOS_CONFIG_ENV_TEST");
|
||||
assert_eq!(
|
||||
env_w_default("LEPTOS_CONFIG_ENV_TEST", "default").unwrap(),
|
||||
String::from("default")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_from_env_test() {
|
||||
std::env::remove_var("LEPTOS_OUTPUT_NAME");
|
||||
assert!(LeptosOptions::try_from_env().is_err());
|
||||
|
||||
// Test config values from environment variables
|
||||
std::env::set_var("LEPTOS_OUTPUT_NAME", "app_test");
|
||||
std::env::set_var("LEPTOS_SITE_ROOT", "my_target/site");
|
||||
std::env::set_var("LEPTOS_SITE_PKG_DIR", "my_pkg");
|
||||
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80");
|
||||
std::env::set_var("LEPTOS_RELOAD_PORT", "8080");
|
||||
|
||||
let config = LeptosOptions::try_from_env().unwrap();
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 8080);
|
||||
|
||||
// Test default config values
|
||||
std::env::remove_var("LEPTOS_SITE_ROOT");
|
||||
std::env::remove_var("LEPTOS_SITE_PKG_DIR");
|
||||
std::env::remove_var("LEPTOS_SITE_ADDR");
|
||||
std::env::remove_var("LEPTOS_RELOAD_PORT");
|
||||
|
||||
let config = LeptosOptions::try_from_env().unwrap();
|
||||
assert_eq!(config.site_root, "target/site");
|
||||
assert_eq!(config.site_pkg_dir, "pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("127.0.0.1:3000").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 3001);
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
use leptos_config::{
|
||||
get_config_from_file, get_config_from_str, get_configuration, Env,
|
||||
LeptosOptions,
|
||||
};
|
||||
use std::{fs::File, io::Write, net::SocketAddr, path::Path, str::FromStr};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn env_default() {
|
||||
assert!(matches!(Env::default(), Env::DEV));
|
||||
}
|
||||
|
||||
const CARGO_TOML_CONTENT_OK: &str = r#"\
|
||||
[package.metadata.leptos]
|
||||
output-name = "app-test"
|
||||
site-root = "my_target/site"
|
||||
site-pkg-dir = "my_pkg"
|
||||
site-addr = "0.0.0.0:80"
|
||||
reload-port = "8080"
|
||||
env = "PROD"
|
||||
"#;
|
||||
|
||||
const CARGO_TOML_CONTENT_ERR: &str = r#"\
|
||||
[package.metadata.leptos]
|
||||
_output-name = "app-test"
|
||||
_site-root = "my_target/site"
|
||||
_site-pkg-dir = "my_pkg"
|
||||
_site-addr = "0.0.0.0:80"
|
||||
_reload-port = "8080"
|
||||
_env = "PROD"
|
||||
"#;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_configuration_from_file_ok() {
|
||||
let cargo_tmp = NamedTempFile::new().unwrap();
|
||||
{
|
||||
let mut output = File::create(&cargo_tmp).unwrap();
|
||||
write!(output, "{CARGO_TOML_CONTENT_OK}").unwrap();
|
||||
}
|
||||
|
||||
let path: &Path = cargo_tmp.as_ref();
|
||||
let path_s = path.to_string_lossy().to_string();
|
||||
|
||||
let config = get_configuration(Some(&path_s))
|
||||
.await
|
||||
.unwrap()
|
||||
.leptos_options;
|
||||
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 8080);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_configuration_from_invalid_file() {
|
||||
let cargo_tmp = NamedTempFile::new().unwrap();
|
||||
{
|
||||
let mut output = File::create(&cargo_tmp).unwrap();
|
||||
write!(output, "{CARGO_TOML_CONTENT_ERR}").unwrap();
|
||||
}
|
||||
let path: &Path = cargo_tmp.as_ref();
|
||||
let path_s = path.to_string_lossy().to_string();
|
||||
assert!(get_configuration(Some(&path_s)).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_configuration_from_empty_file() {
|
||||
let cargo_tmp = NamedTempFile::new().unwrap();
|
||||
{
|
||||
let mut output = File::create(&cargo_tmp).unwrap();
|
||||
write!(output, "").unwrap();
|
||||
}
|
||||
let path: &Path = cargo_tmp.as_ref();
|
||||
let path_s = path.to_string_lossy().to_string();
|
||||
assert!(get_configuration(Some(&path_s)).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_config_from_file_ok() {
|
||||
let cargo_tmp = NamedTempFile::new().unwrap();
|
||||
{
|
||||
let mut output = File::create(&cargo_tmp).unwrap();
|
||||
write!(output, "{CARGO_TOML_CONTENT_OK}").unwrap();
|
||||
}
|
||||
|
||||
let config = get_config_from_file(&cargo_tmp)
|
||||
.await
|
||||
.unwrap()
|
||||
.leptos_options;
|
||||
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 8080);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_config_from_file_invalid() {
|
||||
let cargo_tmp = NamedTempFile::new().unwrap();
|
||||
{
|
||||
let mut output = File::create(&cargo_tmp).unwrap();
|
||||
write!(output, "{CARGO_TOML_CONTENT_ERR}").unwrap();
|
||||
}
|
||||
assert!(get_config_from_file(&cargo_tmp).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_config_from_file_empty() {
|
||||
let cargo_tmp = NamedTempFile::new().unwrap();
|
||||
{
|
||||
let mut output = File::create(&cargo_tmp).unwrap();
|
||||
write!(output, "").unwrap();
|
||||
}
|
||||
assert!(get_config_from_file(&cargo_tmp).await.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_config_from_str_content() {
|
||||
let config = get_config_from_str(CARGO_TOML_CONTENT_OK)
|
||||
.unwrap()
|
||||
.leptos_options;
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 8080);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_config_from_env() {
|
||||
std::env::remove_var("LEPTOS_OUTPUT_NAME");
|
||||
assert!(get_configuration(None).await.is_err());
|
||||
|
||||
// Test config values from environment variables
|
||||
std::env::set_var("LEPTOS_OUTPUT_NAME", "app_test");
|
||||
std::env::set_var("LEPTOS_SITE_ROOT", "my_target/site");
|
||||
std::env::set_var("LEPTOS_SITE_PKG_DIR", "my_pkg");
|
||||
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80");
|
||||
std::env::set_var("LEPTOS_RELOAD_PORT", "8080");
|
||||
|
||||
let config = get_configuration(None).await.unwrap().leptos_options;
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 8080);
|
||||
|
||||
// Test default config values
|
||||
std::env::remove_var("LEPTOS_SITE_ROOT");
|
||||
std::env::remove_var("LEPTOS_SITE_PKG_DIR");
|
||||
std::env::remove_var("LEPTOS_SITE_ADDR");
|
||||
std::env::remove_var("LEPTOS_RELOAD_PORT");
|
||||
|
||||
let config = get_configuration(None).await.unwrap().leptos_options;
|
||||
assert_eq!(config.site_root, "target/site");
|
||||
assert_eq!(config.site_pkg_dir, "pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("127.0.0.1:3000").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 3001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leptos_options_builder_default() {
|
||||
let conf = LeptosOptions::builder().output_name("app_test").build();
|
||||
assert_eq!(conf.output_name, "app_test");
|
||||
assert!(matches!(conf.env, Env::DEV));
|
||||
assert_eq!(conf.site_pkg_dir, "pkg");
|
||||
assert_eq!(conf.site_root, ".");
|
||||
assert_eq!(
|
||||
conf.site_addr,
|
||||
SocketAddr::from_str("127.0.0.1:3000").unwrap()
|
||||
);
|
||||
assert_eq!(conf.reload_port, 3001);
|
||||
}
|
||||
@@ -49,18 +49,15 @@ features = [
|
||||
"BeforeUnloadEvent",
|
||||
"ClipboardEvent",
|
||||
"CompositionEvent",
|
||||
"CustomEvent",
|
||||
"DeviceMotionEvent",
|
||||
"DeviceOrientationEvent",
|
||||
"DragEvent",
|
||||
"ErrorEvent",
|
||||
"Event",
|
||||
"FocusEvent",
|
||||
"GamepadEvent",
|
||||
"HashChangeEvent",
|
||||
"InputEvent",
|
||||
"KeyboardEvent",
|
||||
"MessageEvent",
|
||||
"MouseEvent",
|
||||
"PageTransitionEvent",
|
||||
"PointerEvent",
|
||||
|
||||
@@ -63,8 +63,6 @@ pub struct ComponentRepr {
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ComponentRepr {
|
||||
@@ -207,8 +205,6 @@ impl ComponentRepr {
|
||||
children: Vec::with_capacity(1),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ cfg_if! {
|
||||
}
|
||||
}
|
||||
use leptos_reactive::Scope;
|
||||
use smallvec::SmallVec;
|
||||
use std::{borrow::Cow, cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc};
|
||||
|
||||
/// The internal representation of the [`Each`] core-component.
|
||||
|
||||
@@ -25,8 +25,6 @@ pub struct Fragment {
|
||||
id: HydrationKey,
|
||||
/// The nodes contained in the fragment.
|
||||
pub nodes: Vec<View>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>,
|
||||
}
|
||||
|
||||
impl FromIterator<View> for Fragment {
|
||||
@@ -54,12 +52,7 @@ impl Fragment {
|
||||
|
||||
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
|
||||
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
nodes,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
Self { id, nodes }
|
||||
}
|
||||
|
||||
/// Gives access to the [View] children contained within the fragment.
|
||||
@@ -71,13 +64,6 @@ impl Fragment {
|
||||
pub fn id(&self) -> &HydrationKey {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
/// Adds an optional marker indicating the view macro source.
|
||||
pub fn with_view_marker(mut self, marker: impl Into<String>) -> Self {
|
||||
self.view_marker = Some(marker.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for Fragment {
|
||||
@@ -85,11 +71,6 @@ impl IntoView for Fragment {
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> View {
|
||||
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
frag.view_marker = self.view_marker;
|
||||
}
|
||||
|
||||
frag.children = self.nodes;
|
||||
|
||||
frag.into_view(cx)
|
||||
|
||||
@@ -128,36 +128,22 @@ generate_event_types! {
|
||||
// =========================================================
|
||||
// WindowEventHandlersEventMap
|
||||
// =========================================================
|
||||
#[does_not_bubble]
|
||||
afterprint: Event,
|
||||
#[does_not_bubble]
|
||||
beforeprint: Event,
|
||||
#[does_not_bubble]
|
||||
beforeunload: BeforeUnloadEvent,
|
||||
#[does_not_bubble]
|
||||
gamepadconnected: GamepadEvent,
|
||||
#[does_not_bubble]
|
||||
gamepaddisconnected: GamepadEvent,
|
||||
hashchange: HashChangeEvent,
|
||||
#[does_not_bubble]
|
||||
languagechange: Event,
|
||||
#[does_not_bubble]
|
||||
message: MessageEvent,
|
||||
#[does_not_bubble]
|
||||
messageerror: MessageEvent,
|
||||
#[does_not_bubble]
|
||||
offline: Event,
|
||||
#[does_not_bubble]
|
||||
online: Event,
|
||||
#[does_not_bubble]
|
||||
pagehide: PageTransitionEvent,
|
||||
#[does_not_bubble]
|
||||
pageshow: PageTransitionEvent,
|
||||
popstate: PopStateEvent,
|
||||
rejectionhandled: PromiseRejectionEvent,
|
||||
#[does_not_bubble]
|
||||
storage: StorageEvent,
|
||||
#[does_not_bubble]
|
||||
unhandledrejection: PromiseRejectionEvent,
|
||||
#[does_not_bubble]
|
||||
unload: Event,
|
||||
@@ -175,9 +161,7 @@ generate_event_types! {
|
||||
beforeinput: InputEvent,
|
||||
#[does_not_bubble]
|
||||
blur: FocusEvent,
|
||||
#[does_not_bubble]
|
||||
canplay: Event,
|
||||
#[does_not_bubble]
|
||||
canplaythrough: Event,
|
||||
change: Event,
|
||||
click: MouseEvent,
|
||||
@@ -187,7 +171,6 @@ generate_event_types! {
|
||||
compositionstart: CompositionEvent,
|
||||
compositionupdate: CompositionEvent,
|
||||
contextmenu: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
cuechange: Event,
|
||||
dblclick: MouseEvent,
|
||||
drag: DragEvent,
|
||||
@@ -197,11 +180,8 @@ generate_event_types! {
|
||||
dragover: DragEvent,
|
||||
dragstart: DragEvent,
|
||||
drop: DragEvent,
|
||||
#[does_not_bubble]
|
||||
durationchange: Event,
|
||||
#[does_not_bubble]
|
||||
emptied: Event,
|
||||
#[does_not_bubble]
|
||||
ended: Event,
|
||||
#[does_not_bubble]
|
||||
error: ErrorEvent,
|
||||
@@ -212,43 +192,32 @@ generate_event_types! {
|
||||
#[does_not_bubble]
|
||||
focusout: FocusEvent,
|
||||
formdata: Event, // web_sys does not include `FormDataEvent`
|
||||
#[does_not_bubble]
|
||||
gotpointercapture: PointerEvent,
|
||||
input: Event,
|
||||
#[does_not_bubble]
|
||||
invalid: Event,
|
||||
keydown: KeyboardEvent,
|
||||
keypress: KeyboardEvent,
|
||||
keyup: KeyboardEvent,
|
||||
#[does_not_bubble]
|
||||
load: Event,
|
||||
#[does_not_bubble]
|
||||
loadeddata: Event,
|
||||
#[does_not_bubble]
|
||||
loadedmetadata: Event,
|
||||
#[does_not_bubble]
|
||||
loadstart: Event,
|
||||
lostpointercapture: PointerEvent,
|
||||
mousedown: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
mouseenter: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
mouseleave: MouseEvent,
|
||||
mousemove: MouseEvent,
|
||||
mouseout: MouseEvent,
|
||||
mouseover: MouseEvent,
|
||||
mouseup: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
pause: Event,
|
||||
#[does_not_bubble]
|
||||
play: Event,
|
||||
#[does_not_bubble]
|
||||
playing: Event,
|
||||
pointercancel: PointerEvent,
|
||||
pointerdown: PointerEvent,
|
||||
#[does_not_bubble]
|
||||
pointerenter: PointerEvent,
|
||||
#[does_not_bubble]
|
||||
pointerleave: PointerEvent,
|
||||
pointermove: PointerEvent,
|
||||
pointerout: PointerEvent,
|
||||
@@ -256,29 +225,21 @@ generate_event_types! {
|
||||
pointerup: PointerEvent,
|
||||
#[does_not_bubble]
|
||||
progress: ProgressEvent,
|
||||
#[does_not_bubble]
|
||||
ratechange: Event,
|
||||
reset: Event,
|
||||
#[does_not_bubble]
|
||||
resize: UiEvent,
|
||||
#[does_not_bubble]
|
||||
scroll: Event,
|
||||
securitypolicyviolation: SecurityPolicyViolationEvent,
|
||||
#[does_not_bubble]
|
||||
seeked: Event,
|
||||
#[does_not_bubble]
|
||||
seeking: Event,
|
||||
select: Event,
|
||||
#[does_not_bubble]
|
||||
selectionchange: Event,
|
||||
selectstart: Event,
|
||||
slotchange: Event,
|
||||
#[does_not_bubble]
|
||||
stalled: Event,
|
||||
submit: SubmitEvent,
|
||||
#[does_not_bubble]
|
||||
suspend: Event,
|
||||
#[does_not_bubble]
|
||||
timeupdate: Event,
|
||||
#[does_not_bubble]
|
||||
toggle: Event,
|
||||
@@ -290,9 +251,7 @@ generate_event_types! {
|
||||
transitionend: TransitionEvent,
|
||||
transitionrun: TransitionEvent,
|
||||
transitionstart: TransitionEvent,
|
||||
#[does_not_bubble]
|
||||
volumechange: Event,
|
||||
#[does_not_bubble]
|
||||
waiting: Event,
|
||||
webkitanimationend: Event,
|
||||
webkitanimationiteration: Event,
|
||||
@@ -304,11 +263,8 @@ generate_event_types! {
|
||||
// WindowEventMap
|
||||
// =========================================================
|
||||
DOMContentLoaded: Event,
|
||||
#[does_not_bubble]
|
||||
devicemotion: DeviceMotionEvent,
|
||||
#[does_not_bubble]
|
||||
deviceorientation: DeviceOrientationEvent,
|
||||
#[does_not_bubble]
|
||||
orientationchange: Event,
|
||||
|
||||
// =========================================================
|
||||
@@ -325,18 +281,16 @@ generate_event_types! {
|
||||
fullscreenerror: Event,
|
||||
pointerlockchange: Event,
|
||||
pointerlockerror: Event,
|
||||
#[does_not_bubble]
|
||||
readystatechange: Event,
|
||||
visibilitychange: Event,
|
||||
}
|
||||
|
||||
// Export `web_sys` event types
|
||||
pub use web_sys::{
|
||||
AnimationEvent, BeforeUnloadEvent, CompositionEvent, CustomEvent,
|
||||
DeviceMotionEvent, DeviceOrientationEvent, DragEvent, ErrorEvent, Event,
|
||||
FocusEvent, GamepadEvent, HashChangeEvent, InputEvent, KeyboardEvent,
|
||||
MouseEvent, PageTransitionEvent, PointerEvent, PopStateEvent,
|
||||
ProgressEvent, PromiseRejectionEvent, SecurityPolicyViolationEvent,
|
||||
StorageEvent, SubmitEvent, TouchEvent, TransitionEvent, UiEvent,
|
||||
WheelEvent,
|
||||
AnimationEvent, BeforeUnloadEvent, CompositionEvent, DeviceMotionEvent,
|
||||
DeviceOrientationEvent, DragEvent, ErrorEvent, FocusEvent, GamepadEvent,
|
||||
HashChangeEvent, InputEvent, KeyboardEvent, MouseEvent,
|
||||
PageTransitionEvent, PointerEvent, PopStateEvent, ProgressEvent,
|
||||
PromiseRejectionEvent, SecurityPolicyViolationEvent, StorageEvent,
|
||||
SubmitEvent, TouchEvent, TransitionEvent, UiEvent, WheelEvent,
|
||||
};
|
||||
|
||||
@@ -90,8 +90,6 @@ where
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
span: ::tracing::Span::current(),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,8 +259,6 @@ cfg_if! {
|
||||
pub(crate) span: ::tracing::Span,
|
||||
pub(crate) cx: Scope,
|
||||
pub(crate) element: El,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>
|
||||
}
|
||||
// Server needs to build a virtualized DOM tree
|
||||
} else {
|
||||
@@ -278,9 +274,7 @@ cfg_if! {
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) children: SmallVec<[View; 4]>,
|
||||
#[educe(Debug(ignore))]
|
||||
pub(crate) prerendered: Option<Cow<'static, str>>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>
|
||||
pub(crate) prerendered: Option<Cow<'static, str>>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,9 +302,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
cx,
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
span: ::tracing::Span::current(),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None
|
||||
span: ::tracing::Span::current()
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
@@ -318,9 +310,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
attrs: smallvec![],
|
||||
children: smallvec![],
|
||||
element,
|
||||
prerendered: None,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None
|
||||
prerendered: None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,18 +329,9 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
children: smallvec![],
|
||||
element,
|
||||
prerendered: Some(html.into()),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
/// Adds an optional marker indicating the view macro source.
|
||||
pub fn with_view_marker(mut self, marker: impl Into<String>) -> Self {
|
||||
self.view_marker = Some(marker.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Converts this element into [`HtmlElement<AnyElement>`].
|
||||
pub fn into_any(self) -> HtmlElement<AnyElement> {
|
||||
cfg_if! {
|
||||
@@ -359,9 +340,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
cx,
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
span,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker
|
||||
span
|
||||
} = self;
|
||||
|
||||
HtmlElement {
|
||||
@@ -372,9 +351,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
is_void: element.is_void(),
|
||||
},
|
||||
#[cfg(debug_assertions)]
|
||||
span,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker
|
||||
span
|
||||
}
|
||||
} else {
|
||||
let Self {
|
||||
@@ -382,9 +359,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
attrs,
|
||||
children,
|
||||
element,
|
||||
prerendered,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker
|
||||
prerendered
|
||||
} = self;
|
||||
|
||||
HtmlElement {
|
||||
@@ -395,10 +370,8 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
element: AnyElement {
|
||||
name: element.name(),
|
||||
is_void: element.is_void(),
|
||||
id: element.hydration_id().clone()
|
||||
id: element.hydration_id().clone(),
|
||||
},
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -603,17 +576,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a list of classes separated by ASCII whitespace to an element.
|
||||
#[track_caller]
|
||||
pub fn classes(self, classes: impl Into<Cow<'static, str>>) -> Self {
|
||||
let classes = classes.into();
|
||||
let mut this = self;
|
||||
for class in classes.split_ascii_whitespace() {
|
||||
this = this.class(class.to_string(), true);
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
/// Sets a property on an element.
|
||||
#[track_caller]
|
||||
pub fn prop(
|
||||
@@ -769,8 +731,6 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
|
||||
mut attrs,
|
||||
children,
|
||||
prerendered,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker,
|
||||
..
|
||||
} = self;
|
||||
|
||||
@@ -789,11 +749,6 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
|
||||
element.children.extend(children);
|
||||
element.prerendered = prerendered;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
element.view_marker = view_marker;
|
||||
}
|
||||
|
||||
View::Element(element)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,24 +30,6 @@ cfg_if! {
|
||||
map
|
||||
});
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) static VIEW_MARKERS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
|
||||
let document = crate::document();
|
||||
let body = document.body().unwrap();
|
||||
let walker = document
|
||||
.create_tree_walker_with_what_to_show(&body, 128)
|
||||
.unwrap();
|
||||
let mut map = HashMap::new();
|
||||
while let Ok(Some(node)) = walker.next_node() {
|
||||
if let Some(content) = node.text_content() {
|
||||
if let Some(id) = content.strip_prefix("leptos-view|") {
|
||||
map.insert(id.into(), node.unchecked_into());
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
});
|
||||
|
||||
static IS_HYDRATING: RefCell<LazyCell<bool>> = RefCell::new(LazyCell::new(|| {
|
||||
#[cfg(debug_assertions)]
|
||||
return crate::document().get_element_by_id("_0-0-0").is_some()
|
||||
|
||||
@@ -148,9 +148,6 @@ cfg_if! {
|
||||
pub name: Cow<'static, str>,
|
||||
#[doc(hidden)]
|
||||
pub element: web_sys::HtmlElement,
|
||||
#[cfg(debug_assertions)]
|
||||
/// Optional marker for the view macro source of the element.
|
||||
pub view_marker: Option<String>
|
||||
}
|
||||
|
||||
impl fmt::Debug for Element {
|
||||
@@ -170,9 +167,6 @@ cfg_if! {
|
||||
children: Vec<View>,
|
||||
prerendered: Option<Cow<'static, str>>,
|
||||
id: HydrationKey,
|
||||
#[cfg(debug_assertions)]
|
||||
/// Optional marker for the view macro source, in debug mode.
|
||||
pub view_marker: Option<String>
|
||||
}
|
||||
|
||||
impl fmt::Debug for Element {
|
||||
@@ -206,12 +200,7 @@ impl Element {
|
||||
pub fn into_html_element(self, cx: Scope) -> HtmlElement<AnyElement> {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
let Self {
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker,
|
||||
..
|
||||
} = self;
|
||||
let Self { element, .. } = self;
|
||||
|
||||
let name = element.node_name().to_ascii_lowercase();
|
||||
|
||||
@@ -226,8 +215,6 @@ impl Element {
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
span: ::tracing::Span::current(),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,8 +227,6 @@ impl Element {
|
||||
children,
|
||||
id,
|
||||
prerendered,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker,
|
||||
} = self;
|
||||
|
||||
let element = AnyElement { name, is_void, id };
|
||||
@@ -252,8 +237,6 @@ impl Element {
|
||||
attrs,
|
||||
children: children.into_iter().collect(),
|
||||
prerendered,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,8 +258,6 @@ impl Element {
|
||||
#[cfg(debug_assertions)]
|
||||
name: el.name(),
|
||||
element: el.as_ref().clone(),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -286,9 +267,7 @@ impl Element {
|
||||
attrs: Default::default(),
|
||||
children: Default::default(),
|
||||
id: el.hydration_id().clone(),
|
||||
prerendered: None,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None
|
||||
prerendered: None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -451,12 +430,6 @@ impl<const N: usize> IntoView for [View; N] {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for &Fragment {
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
self.to_owned().into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for View {
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
|
||||
@@ -67,7 +67,7 @@ pub fn console_error(s: &str) {
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::error_1(&JsValue::from_str(s));
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -267,12 +267,12 @@ pub fn attribute_helper(
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) {
|
||||
attribute_expression(&el, &name, new.clone(), true);
|
||||
attribute_expression(&el, &name, new.clone());
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
_ => attribute_expression(el, &name, value, false),
|
||||
_ => attribute_expression(el, &name, value),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -281,44 +281,39 @@ pub(crate) fn attribute_expression(
|
||||
el: &web_sys::Element,
|
||||
attr_name: &str,
|
||||
value: Attribute,
|
||||
force: bool,
|
||||
) {
|
||||
use crate::HydrationCtx;
|
||||
|
||||
if force || !HydrationCtx::is_hydrating() {
|
||||
match value {
|
||||
Attribute::String(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(value);
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
}
|
||||
Attribute::Option(_, value) => {
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(&value.unwrap_or_default());
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
match value {
|
||||
Some(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
None => el.remove_attribute(attr_name).unwrap_throw(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Attribute::Bool(value) => {
|
||||
match value {
|
||||
Attribute::String(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(value);
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
if value {
|
||||
el.set_attribute(attr_name, attr_name).unwrap_throw();
|
||||
} else {
|
||||
el.remove_attribute(attr_name).unwrap_throw();
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
}
|
||||
Attribute::Option(_, value) => {
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(&value.unwrap_or_default());
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
match value {
|
||||
Some(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
None => el.remove_attribute(attr_name).unwrap_throw(),
|
||||
}
|
||||
}
|
||||
_ => panic!("Remove nested Fn in Attribute"),
|
||||
}
|
||||
Attribute::Bool(value) => {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
if value {
|
||||
el.set_attribute(attr_name, attr_name).unwrap_throw();
|
||||
} else {
|
||||
el.remove_attribute(attr_name).unwrap_throw();
|
||||
}
|
||||
}
|
||||
_ => panic!("Remove nested Fn in Attribute"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use leptos_reactive::Scope;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
/// Represents the different possible values a single class on an element could have,
|
||||
/// allowing you to do fine-grained updates to single items
|
||||
@@ -83,14 +85,12 @@ pub fn class_helper(
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) && (old.is_some() || new) {
|
||||
class_expression(&class_list, &name, new, true)
|
||||
class_expression(&class_list, &name, new)
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Class::Value(value) => {
|
||||
class_expression(&class_list, &name, value, false)
|
||||
}
|
||||
Class::Value(value) => class_expression(&class_list, &name, value),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,21 +99,11 @@ pub(crate) fn class_expression(
|
||||
class_list: &web_sys::DomTokenList,
|
||||
class_name: &str,
|
||||
value: bool,
|
||||
force: bool,
|
||||
) {
|
||||
use crate::HydrationCtx;
|
||||
|
||||
if force || !HydrationCtx::is_hydrating() {
|
||||
let class_name = wasm_bindgen::intern(class_name);
|
||||
|
||||
if value {
|
||||
if let Err(e) = class_list.add_1(class_name) {
|
||||
crate::error!("[HtmlElement::class()] {e:?}");
|
||||
}
|
||||
} else {
|
||||
if let Err(e) = class_list.remove_1(class_name) {
|
||||
crate::error!("[HtmlElement::class()] {e:?}");
|
||||
}
|
||||
}
|
||||
let class_name = wasm_bindgen::intern(class_name);
|
||||
if value {
|
||||
class_list.add_1(class_name).unwrap_throw();
|
||||
} else {
|
||||
class_list.remove_1(class_name).unwrap_throw();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
/// let html = leptos::ssr::render_to_string(|cx| view! { cx,
|
||||
/// <p>"Hello, world!"</p>
|
||||
/// });
|
||||
/// // trim off the beginning, which has a bunch of hydration info, for comparison
|
||||
/// assert!(html.contains("Hello, world!</p>"));
|
||||
/// // static HTML includes some hydration info
|
||||
/// assert_eq!(html, "<p id=\"_0-1\">Hello, world!</p>");
|
||||
/// # }}
|
||||
/// ```
|
||||
pub fn render_to_string<F, N>(f: F) -> String
|
||||
@@ -132,7 +132,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
let (
|
||||
(shell, prefix, pending_resources, pending_fragments, serializers),
|
||||
scope,
|
||||
disposer,
|
||||
_,
|
||||
) = run_scope_undisposed(runtime, {
|
||||
move |cx| {
|
||||
// Add additional context items
|
||||
@@ -164,31 +164,31 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
// TODO can remove id_before_suspense entirely now
|
||||
let fragments = fragments.map(|(fragment_id, html)| {
|
||||
format!(
|
||||
r#"
|
||||
<template id="{fragment_id}f">{html}</template>
|
||||
<script>
|
||||
var id = "{fragment_id}";
|
||||
var open;
|
||||
var close;
|
||||
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
||||
while(walker.nextNode()) {{
|
||||
if(walker.currentNode.textContent == `suspense-open-${{id}}`) {{
|
||||
open = walker.currentNode;
|
||||
}} else if(walker.currentNode.textContent == `suspense-close-${{id}}`) {{
|
||||
close = walker.currentNode;
|
||||
}}
|
||||
}}
|
||||
var range = new Range();
|
||||
range.setStartAfter(open);
|
||||
range.setEndBefore(close);
|
||||
range.deleteContents();
|
||||
var tpl = document.getElementById("{fragment_id}f");
|
||||
close.parentNode.insertBefore(tpl.content.cloneNode(true), close);
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
});
|
||||
format!(
|
||||
r#"
|
||||
<template id="{fragment_id}f">{html}</template>
|
||||
<script>
|
||||
var id = "{fragment_id}";
|
||||
var open;
|
||||
var close;
|
||||
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
||||
while(walker.nextNode()) {{
|
||||
if(walker.currentNode.textContent == `suspense-open-${{id}}`) {{
|
||||
open = walker.currentNode;
|
||||
}} else if(walker.currentNode.textContent == `suspense-close-${{id}}`) {{
|
||||
close = walker.currentNode;
|
||||
}}
|
||||
}}
|
||||
var range = new Range();
|
||||
range.setStartAfter(open);
|
||||
range.setEndBefore(close);
|
||||
range.deleteContents();
|
||||
var tpl = document.getElementById("{fragment_id}f");
|
||||
close.parentNode.insertBefore(tpl.content.cloneNode(true), close);
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
});
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = render_serializers(serializers);
|
||||
|
||||
@@ -196,25 +196,20 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
let stream = futures::stream::once(async move {
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
{shell}
|
||||
<script>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
"#
|
||||
{prefix}
|
||||
{shell}
|
||||
<script>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
.chain(fragments)
|
||||
.chain(resources)
|
||||
// dispose of the root scope
|
||||
.chain(futures::stream::once(async move {
|
||||
disposer.dispose();
|
||||
Default::default()
|
||||
}));
|
||||
.chain(resources);
|
||||
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
@@ -237,17 +232,12 @@ impl View {
|
||||
};
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let content = format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&node.id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true),
|
||||
name = to_kebab_case(&node.name)
|
||||
);
|
||||
if let Some(id) = node.view_marker {
|
||||
format!("<!--leptos-view|{id}|open-->{content}<!--leptos-view|{id}|close-->").into()
|
||||
} else {
|
||||
content.into()
|
||||
}
|
||||
).into()
|
||||
} else {
|
||||
format!(
|
||||
r#"{}<!--hk={}-->"#,
|
||||
@@ -385,7 +375,7 @@ impl View {
|
||||
}
|
||||
}
|
||||
View::Element(el) => {
|
||||
let el_html = if let Some(prerendered) = el.prerendered {
|
||||
if let Some(prerendered) = el.prerendered {
|
||||
prerendered
|
||||
} else {
|
||||
let tag_name = el.name;
|
||||
@@ -430,17 +420,6 @@ impl View {
|
||||
format!("<{tag_name}{attrs}>{children}</{tag_name}>")
|
||||
.into()
|
||||
}
|
||||
};
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
if let Some(id) = el.view_marker {
|
||||
format!("<!--leptos-view|{id}|open-->{el_html}<!--leptos-view|{id}|close-->").into()
|
||||
} else {
|
||||
el_html
|
||||
}
|
||||
} else {
|
||||
el_html
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Transparent(_) => Default::default(),
|
||||
|
||||
@@ -76,7 +76,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
// create the runtime
|
||||
let runtime = create_runtime();
|
||||
|
||||
let ((chunks, prefix, pending_resources, serializers), scope_id, disposer) =
|
||||
let ((chunks, prefix, pending_resources, serializers), scope_id, _) =
|
||||
run_scope_undisposed(runtime, |cx| {
|
||||
// add additional context
|
||||
additional_context(cx);
|
||||
@@ -111,12 +111,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
)
|
||||
})
|
||||
.chain(rx)
|
||||
.chain(render_serializers(serializers))
|
||||
// dispose of the scope
|
||||
.chain(futures::stream::once(async move {
|
||||
disposer.dispose();
|
||||
Default::default()
|
||||
}));
|
||||
.chain(render_serializers(serializers));
|
||||
|
||||
(stream, runtime, scope_id)
|
||||
}
|
||||
@@ -180,12 +175,6 @@ impl View {
|
||||
}
|
||||
}
|
||||
View::Element(el) => {
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(id) = &el.view_marker {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!("<!--leptos-view|{id}|open-->").into(),
|
||||
));
|
||||
}
|
||||
if let Some(prerendered) = el.prerendered {
|
||||
chunks.push(StreamChunk::Sync(prerendered))
|
||||
} else {
|
||||
@@ -240,12 +229,6 @@ impl View {
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(id) = &el.view_marker {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!("<!--leptos-view|{id}|close-->").into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
View::Transparent(_) => {}
|
||||
View::CoreComponent(node) => {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
[package]
|
||||
name = "leptos_hot_reload"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Utility types used for dev mode and hot-reloading for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
syn = { version = "1", features = [
|
||||
"full",
|
||||
"parsing",
|
||||
"extra-traits",
|
||||
"visit",
|
||||
"printing",
|
||||
] }
|
||||
quote = "1"
|
||||
syn-rsx = "0.9"
|
||||
proc-macro2 = { version = "1", features = ["span-locations", "nightly"] }
|
||||
parking_lot = "0.12"
|
||||
walkdir = "2"
|
||||
camino = "1.1.3"
|
||||
indexmap = "1.9.2"
|
||||
@@ -1,550 +0,0 @@
|
||||
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>>);
|
||||
|
||||
impl LNode {
|
||||
pub fn diff(&self, other: &LNode) -> Vec<Patch> {
|
||||
let mut old_children = OldChildren::default();
|
||||
self.add_old_children(vec![], &mut old_children);
|
||||
self.diff_at(other, &[], &old_children)
|
||||
}
|
||||
|
||||
fn to_replacement_node(
|
||||
&self,
|
||||
old_children: &OldChildren,
|
||||
) -> ReplacementNode {
|
||||
match old_children.0.get(self) {
|
||||
// if the child already exists in the DOM, we can pluck it out
|
||||
// and move it around
|
||||
Some(path) => ReplacementNode::Path(path.to_owned()),
|
||||
// otherwise, we should generate some HTML
|
||||
// but we need to do this recursively in case we're replacing an element
|
||||
// with children who need to be plucked out
|
||||
None => match self {
|
||||
LNode::Fragment(fragment) => ReplacementNode::Fragment(
|
||||
fragment
|
||||
.iter()
|
||||
.map(|node| node.to_replacement_node(old_children))
|
||||
.collect(),
|
||||
),
|
||||
LNode::Element {
|
||||
name,
|
||||
attrs,
|
||||
children,
|
||||
} => ReplacementNode::Element {
|
||||
name: name.to_owned(),
|
||||
attrs: attrs
|
||||
.iter()
|
||||
.filter_map(|(name, value)| match value {
|
||||
LAttributeValue::Boolean => {
|
||||
Some((name.to_owned(), name.to_owned()))
|
||||
}
|
||||
LAttributeValue::Static(value) => {
|
||||
Some((name.to_owned(), value.to_owned()))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
children: children
|
||||
.iter()
|
||||
.map(|node| node.to_replacement_node(old_children))
|
||||
.collect(),
|
||||
},
|
||||
LNode::Text(_)
|
||||
| LNode::Component(_, _)
|
||||
| LNode::DynChild(_) => ReplacementNode::Html(self.to_html()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn add_old_children(&self, path: Vec<usize>, positions: &mut OldChildren) {
|
||||
match self {
|
||||
LNode::Fragment(frag) => {
|
||||
for (idx, child) in frag.iter().enumerate() {
|
||||
let mut new_path = path.clone();
|
||||
new_path.push(idx);
|
||||
child.add_old_children(new_path, positions);
|
||||
}
|
||||
}
|
||||
LNode::Element { children, .. } => {
|
||||
for (idx, child) in children.iter().enumerate() {
|
||||
let mut new_path = path.clone();
|
||||
new_path.push(idx);
|
||||
child.add_old_children(new_path, positions);
|
||||
}
|
||||
}
|
||||
// only need to insert dynamic content, as these might change
|
||||
LNode::Component(_, _) | LNode::DynChild(_) => {
|
||||
positions.0.insert(self.clone(), path);
|
||||
}
|
||||
// can just create text nodes, whatever
|
||||
LNode::Text(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_at(
|
||||
&self,
|
||||
other: &LNode,
|
||||
path: &[usize],
|
||||
orig_children: &OldChildren,
|
||||
) -> Vec<Patch> {
|
||||
if std::mem::discriminant(self) != std::mem::discriminant(other) {
|
||||
return vec![Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::ReplaceWith(
|
||||
other.to_replacement_node(orig_children),
|
||||
),
|
||||
}];
|
||||
}
|
||||
match (self, other) {
|
||||
// fragment: diff children
|
||||
(LNode::Fragment(old), LNode::Fragment(new)) => {
|
||||
LNode::diff_children(path, old, new, orig_children)
|
||||
}
|
||||
// text node: replace text
|
||||
(LNode::Text(_), LNode::Text(new)) => vec![Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::SetText(new.to_owned()),
|
||||
}],
|
||||
// elements
|
||||
(
|
||||
LNode::Element {
|
||||
name: old_name,
|
||||
attrs: old_attrs,
|
||||
children: old_children,
|
||||
},
|
||||
LNode::Element {
|
||||
name: new_name,
|
||||
attrs: new_attrs,
|
||||
children: new_children,
|
||||
},
|
||||
) => {
|
||||
let tag_patch = (old_name != new_name).then(|| Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::ChangeTagName(new_name.to_owned()),
|
||||
});
|
||||
|
||||
let attrs_patch = LNode::diff_attrs(path, old_attrs, new_attrs);
|
||||
|
||||
let children_patch = LNode::diff_children(
|
||||
path,
|
||||
old_children,
|
||||
new_children,
|
||||
orig_children,
|
||||
);
|
||||
|
||||
attrs_patch
|
||||
.into_iter()
|
||||
// tag patch comes second so we remove old attrs before copying them over
|
||||
.chain(tag_patch)
|
||||
.chain(children_patch)
|
||||
.collect()
|
||||
}
|
||||
// components + dynamic context: no patches
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_attrs<'a>(
|
||||
path: &'a [usize],
|
||||
old: &'a [(String, LAttributeValue)],
|
||||
new: &'a [(String, LAttributeValue)],
|
||||
) -> impl Iterator<Item = Patch> + 'a {
|
||||
let additions = new
|
||||
.iter()
|
||||
.filter_map(|(name, new_value)| {
|
||||
let old_attr = old.iter().find(|(o_name, _)| o_name == name);
|
||||
let replace = match old_attr {
|
||||
None => true,
|
||||
Some((_, old_value)) if old_value != new_value => true,
|
||||
_ => false,
|
||||
};
|
||||
if replace {
|
||||
match &new_value {
|
||||
LAttributeValue::Boolean => {
|
||||
Some((name.to_owned(), "".to_string()))
|
||||
}
|
||||
LAttributeValue::Static(s) => {
|
||||
Some((name.to_owned(), s.to_owned()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|(name, value)| Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::SetAttribute(name, value),
|
||||
});
|
||||
|
||||
let removals = old.iter().filter_map(|(name, _)| {
|
||||
if !new.iter().any(|(new_name, _)| new_name == name) {
|
||||
Some(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::RemoveAttribute(name.to_owned()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
additions.chain(removals)
|
||||
}
|
||||
|
||||
fn diff_children(
|
||||
path: &[usize],
|
||||
old: &[LNode],
|
||||
new: &[LNode],
|
||||
old_children: &OldChildren,
|
||||
) -> Vec<Patch> {
|
||||
if old.is_empty() && new.is_empty() {
|
||||
vec![]
|
||||
} else if old.is_empty() {
|
||||
vec![Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::AppendChildren(
|
||||
new.iter()
|
||||
.map(LNode::to_html)
|
||||
.map(ReplacementNode::Html)
|
||||
.collect(),
|
||||
),
|
||||
}]
|
||||
} else if new.is_empty() {
|
||||
vec![Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::ClearChildren,
|
||||
}]
|
||||
} else {
|
||||
let mut a = 0;
|
||||
let mut b = std::cmp::max(old.len(), new.len()) - 1; // min is 0, have checked both have items
|
||||
let mut patches = vec![];
|
||||
// common prefix
|
||||
while a < b {
|
||||
let old = old.get(a);
|
||||
let new = new.get(a);
|
||||
|
||||
match (old, new) {
|
||||
(None, None) => {}
|
||||
(None, Some(new)) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::InsertChild {
|
||||
before: a,
|
||||
child: new.to_replacement_node(old_children),
|
||||
},
|
||||
}),
|
||||
(Some(_), None) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::RemoveChild { at: a },
|
||||
}),
|
||||
(Some(old), Some(new)) => {
|
||||
if old != new {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a += 1;
|
||||
}
|
||||
|
||||
// common suffix
|
||||
while b >= a {
|
||||
let old = old.get(b);
|
||||
let new = new.get(b);
|
||||
|
||||
match (old, new) {
|
||||
(None, None) => {}
|
||||
(None, Some(new)) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::InsertChildAfter {
|
||||
after: b - 1,
|
||||
child: new.to_replacement_node(old_children),
|
||||
},
|
||||
}),
|
||||
(Some(_), None) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::RemoveChild { at: b },
|
||||
}),
|
||||
(Some(old), Some(new)) => {
|
||||
if old != new {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b == 0 {
|
||||
break;
|
||||
} else {
|
||||
b -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// diffing in middle
|
||||
if b >= a {
|
||||
let old_slice_end =
|
||||
if b >= old.len() { old.len() - 1 } else { b };
|
||||
let new_slice_end =
|
||||
if b >= new.len() { new.len() - 1 } else { b };
|
||||
let old = &old[a..=old_slice_end];
|
||||
let new = &new[a..=new_slice_end];
|
||||
|
||||
for (new_idx, new_node) in new.iter().enumerate() {
|
||||
match old.get(new_idx) {
|
||||
Some(old_node) => {
|
||||
let mut new_path = path.to_vec();
|
||||
new_path.push(new_idx + a);
|
||||
let diffs = old_node.diff_at(
|
||||
new_node,
|
||||
&new_path,
|
||||
old_children,
|
||||
);
|
||||
patches.extend(&mut diffs.into_iter());
|
||||
}
|
||||
None => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::InsertChild {
|
||||
before: new_idx,
|
||||
child: new_node
|
||||
.to_replacement_node(old_children),
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patches
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Patches(pub Vec<(String, Vec<Patch>)>);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Patch {
|
||||
path: Vec<usize>,
|
||||
action: PatchAction,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PatchAction {
|
||||
ReplaceWith(ReplacementNode),
|
||||
ChangeTagName(String),
|
||||
RemoveAttribute(String),
|
||||
SetAttribute(String, String),
|
||||
SetText(String),
|
||||
ClearChildren,
|
||||
AppendChildren(Vec<ReplacementNode>),
|
||||
RemoveChild {
|
||||
at: usize,
|
||||
},
|
||||
InsertChild {
|
||||
before: usize,
|
||||
child: ReplacementNode,
|
||||
},
|
||||
InsertChildAfter {
|
||||
after: usize,
|
||||
child: ReplacementNode,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ReplacementNode {
|
||||
Html(String),
|
||||
Path(Vec<usize>),
|
||||
Fragment(Vec<ReplacementNode>),
|
||||
Element {
|
||||
name: String,
|
||||
attrs: Vec<(String, String)>,
|
||||
children: Vec<ReplacementNode>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
diff::{Patch, PatchAction, ReplacementNode},
|
||||
node::LAttributeValue,
|
||||
LNode,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn patches_text() {
|
||||
let a = LNode::Text("foo".into());
|
||||
let b = LNode::Text("bar".into());
|
||||
let delta = a.diff(&b);
|
||||
assert_eq!(
|
||||
delta,
|
||||
vec![Patch {
|
||||
path: vec![],
|
||||
action: PatchAction::SetText("bar".into())
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patches_attrs() {
|
||||
let a = LNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![
|
||||
("class".into(), LAttributeValue::Static("a".into())),
|
||||
("type".into(), LAttributeValue::Static("button".into())),
|
||||
],
|
||||
children: vec![],
|
||||
};
|
||||
let b = LNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![
|
||||
("class".into(), LAttributeValue::Static("a b".into())),
|
||||
("id".into(), LAttributeValue::Static("button".into())),
|
||||
],
|
||||
children: vec![],
|
||||
};
|
||||
let delta = a.diff(&b);
|
||||
assert_eq!(
|
||||
delta,
|
||||
vec![
|
||||
Patch {
|
||||
path: vec![],
|
||||
action: PatchAction::SetAttribute(
|
||||
"class".into(),
|
||||
"a b".into()
|
||||
)
|
||||
},
|
||||
Patch {
|
||||
path: vec![],
|
||||
action: PatchAction::SetAttribute(
|
||||
"id".into(),
|
||||
"button".into()
|
||||
)
|
||||
},
|
||||
Patch {
|
||||
path: vec![],
|
||||
action: PatchAction::RemoveAttribute("type".into())
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patches_child_text() {
|
||||
let a = LNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![
|
||||
LNode::Text("foo".into()),
|
||||
LNode::Text("bar".into()),
|
||||
],
|
||||
};
|
||||
let b = LNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![
|
||||
LNode::Text("foo".into()),
|
||||
LNode::Text("baz".into()),
|
||||
],
|
||||
};
|
||||
let delta = a.diff(&b);
|
||||
assert_eq!(
|
||||
delta,
|
||||
vec![Patch {
|
||||
path: vec![1],
|
||||
action: PatchAction::SetText("baz".into())
|
||||
},]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inserts_child() {
|
||||
let a = LNode::Element {
|
||||
name: "div".into(),
|
||||
attrs: vec![],
|
||||
children: vec![LNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![LNode::Text("bar".into())],
|
||||
}],
|
||||
};
|
||||
let b = LNode::Element {
|
||||
name: "div".into(),
|
||||
attrs: vec![],
|
||||
children: vec![
|
||||
LNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![LNode::Text("foo".into())],
|
||||
},
|
||||
LNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![LNode::Text("bar".into())],
|
||||
},
|
||||
],
|
||||
};
|
||||
let delta = a.diff(&b);
|
||||
assert_eq!(
|
||||
delta,
|
||||
vec![
|
||||
Patch {
|
||||
path: vec![],
|
||||
action: PatchAction::InsertChildAfter {
|
||||
after: 0,
|
||||
child: ReplacementNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![ReplacementNode::Html("bar".into())]
|
||||
}
|
||||
}
|
||||
},
|
||||
Patch {
|
||||
path: vec![0, 0],
|
||||
action: PatchAction::SetText("foo".into())
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removes_child() {
|
||||
let a = LNode::Element {
|
||||
name: "div".into(),
|
||||
attrs: vec![],
|
||||
children: vec![
|
||||
LNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![LNode::Text("foo".into())],
|
||||
},
|
||||
LNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![LNode::Text("bar".into())],
|
||||
},
|
||||
],
|
||||
};
|
||||
let b = LNode::Element {
|
||||
name: "div".into(),
|
||||
attrs: vec![],
|
||||
children: vec![LNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![LNode::Text("foo".into())],
|
||||
}],
|
||||
};
|
||||
let delta = a.diff(&b);
|
||||
assert_eq!(
|
||||
delta,
|
||||
vec![Patch {
|
||||
path: vec![],
|
||||
action: PatchAction::RemoveChild { at: 1 }
|
||||
},]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
extern crate proc_macro;
|
||||
|
||||
use anyhow::Result;
|
||||
use camino::Utf8PathBuf;
|
||||
use diff::Patches;
|
||||
use node::LNode;
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use syn::{
|
||||
spanned::Spanned,
|
||||
visit::{self, Visit},
|
||||
Macro,
|
||||
};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub mod diff;
|
||||
pub mod node;
|
||||
pub mod parsing;
|
||||
|
||||
pub const HOT_RELOAD_JS: &str = include_str!("patch.js");
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ViewMacros {
|
||||
// keyed by original location identifier
|
||||
views: Arc<RwLock<HashMap<Utf8PathBuf, Vec<MacroInvocation>>>>,
|
||||
}
|
||||
|
||||
impl ViewMacros {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn update_from_paths<T: AsRef<Path>>(&self, paths: &[T]) -> Result<()> {
|
||||
let mut views = HashMap::new();
|
||||
|
||||
for path in paths {
|
||||
for entry in WalkDir::new(path).into_iter().flatten() {
|
||||
if entry.file_type().is_file() {
|
||||
let path: PathBuf = entry.path().into();
|
||||
let path = Utf8PathBuf::try_from(path)?;
|
||||
if path.extension() == Some("rs") || path.ends_with(".rs") {
|
||||
let macros = Self::parse_file(&path)?;
|
||||
let entry = views.entry(path.clone()).or_default();
|
||||
*entry = macros;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*self.views.write() = views;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_file(path: &Utf8PathBuf) -> Result<Vec<MacroInvocation>> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content)?;
|
||||
let ast = syn::parse_file(&content)?;
|
||||
|
||||
let mut visitor = ViewMacroVisitor::default();
|
||||
visitor.visit_file(&ast);
|
||||
let mut views = Vec::new();
|
||||
for view in visitor.views {
|
||||
let span = view.span();
|
||||
let id = span_to_stable_id(path, span);
|
||||
let mut tokens = view.tokens.clone().into_iter();
|
||||
tokens.next(); // cx
|
||||
tokens.next(); // ,
|
||||
// TODO handle class = ...
|
||||
let rsx =
|
||||
syn_rsx::parse2(tokens.collect::<proc_macro2::TokenStream>())?;
|
||||
let template = LNode::parse_view(rsx)?;
|
||||
views.push(MacroInvocation { id, template })
|
||||
}
|
||||
Ok(views)
|
||||
}
|
||||
|
||||
pub fn patch(&self, path: &Utf8PathBuf) -> Result<Option<Patches>> {
|
||||
let new_views = Self::parse_file(path)?;
|
||||
let mut lock = self.views.write();
|
||||
let diffs = match lock.get(path) {
|
||||
None => return Ok(None),
|
||||
Some(current_views) => {
|
||||
if current_views.len() == new_views.len() {
|
||||
let mut diffs = Vec::new();
|
||||
for (current_view, new_view) in
|
||||
current_views.iter().zip(&new_views)
|
||||
{
|
||||
if current_view.id == new_view.id
|
||||
&& current_view.template != new_view.template
|
||||
{
|
||||
diffs.push((
|
||||
current_view.id.clone(),
|
||||
current_view.template.diff(&new_view.template),
|
||||
));
|
||||
}
|
||||
}
|
||||
diffs
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// update the status to the new views
|
||||
lock.insert(path.clone(), new_views);
|
||||
|
||||
Ok(Some(Patches(diffs)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct MacroInvocation {
|
||||
id: String,
|
||||
template: LNode,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MacroInvocation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("MacroInvocation")
|
||||
.field("id", &self.id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ViewMacroVisitor<'a> {
|
||||
views: Vec<&'a Macro>,
|
||||
}
|
||||
|
||||
impl<'ast> Visit<'ast> for ViewMacroVisitor<'ast> {
|
||||
fn visit_macro(&mut self, node: &'ast Macro) {
|
||||
let ident = node.path.get_ident().map(|n| n.to_string());
|
||||
if ident == Some("view".to_string()) {
|
||||
self.views.push(node);
|
||||
}
|
||||
|
||||
// Delegate to the default impl to visit any nested functions.
|
||||
visit::visit_macro(self, node);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span_to_stable_id(
|
||||
path: impl AsRef<Path>,
|
||||
site: proc_macro2::Span,
|
||||
) -> String {
|
||||
let file = path
|
||||
.as_ref()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.replace(['/', '\\'], "-");
|
||||
let start = site.start();
|
||||
format!("{}-{:?}", file, start.line)
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
use crate::parsing::{is_component_node, value_to_string};
|
||||
use anyhow::Result;
|
||||
use quote::quote;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use syn_rsx::Node;
|
||||
|
||||
// A lightweight virtual DOM structure we can use to hold
|
||||
// the state of a Leptos view macro template. This is because
|
||||
// `syn` types are `!Send` so we can't store them as we might like.
|
||||
// This is only used to diff view macros for hot reloading so it's very minimal
|
||||
// and ignores many of the data types.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum LNode {
|
||||
Fragment(Vec<LNode>),
|
||||
Text(String),
|
||||
Element {
|
||||
name: String,
|
||||
attrs: Vec<(String, LAttributeValue)>,
|
||||
children: Vec<LNode>,
|
||||
},
|
||||
// don't need anything; skipped during patching because it should
|
||||
// contain its own view macros
|
||||
Component(String, Vec<(String, String)>),
|
||||
DynChild(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum LAttributeValue {
|
||||
Boolean,
|
||||
Static(String),
|
||||
// safely ignored
|
||||
Dynamic,
|
||||
Noop,
|
||||
}
|
||||
|
||||
impl LNode {
|
||||
pub fn parse_view(nodes: Vec<Node>) -> Result<LNode> {
|
||||
let mut out = Vec::new();
|
||||
for node in nodes {
|
||||
LNode::parse_node(node, &mut out)?;
|
||||
}
|
||||
if out.len() == 1 {
|
||||
Ok(out.pop().unwrap())
|
||||
} else {
|
||||
Ok(LNode::Fragment(out))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_node(node: Node, views: &mut Vec<LNode>) -> Result<()> {
|
||||
match node {
|
||||
Node::Fragment(frag) => {
|
||||
for child in frag.children {
|
||||
LNode::parse_node(child, views)?;
|
||||
}
|
||||
}
|
||||
Node::Text(text) => {
|
||||
if let Some(value) = value_to_string(&text.value) {
|
||||
views.push(LNode::Text(value));
|
||||
} else {
|
||||
let value = text.value.as_ref();
|
||||
let code = quote! { #value };
|
||||
let code = code.to_string();
|
||||
views.push(LNode::DynChild(code));
|
||||
}
|
||||
}
|
||||
Node::Block(block) => {
|
||||
let value = block.value.as_ref();
|
||||
let code = quote! { #value };
|
||||
let code = code.to_string();
|
||||
views.push(LNode::DynChild(code));
|
||||
}
|
||||
Node::Element(el) => {
|
||||
if is_component_node(&el) {
|
||||
views.push(LNode::Component(
|
||||
el.name.to_string(),
|
||||
el.attributes
|
||||
.into_iter()
|
||||
.filter_map(|attr| match attr {
|
||||
Node::Attribute(attr) => Some((
|
||||
attr.key.to_string(),
|
||||
format!("{:#?}", attr.value),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
));
|
||||
} else {
|
||||
let name = el.name.to_string();
|
||||
let mut attrs = Vec::new();
|
||||
|
||||
for attr in el.attributes {
|
||||
if let Node::Attribute(attr) = attr {
|
||||
let name = attr.key.to_string();
|
||||
if let Some(value) =
|
||||
attr.value.as_ref().and_then(value_to_string)
|
||||
{
|
||||
attrs.push((
|
||||
name,
|
||||
LAttributeValue::Static(value),
|
||||
));
|
||||
} else {
|
||||
attrs.push((name, LAttributeValue::Dynamic));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut children = Vec::new();
|
||||
for child in el.children {
|
||||
LNode::parse_node(child, &mut children)?;
|
||||
}
|
||||
|
||||
views.push(LNode::Element {
|
||||
name,
|
||||
attrs,
|
||||
children,
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_html(&self) -> String {
|
||||
match self {
|
||||
LNode::Fragment(frag) => frag.iter().map(LNode::to_html).collect(),
|
||||
LNode::Text(text) => text.to_owned(),
|
||||
LNode::Component(name, _) => format!(
|
||||
"<!--<{name}>--><pre><{name}/> will load once Rust code \
|
||||
has been compiled.</pre><!--</{name}>-->"
|
||||
),
|
||||
LNode::DynChild(_) => "<!--<DynChild>--><pre>Dynamic content will \
|
||||
load once Rust code has been \
|
||||
compiled.</pre><!--</DynChild>-->"
|
||||
.to_string(),
|
||||
LNode::Element {
|
||||
name,
|
||||
attrs,
|
||||
children,
|
||||
} => {
|
||||
// this is naughty, but the browsers are tough and can handle it
|
||||
// I wouldn't do this for real code, but this is just for dev mode
|
||||
let is_self_closing = children.is_empty();
|
||||
|
||||
let attrs = attrs
|
||||
.iter()
|
||||
.filter_map(|(name, value)| match value {
|
||||
LAttributeValue::Boolean => Some(format!("{name} ")),
|
||||
LAttributeValue::Static(value) => {
|
||||
Some(format!("{name}=\"{value}\" "))
|
||||
}
|
||||
LAttributeValue::Dynamic => None,
|
||||
LAttributeValue::Noop => None,
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
let children =
|
||||
children.iter().map(LNode::to_html).collect::<String>();
|
||||
|
||||
if is_self_closing {
|
||||
format!("<{name} {attrs}/>")
|
||||
} else {
|
||||
format!("<{name} {attrs}>{children}</{name}>")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use syn_rsx::{NodeElement, NodeValueExpr};
|
||||
|
||||
pub fn value_to_string(value: &NodeValueExpr) -> Option<String> {
|
||||
match &value.as_ref() {
|
||||
syn::Expr::Lit(lit) => match &lit.lit {
|
||||
syn::Lit::Str(s) => Some(s.value()),
|
||||
syn::Lit::Char(c) => Some(c.value().to_string()),
|
||||
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
|
||||
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_component_node(node: &NodeElement) -> bool {
|
||||
node.name
|
||||
.to_string()
|
||||
.starts_with(|c: char| c.is_ascii_uppercase())
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
console.log("[HOT RELOADING] Connected to server.");
|
||||
function patch(json) {
|
||||
try {
|
||||
const views = JSON.parse(json);
|
||||
for (const [id, patches] of views) {
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
|
||||
open = `leptos-view|${id}|open`,
|
||||
close = `leptos-view|${id}|close`;
|
||||
let start, end;
|
||||
const instances = [];
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent == open) {
|
||||
start = walker.currentNode;
|
||||
} else if (walker.currentNode.textContent == close) {
|
||||
end = walker.currentNode;
|
||||
instances.push([start, end]);
|
||||
start = undefined;
|
||||
end = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
for(const [start, end] of instances) {
|
||||
// build tree of current actual children
|
||||
const actualChildren = childrenFromRange(start.parentElement, start, end);
|
||||
const actions = [];
|
||||
|
||||
// build up the set of actions
|
||||
for (const patch of patches) {
|
||||
const child = childAtPath(
|
||||
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
|
||||
patch.path
|
||||
);
|
||||
const action = patch.action;
|
||||
if (action == "ClearChildren") {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > ClearChildren", child.node);
|
||||
child.node.textContent = ""
|
||||
});
|
||||
} else if (action.ReplaceWith) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > ReplaceWith", child, action.ReplaceWith);
|
||||
const replacement = fromReplacementNode(action.ReplaceWith, actualChildren);
|
||||
if (child.node) {
|
||||
child.node.replaceWith(replacement)
|
||||
} else {
|
||||
const range = new Range();
|
||||
range.setStartAfter(child.start);
|
||||
range.setEndAfter(child.end);
|
||||
range.deleteContents();
|
||||
child.start.replaceWith(replacement);
|
||||
}
|
||||
});
|
||||
} else if (action.ChangeTagName) {
|
||||
const oldNode = child.node;
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > ChangeTagName", child.node, action.ChangeTagName);
|
||||
const newElement = document.createElement(action.ChangeTagName);
|
||||
for (const attr of oldNode.attributes) {
|
||||
newElement.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
for (const childNode of child.node.childNodes) {
|
||||
newElement.appendChild(childNode);
|
||||
}
|
||||
|
||||
child.node.replaceWith(newElement)
|
||||
});
|
||||
} else if (action.RemoveAttribute) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > RemoveAttribute", child.node, action.RemoveAttribute);
|
||||
child.node.removeAttribute(action.RemoveAttribute);
|
||||
});
|
||||
} else if (action.SetAttribute) {
|
||||
const [name, value] = action.SetAttribute;
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > SetAttribute", child.node, action.SetAttribute);
|
||||
child.node.setAttribute(name, value);
|
||||
});
|
||||
} else if (action.SetText) {
|
||||
const node = child.node;
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > SetText", child.node, action.SetText);
|
||||
node.textContent = action.SetText
|
||||
});
|
||||
} else if (action.AppendChildren) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > AppendChildren", child.node, action.AppendChildren);
|
||||
const newChildren = fromReplacementNode(action.AppendChildren, actualChildren);
|
||||
child.node.append(newChildren);
|
||||
});
|
||||
} else if (action.RemoveChild) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > RemoveChild", child.node, child.children, action.RemoveChild);
|
||||
const toRemove = child.children[action.RemoveChild.at];
|
||||
let toRemoveNode = toRemove.node;
|
||||
if (!toRemoveNode) {
|
||||
const range = new Range();
|
||||
range.setStartBefore(toRemove.start);
|
||||
range.setEndAfter(toRemove.end);
|
||||
toRemoveNode = range.deleteContents();
|
||||
} else {
|
||||
toRemoveNode.parentNode.removeChild(toRemoveNode);
|
||||
}
|
||||
})
|
||||
} else if (action.InsertChild) {
|
||||
const newChild = fromReplacementNode(action.InsertChild.child, actualChildren);
|
||||
let children = [];
|
||||
if(child.children) {
|
||||
children = child.children;
|
||||
} else if (child.start && child.end) {
|
||||
children = childrenFromRange(child.node || child.start.parentElement, start, end);
|
||||
} else {
|
||||
console.warn("InsertChildAfter could not build children.");
|
||||
}
|
||||
const before = children[action.InsertChild.before];
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > InsertChild", child, child.node, action.InsertChild, " before ", before);
|
||||
if (!before && child.node) {
|
||||
child.node.appendChild(newChild);
|
||||
} else {
|
||||
let node = child.node || child.end.parentElement;
|
||||
const reference = before ? before.node || before.start : child.end;
|
||||
node.insertBefore(newChild, reference);
|
||||
}
|
||||
})
|
||||
} else if (action.InsertChildAfter) {
|
||||
const newChild = fromReplacementNode(action.InsertChildAfter.child, actualChildren);
|
||||
let children = [];
|
||||
if(child.children) {
|
||||
children = child.children;
|
||||
} else if (child.start && child.end) {
|
||||
children = childrenFromRange(child.node || child.start.parentElement, start, end);
|
||||
} else {
|
||||
console.warn("InsertChildAfter could not build children.");
|
||||
}
|
||||
const after = children[action.InsertChildAfter.after];
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > InsertChildAfter", child, child.node, action.InsertChildAfter, " after ", after);
|
||||
if (child.node && (!after || !(after.node || after.start).nextSibling)) {
|
||||
child.node.appendChild(newChild);
|
||||
} else {
|
||||
const node = child.node || child.end;
|
||||
const parent = node.nodeType === Node.COMMENT_NODE ? node.parentNode : node;
|
||||
if(!after) {
|
||||
parent.appendChild(newChild);
|
||||
} else {
|
||||
parent.insertBefore(newChild, (after.node || after.start).nextSibling);
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Unmatched action", action);
|
||||
}
|
||||
}
|
||||
|
||||
// actually run the actions
|
||||
// the reason we delay them is so that children aren't moved before other children are found, etc.
|
||||
for (const action of actions) {
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[HOT RELOADING] Error: ", e);
|
||||
}
|
||||
|
||||
function fromReplacementNode(node, actualChildren) {
|
||||
if (node.Html) {
|
||||
return fromHTML(node.Html);
|
||||
}
|
||||
else if (node.Fragment) {
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const child of node.Fragment) {
|
||||
frag.appendChild(fromReplacementNode(child, actualChildren));
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
else if (node.Element) {
|
||||
const element = document.createElement(node.Element.name);
|
||||
for (const [name, value] of node.Element.attrs) {
|
||||
element.setAttribute(name, value);
|
||||
}
|
||||
for (const child of node.Element.children) {
|
||||
element.appendChild(fromReplacementNode(child, actualChildren));
|
||||
}
|
||||
return element;
|
||||
}
|
||||
else {
|
||||
const child = childAtPath(
|
||||
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
|
||||
node.Path
|
||||
);
|
||||
if (child) {
|
||||
let childNode = child.node;
|
||||
if (!childNode) {
|
||||
const range = new Range();
|
||||
range.setStartBefore(child.start);
|
||||
range.setEndAfter(child.end);
|
||||
// okay this is somewhat silly
|
||||
// if we do cloneContents() here to return it,
|
||||
// we strip away the event listeners
|
||||
// if we're moving just one object, this is less than ideal
|
||||
// so I'm actually going to *extract* them, then clone and reinsert
|
||||
/* const toReinsert = range.cloneContents();
|
||||
if (child.end.nextSibling) {
|
||||
child.end.parentNode.insertBefore(toReinsert, child.end.nextSibling);
|
||||
} else {
|
||||
child.end.parentNode.appendChild(toReinsert);
|
||||
} */
|
||||
childNode = range.cloneContents();
|
||||
}
|
||||
return childNode;
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Could not find replacement node at ", node.Path);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildActualChildren(element, range) {
|
||||
const walker = document.createTreeWalker(
|
||||
element,
|
||||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT,
|
||||
{
|
||||
acceptNode(node) {
|
||||
return node.parentNode == element && (!range || range.isPointInRange(node, 0));
|
||||
}
|
||||
}
|
||||
);
|
||||
const actualChildren = [],
|
||||
elementCount = {};
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.nodeType == Node.ELEMENT_NODE) {
|
||||
if (elementCount[walker.currentNode.nodeName]) {
|
||||
elementCount[walker.currentNode.nodeName] += 1;
|
||||
} else {
|
||||
elementCount[walker.currentNode.nodeName] = 0;
|
||||
}
|
||||
elementCount[walker.currentNode.nodeName];
|
||||
|
||||
actualChildren.push({
|
||||
type: "element",
|
||||
name: walker.currentNode.nodeName,
|
||||
number: elementCount[walker.currentNode.nodeName],
|
||||
node: walker.currentNode,
|
||||
children: buildActualChildren(walker.currentNode)
|
||||
});
|
||||
} else if (walker.currentNode.nodeType == Node.TEXT_NODE) {
|
||||
actualChildren.push({
|
||||
type: "text",
|
||||
node: walker.currentNode
|
||||
});
|
||||
} else if (walker.currentNode.nodeType == Node.COMMENT_NODE) {
|
||||
if (walker.currentNode.textContent.trim().startsWith("leptos-view")) {
|
||||
} else if (walker.currentNode.textContent.trim() == "<() />") {
|
||||
actualChildren.push({
|
||||
type: "unit",
|
||||
node: walker.currentNode
|
||||
});
|
||||
} else if (walker.currentNode.textContent.trim() == "<DynChild>") {
|
||||
let start = walker.currentNode;
|
||||
let depth = 1;
|
||||
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.trim() == "</DynChild>") {
|
||||
depth--;
|
||||
} else if (walker.currentNode.textContent.trim() == "<DynChild>") {
|
||||
depth++;
|
||||
}
|
||||
|
||||
if(depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let end = walker.currentNode;
|
||||
actualChildren.push({
|
||||
type: "dyn-child",
|
||||
start, end
|
||||
});
|
||||
} else if (walker.currentNode.textContent.trim() == "<>") {
|
||||
let start = walker.currentNode;
|
||||
let depth = 1;
|
||||
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.trim() == "</>") {
|
||||
depth--;
|
||||
} else if (walker.currentNode.textContent.trim() == "<>") {
|
||||
depth++;
|
||||
}
|
||||
|
||||
if(depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let end = walker.currentNode;
|
||||
actualChildren.push({
|
||||
type: "fragment",
|
||||
children: childrenFromRange(start.parentElement, start, end),
|
||||
start, end
|
||||
});
|
||||
} else if (walker.currentNode.textContent.trim().startsWith("<")) {
|
||||
let componentName = walker.currentNode.textContent.trim();
|
||||
let endMarker = componentName.replace("<", "</");
|
||||
let depth = 1;
|
||||
let start = walker.currentNode;
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.trim() == endMarker) {
|
||||
depth--;
|
||||
} else if (walker.currentNode.textContent.trim() == componentName) {
|
||||
depth++;
|
||||
}
|
||||
|
||||
if(depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let end = walker.currentNode;
|
||||
actualChildren.push({
|
||||
type: "component",
|
||||
start, end
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Building children, encountered", walker.currentNode);
|
||||
}
|
||||
}
|
||||
return actualChildren;
|
||||
}
|
||||
|
||||
function childAtPath(element, path) {
|
||||
if (path.length == 0) {
|
||||
return element;
|
||||
} else if (element.children) {
|
||||
const next = element.children[path[0]],
|
||||
rest = path.slice(1);
|
||||
return childAtPath(next, rest);
|
||||
} else if (path == [0]) {
|
||||
return element;
|
||||
} else if (element.start && element.end) {
|
||||
const actualChildren = childrenFromRange(element.node || element.start.parentElement, element.start, element.end);
|
||||
return childAtPath({ children: actualChildren }, path);
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Child at ", path, "not found in ", element);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
function childrenFromRange(parent, start, end) {
|
||||
const range = new Range();
|
||||
range.setStartAfter(start);
|
||||
range.setEndBefore(end);
|
||||
return buildActualChildren(parent, range);
|
||||
}
|
||||
|
||||
function fromHTML(html) {
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = html;
|
||||
return template.content.cloneNode(true);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ readme = "../README.md"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
attribute-derive = { version = "0.5", features = ["syn-full"] }
|
||||
cfg-if = "1"
|
||||
html-escape = "0.2"
|
||||
itertools = "0.10"
|
||||
@@ -23,16 +22,14 @@ quote = "1"
|
||||
syn = { version = "1", features = ["full"] }
|
||||
syn-rsx = "0.9"
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_hot_reload = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4"
|
||||
typed-builder = "0.14"
|
||||
trybuild = "1"
|
||||
typed-builder = "0.12"
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use attribute_derive::Attribute as AttributeDerive;
|
||||
use convert_case::{
|
||||
Case::{Pascal, Snake},
|
||||
Casing,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use proc_macro_error::ResultExt;
|
||||
use quote::{format_ident, ToTokens, TokenStreamExt};
|
||||
use std::collections::HashSet;
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute,
|
||||
FnArg, GenericArgument, ItemFn, LitStr, Meta, MetaNameValue, Pat, PatIdent,
|
||||
Path, PathArguments, ReturnType, Type, TypePath, Visibility,
|
||||
FnArg, GenericArgument, ItemFn, LitStr, Meta, MetaList, MetaNameValue,
|
||||
NestedMeta, Pat, PatIdent, Path, PathArguments, ReturnType, Type, TypePath,
|
||||
Visibility,
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
@@ -236,7 +238,7 @@ impl Model {
|
||||
|
||||
struct Prop {
|
||||
docs: Docs,
|
||||
prop_opts: PropOpt,
|
||||
prop_opts: HashSet<PropOpt>,
|
||||
name: PatIdent,
|
||||
ty: Type,
|
||||
}
|
||||
@@ -249,12 +251,53 @@ impl Prop {
|
||||
abort!(arg, "receiver not allowed in `fn`");
|
||||
};
|
||||
|
||||
let prop_opts =
|
||||
PropOpt::from_attributes(&typed.attrs).unwrap_or_else(|e| {
|
||||
// TODO: replace with `.unwrap_or_abort()` once https://gitlab.com/CreepySkeleton/proc-macro-error/-/issues/17 is fixed
|
||||
abort!(e.span(), e.to_string());
|
||||
let prop_opts = typed
|
||||
.attrs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, attr)| {
|
||||
PropOpt::from_attribute(attr).map(|opt| (i, opt))
|
||||
})
|
||||
.fold(HashSet::new(), |mut acc, cur| {
|
||||
// Make sure opts aren't repeated
|
||||
if acc.intersection(&cur.1).next().is_some() {
|
||||
abort!(
|
||||
typed.attrs[cur.0],
|
||||
"`#[prop]` options are repeated"
|
||||
);
|
||||
}
|
||||
|
||||
acc.extend(cur.1);
|
||||
|
||||
acc
|
||||
});
|
||||
|
||||
// Make sure conflicting options are not present
|
||||
if prop_opts.contains(&PropOpt::Optional)
|
||||
&& prop_opts.contains(&PropOpt::OptionalNoStrip)
|
||||
{
|
||||
abort!(
|
||||
typed,
|
||||
"`optional` and `optional_no_strip` options are mutually \
|
||||
exclusive"
|
||||
);
|
||||
} else if prop_opts.contains(&PropOpt::Optional)
|
||||
&& prop_opts.contains(&PropOpt::StripOption)
|
||||
{
|
||||
abort!(
|
||||
typed,
|
||||
"`optional` and `strip_option` options are mutually exclusive"
|
||||
);
|
||||
} else if prop_opts.contains(&PropOpt::OptionalNoStrip)
|
||||
&& prop_opts.contains(&PropOpt::StripOption)
|
||||
{
|
||||
abort!(
|
||||
typed,
|
||||
"`optional_no_strip` and `strip_option` options are mutually \
|
||||
exclusive"
|
||||
);
|
||||
}
|
||||
|
||||
let name = if let Pat::Ident(i) = *typed.pat {
|
||||
i
|
||||
} else {
|
||||
@@ -364,34 +407,98 @@ impl Docs {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, AttributeDerive)]
|
||||
#[attribute(ident = prop)]
|
||||
struct PropOpt {
|
||||
#[attribute(conflicts = [optional_no_strip, strip_option])]
|
||||
optional: bool,
|
||||
#[attribute(conflicts = [optional, strip_option])]
|
||||
optional_no_strip: bool,
|
||||
#[attribute(conflicts = [optional, optional_no_strip])]
|
||||
strip_option: bool,
|
||||
#[attribute(example = "5 * 10")]
|
||||
default: Option<syn::Expr>,
|
||||
into: bool,
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
||||
enum PropOpt {
|
||||
Optional,
|
||||
OptionalNoStrip,
|
||||
OptionalWithDefault(syn::Lit),
|
||||
StripOption,
|
||||
Into,
|
||||
}
|
||||
|
||||
impl PropOpt {
|
||||
fn from_attribute(attr: &Attribute) -> Option<HashSet<Self>> {
|
||||
const ABORT_OPT_MESSAGE: &str =
|
||||
"only `optional`, `optional_no_strip`, `strip_option`, `default` \
|
||||
and `into` are allowed as arguments to `#[prop()]`";
|
||||
|
||||
if attr.path != parse_quote!(prop) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Meta::List(MetaList { nested, .. }) =
|
||||
attr.parse_meta().unwrap_or_abort()
|
||||
{
|
||||
Some(
|
||||
nested
|
||||
.iter()
|
||||
.map(|opt| match opt {
|
||||
NestedMeta::Meta(Meta::Path(opt)) => {
|
||||
if *opt == parse_quote!(optional) {
|
||||
PropOpt::Optional
|
||||
} else if *opt == parse_quote!(optional_no_strip) {
|
||||
PropOpt::OptionalNoStrip
|
||||
} else if *opt == parse_quote!(strip_option) {
|
||||
PropOpt::StripOption
|
||||
} else if *opt == parse_quote!(into) {
|
||||
PropOpt::Into
|
||||
} else {
|
||||
abort!(
|
||||
opt,
|
||||
"invalid prop option";
|
||||
help = ABORT_OPT_MESSAGE
|
||||
);
|
||||
}
|
||||
}
|
||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||
path,
|
||||
eq_token: _,
|
||||
lit,
|
||||
})) => {
|
||||
if *path == parse_quote!(default) {
|
||||
PropOpt::OptionalWithDefault(lit.to_owned())
|
||||
} else {
|
||||
abort!(
|
||||
opt,
|
||||
"invalid prop option";
|
||||
help = ABORT_OPT_MESSAGE
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => abort!(opt, ABORT_OPT_MESSAGE,),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
abort!(
|
||||
attr,
|
||||
"the syntax for `#[prop]` is incorrect";
|
||||
help = "try `#[prop(optional)]`";
|
||||
help = ABORT_OPT_MESSAGE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TypedBuilderOpts {
|
||||
default: bool,
|
||||
default_with_value: Option<syn::Expr>,
|
||||
default_with_value: Option<syn::Lit>,
|
||||
strip_option: bool,
|
||||
into: bool,
|
||||
}
|
||||
|
||||
impl TypedBuilderOpts {
|
||||
fn from_opts(opts: &PropOpt, is_ty_option: bool) -> Self {
|
||||
fn from_opts(opts: &HashSet<PropOpt>, is_ty_option: bool) -> Self {
|
||||
Self {
|
||||
default: opts.optional || opts.optional_no_strip,
|
||||
default_with_value: opts.default.clone(),
|
||||
strip_option: opts.strip_option || opts.optional && is_ty_option,
|
||||
into: opts.into,
|
||||
default: opts.contains(&PropOpt::Optional)
|
||||
|| opts.contains(&PropOpt::OptionalNoStrip),
|
||||
default_with_value: opts.iter().find_map(|p| match p {
|
||||
PropOpt::OptionalWithDefault(v) => Some(v.to_owned()),
|
||||
_ => None,
|
||||
}),
|
||||
strip_option: opts.contains(&PropOpt::StripOption)
|
||||
|| (opts.contains(&PropOpt::Optional) && is_ty_option),
|
||||
into: opts.contains(&PropOpt::Into),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,8 +506,7 @@ impl TypedBuilderOpts {
|
||||
impl ToTokens for TypedBuilderOpts {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let default = if let Some(v) = &self.default_with_value {
|
||||
let v = v.to_token_stream().to_string();
|
||||
quote! { default_code=#v, }
|
||||
quote! { default=#v, }
|
||||
} else if self.default {
|
||||
quote! { default, }
|
||||
} else {
|
||||
@@ -470,7 +576,8 @@ fn generate_component_fn_prop_docs(props: &[Prop]) -> TokenStream {
|
||||
let required_prop_docs = props
|
||||
.iter()
|
||||
.filter(|Prop { prop_opts, .. }| {
|
||||
!(prop_opts.optional || prop_opts.optional_no_strip)
|
||||
!(prop_opts.contains(&PropOpt::Optional)
|
||||
|| prop_opts.contains(&PropOpt::OptionalNoStrip))
|
||||
})
|
||||
.map(|p| prop_to_doc(p, PropDocStyle::List))
|
||||
.collect::<TokenStream>();
|
||||
@@ -478,7 +585,8 @@ fn generate_component_fn_prop_docs(props: &[Prop]) -> TokenStream {
|
||||
let optional_prop_docs = props
|
||||
.iter()
|
||||
.filter(|Prop { prop_opts, .. }| {
|
||||
prop_opts.optional || prop_opts.optional_no_strip
|
||||
prop_opts.contains(&PropOpt::Optional)
|
||||
|| prop_opts.contains(&PropOpt::OptionalNoStrip)
|
||||
})
|
||||
.map(|p| prop_to_doc(p, PropDocStyle::List))
|
||||
.collect::<TokenStream>();
|
||||
@@ -539,10 +647,10 @@ fn unwrap_option(ty: &Type) -> Type {
|
||||
AngleBracketedGenericArguments { args, .. },
|
||||
) = &first.arguments
|
||||
{
|
||||
if let [GenericArgument::Type(ty)] =
|
||||
&args.iter().collect::<Vec<_>>()[..]
|
||||
{
|
||||
return ty.clone();
|
||||
if let [first] = &args.iter().collect::<Vec<_>>()[..] {
|
||||
if let GenericArgument::Type(ty) = first {
|
||||
return ty.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -571,7 +679,9 @@ fn prop_to_doc(
|
||||
}: &Prop,
|
||||
style: PropDocStyle,
|
||||
) -> TokenStream {
|
||||
let ty = if (prop_opts.optional || prop_opts.strip_option) && is_option(ty)
|
||||
let ty = if (prop_opts.contains(&PropOpt::Optional)
|
||||
|| prop_opts.contains(&PropOpt::StripOption))
|
||||
&& is_option(ty)
|
||||
{
|
||||
unwrap_option(ty)
|
||||
} else {
|
||||
@@ -595,7 +705,7 @@ fn prop_to_doc(
|
||||
match style {
|
||||
PropDocStyle::List => {
|
||||
let arg_ty_doc = LitStr::new(
|
||||
&if !prop_opts.into {
|
||||
&if !prop_opts.contains(&PropOpt::Into) {
|
||||
format!("- **{}**: [`{}`]", quote!(#name), pretty_ty)
|
||||
} else {
|
||||
format!(
|
||||
@@ -616,7 +726,7 @@ fn prop_to_doc(
|
||||
}
|
||||
PropDocStyle::Inline => {
|
||||
let arg_ty_doc = LitStr::new(
|
||||
&if !prop_opts.into {
|
||||
&if !prop_opts.contains(&PropOpt::Into) {
|
||||
format!(
|
||||
"**{}**: [`{}`]{}",
|
||||
quote!(#name),
|
||||
|
||||
@@ -7,9 +7,9 @@ extern crate proc_macro_error;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenTree;
|
||||
use quote::ToTokens;
|
||||
use server_fn_macro::{server_macro_impl, ServerContext};
|
||||
use server::server_macro_impl;
|
||||
use syn::parse_macro_input;
|
||||
use syn_rsx::{parse, NodeAttribute};
|
||||
use syn_rsx::{parse, NodeAttribute, NodeElement};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Mode {
|
||||
@@ -35,6 +35,7 @@ mod view;
|
||||
use template::render_template;
|
||||
use view::render_view;
|
||||
mod component;
|
||||
mod server;
|
||||
mod template;
|
||||
|
||||
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
|
||||
@@ -284,7 +285,6 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
let tokens: proc_macro2::TokenStream = tokens.into();
|
||||
let mut tokens = tokens.into_iter();
|
||||
let (cx, comma) = (tokens.next(), tokens.next());
|
||||
|
||||
match (cx, comma) {
|
||||
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct)))
|
||||
if punct.as_char() == ',' =>
|
||||
@@ -327,9 +327,8 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
Ok(nodes) => render_view(
|
||||
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
|
||||
&nodes,
|
||||
Mode::default(),
|
||||
Mode::Client, //Mode::default(),
|
||||
global_class.as_ref(),
|
||||
normalized_call_site(proc_macro::Span::call_site()),
|
||||
),
|
||||
Err(error) => error.to_compile_error(),
|
||||
}
|
||||
@@ -344,20 +343,6 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(all(debug_assertions, not(feature = "stable")))] {
|
||||
Some(leptos_hot_reload::span_to_stable_id(
|
||||
site.source_file().path(),
|
||||
site.into()
|
||||
))
|
||||
} else {
|
||||
_ = site;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An optimized, cached template for client-side rendering. Follows the same
|
||||
/// syntax as the [view!] macro. In hydration or server-side rendering mode,
|
||||
/// behaves exactly as the `view` macro. In client-side rendering mode, uses a `<template>`
|
||||
@@ -693,16 +678,7 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
let context = ServerContext {
|
||||
ty: syn::parse_quote!(Scope),
|
||||
path: syn::parse_quote!(::leptos::Scope),
|
||||
};
|
||||
match server_macro_impl(
|
||||
args.into(),
|
||||
s.into(),
|
||||
Some(context),
|
||||
Some(syn::parse_quote!(::leptos::server_fn)),
|
||||
) {
|
||||
match server_macro_impl(args, s.into()) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
}
|
||||
@@ -720,6 +696,12 @@ pub fn params_derive(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_component_node(node: &NodeElement) -> bool {
|
||||
node.name
|
||||
.to_string()
|
||||
.starts_with(|c: char| c.is_ascii_uppercase())
|
||||
}
|
||||
|
||||
pub(crate) fn attribute_value(attr: &NodeAttribute) -> &syn::Expr {
|
||||
match &attr.value {
|
||||
Some(value) => value.as_ref(),
|
||||
|
||||
@@ -19,7 +19,7 @@ pub fn impl_params(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
|
||||
let span = field.span();
|
||||
|
||||
quote_spanned! {
|
||||
span => #ident: <#ty>::into_param(map.get(#field_name_string).map(|n| n.as_str()), #field_name_string)?
|
||||
span.into() => #ident: <#ty>::into_param(map.get(#field_name_string).map(|n| n.as_str()), #field_name_string)?
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
//! Implementation of the server_fn macro.
|
||||
//!
|
||||
//! This crate contains the implementation of the server_fn macro. [server_macro_impl] can be used to implement custom versions of the macro for different frameworks that allow users to pass a custom context from the server to the server function.
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_server::Encoding;
|
||||
use proc_macro2::{Literal, TokenStream as TokenStream2};
|
||||
use proc_macro_error::abort;
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
@@ -14,22 +8,13 @@ use syn::{
|
||||
*,
|
||||
};
|
||||
|
||||
/// Discribes the custom context from the server that passed to the server function. Optionally, the first argument of a server function
|
||||
/// can be a custom context of this type. This context can be used to access the server's state within the server function.
|
||||
pub struct ServerContext {
|
||||
/// The type of the context.
|
||||
pub ty: Ident,
|
||||
/// The path to the context type. Used to reference the context type in the generated code.
|
||||
pub path: Path,
|
||||
}
|
||||
|
||||
fn fn_arg_is_cx(f: &syn::FnArg, server_context: &ServerContext) -> bool {
|
||||
fn fn_arg_is_cx(f: &syn::FnArg) -> bool {
|
||||
if let FnArg::Typed(t) = f {
|
||||
if let Type::Path(path) = &*t.ty {
|
||||
path.path
|
||||
.segments
|
||||
.iter()
|
||||
.any(|segment| segment.ident == server_context.ty)
|
||||
.any(|segment| segment.ident == "Scope")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -38,81 +23,57 @@ fn fn_arg_is_cx(f: &syn::FnArg, server_context: &ServerContext) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation of the server_fn macro.
|
||||
/// To allow the macro to accept a custom context from the server, pass a custom server context to this function.
|
||||
/// **The Context comes from the server.** Optionally, the first argument of a server function
|
||||
/// can be a custom context. This context can be used to inject dependencies like the HTTP request
|
||||
/// or response or other server-only dependencies, but it does *not* have access to state that exists in the client.
|
||||
///
|
||||
/// The paths passed into this function are used in the generated code, so they must be in scope when the macro is called.
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[proc_macro_attribute]
|
||||
/// pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// let server_context = Some(ServerContext {
|
||||
/// ty: syn::parse_quote!(MyContext),
|
||||
/// path: syn::parse_quote!(my_crate::prelude::MyContext),
|
||||
/// });
|
||||
/// match server_macro_impl(
|
||||
/// args.into(),
|
||||
/// s.into(),
|
||||
/// Some(server_context),
|
||||
/// Some(syn::parse_quote!(my_crate::exports::server_fn)),
|
||||
/// ) {
|
||||
/// Err(e) => e.to_compile_error().into(),
|
||||
/// Ok(s) => s.to_token_stream().into(),
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
pub fn server_macro_impl(
|
||||
args: TokenStream2,
|
||||
body: TokenStream2,
|
||||
server_context: Option<ServerContext>,
|
||||
server_fn_path: Option<Path>,
|
||||
args: proc_macro::TokenStream,
|
||||
s: TokenStream2,
|
||||
) -> Result<TokenStream2> {
|
||||
let ServerFnName {
|
||||
struct_name,
|
||||
prefix,
|
||||
encoding,
|
||||
..
|
||||
} = syn::parse2::<ServerFnName>(args)?;
|
||||
} = syn::parse::<ServerFnName>(args)?;
|
||||
let prefix = prefix.unwrap_or_else(|| Literal::string(""));
|
||||
let encoding = quote!(#server_fn_path::#encoding);
|
||||
let encoding = match encoding {
|
||||
Encoding::Cbor => quote! { ::leptos::leptos_server::Encoding::Cbor },
|
||||
Encoding::Url => quote! { ::leptos::leptos_server::Encoding::Url },
|
||||
};
|
||||
|
||||
let body = syn::parse::<ServerFnBody>(body.into())?;
|
||||
let body = syn::parse::<ServerFnBody>(s.into())?;
|
||||
let fn_name = &body.ident;
|
||||
let fn_name_as_str = body.ident.to_string();
|
||||
let vis = body.vis;
|
||||
let block = body.block;
|
||||
|
||||
let fields = body
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|f| {
|
||||
if let Some(ctx) = &server_context {
|
||||
!fn_arg_is_cx(f, ctx)
|
||||
} else {
|
||||
true
|
||||
cfg_if! {
|
||||
if #[cfg(all(not(feature = "stable"), debug_assertions))] {
|
||||
use proc_macro::Span;
|
||||
let span = Span::call_site();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace('/', "-");
|
||||
#[cfg(target_os = "windows")]
|
||||
let url = format!("{}\\{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("\\", "-");
|
||||
} else {
|
||||
let url = fn_name_as_str;
|
||||
}
|
||||
}
|
||||
|
||||
let fields = body.inputs.iter().filter(|f| !fn_arg_is_cx(f)).map(|f| {
|
||||
let typed_arg = match f {
|
||||
FnArg::Receiver(_) => {
|
||||
abort!(f, "cannot use receiver types in server function macro")
|
||||
}
|
||||
})
|
||||
.map(|f| {
|
||||
let typed_arg = match f {
|
||||
FnArg::Receiver(_) => {
|
||||
abort!(
|
||||
f,
|
||||
"cannot use receiver types in server function macro"
|
||||
)
|
||||
}
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
quote! { pub #typed_arg }
|
||||
});
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
quote! { pub #typed_arg }
|
||||
});
|
||||
|
||||
let cx_arg = body.inputs.iter().next().and_then(|f| {
|
||||
server_context
|
||||
.as_ref()
|
||||
.and_then(|ctx| fn_arg_is_cx(f, ctx).then_some(f))
|
||||
if fn_arg_is_cx(f) {
|
||||
Some(f)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let cx_assign_statement = if let Some(FnArg::Typed(arg)) = cx_arg {
|
||||
if let Pat::Ident(id) = &*arg.pat {
|
||||
@@ -139,11 +100,7 @@ pub fn server_macro_impl(
|
||||
}
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
let is_cx = if let Some(ctx) = &server_context {
|
||||
!fn_arg_is_cx(f, ctx)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
let is_cx = fn_arg_is_cx(f);
|
||||
if is_cx {
|
||||
quote! {
|
||||
#[allow(unused)]
|
||||
@@ -158,12 +115,8 @@ pub fn server_macro_impl(
|
||||
let field_names = body.inputs.iter().filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => todo!(),
|
||||
FnArg::Typed(t) => {
|
||||
if let Some(ctx) = &server_context {
|
||||
if fn_arg_is_cx(f, ctx) {
|
||||
None
|
||||
} else {
|
||||
Some(&t.pat)
|
||||
}
|
||||
if fn_arg_is_cx(f) {
|
||||
None
|
||||
} else {
|
||||
Some(&t.pat)
|
||||
}
|
||||
@@ -195,24 +148,13 @@ pub fn server_macro_impl(
|
||||
);
|
||||
};
|
||||
|
||||
let server_ctx_path = if let Some(ctx) = &server_context {
|
||||
let path = &ctx.path;
|
||||
quote!(#path)
|
||||
} else {
|
||||
quote!(())
|
||||
};
|
||||
|
||||
let server_fn_path = server_fn_path
|
||||
.map(|path| quote!(#path))
|
||||
.unwrap_or_else(|| quote! { server_fn });
|
||||
|
||||
Ok(quote::quote! {
|
||||
#[derive(Clone, Debug, ::serde::Serialize, ::serde::Deserialize)]
|
||||
pub struct #struct_name {
|
||||
#(#fields),*
|
||||
}
|
||||
|
||||
impl #server_fn_path::ServerFn<#server_ctx_path> for #struct_name {
|
||||
impl leptos::ServerFn for #struct_name {
|
||||
type Output = #output_ty;
|
||||
|
||||
fn prefix() -> &'static str {
|
||||
@@ -220,22 +162,22 @@ pub fn server_macro_impl(
|
||||
}
|
||||
|
||||
fn url() -> &'static str {
|
||||
#server_fn_path::const_format::concatcp!(#fn_name_as_str, #server_fn_path::xxhash_rust::const_xxh64::xxh64(concat!(env!("CARGO_MANIFEST_DIR"), ":", file!(), ":", line!(), ":", column!()).as_bytes(), 0))
|
||||
#url
|
||||
}
|
||||
|
||||
fn encoding() -> #server_fn_path::Encoding {
|
||||
fn encoding() -> ::leptos::leptos_server::Encoding {
|
||||
#encoding
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn call_fn(self, cx: #server_ctx_path) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, server_fn::ServerFnError>>>> {
|
||||
fn call_fn(self, cx: ::leptos::Scope) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names),* } = self;
|
||||
#cx_assign_statement;
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_2),*).await })
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn call_fn_client(self, cx: #server_ctx_path) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, server_fn::ServerFnError>>>> {
|
||||
fn call_fn_client(self, cx: ::leptos::Scope) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names_3),* } = self;
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_4),*).await })
|
||||
}
|
||||
@@ -245,22 +187,21 @@ pub fn server_macro_impl(
|
||||
#vis async fn #fn_name(#(#fn_args),*) #output_arrow #return_ty {
|
||||
#block
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
#vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty {
|
||||
let prefix = #struct_name::prefix().to_string();
|
||||
let url = prefix + "/" + #struct_name::url();
|
||||
#server_fn_path::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
|
||||
::leptos::leptos_server::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
struct ServerFnName {
|
||||
pub struct ServerFnName {
|
||||
struct_name: Ident,
|
||||
_comma: Option<Token![,]>,
|
||||
prefix: Option<Literal>,
|
||||
_comma2: Option<Token![,]>,
|
||||
encoding: Path,
|
||||
encoding: Encoding,
|
||||
}
|
||||
|
||||
impl Parse for ServerFnName {
|
||||
@@ -269,14 +210,7 @@ impl Parse for ServerFnName {
|
||||
let _comma = input.parse()?;
|
||||
let prefix = input.parse()?;
|
||||
let _comma2 = input.parse()?;
|
||||
let encoding = input
|
||||
.parse::<Literal>()
|
||||
.map(|encoding| match encoding.to_string().as_str() {
|
||||
"\"Url\"" => syn::parse_quote!(Encoding::Url),
|
||||
"\"Cbor\"" => syn::parse_quote!(Encoding::Cbor),
|
||||
_ => abort!(encoding, "Encoding Not Found"),
|
||||
})
|
||||
.unwrap_or(syn::parse_quote!(Encoding::Url));
|
||||
let encoding = input.parse().unwrap_or(Encoding::Url);
|
||||
|
||||
Ok(Self {
|
||||
struct_name,
|
||||
@@ -288,8 +222,7 @@ impl Parse for ServerFnName {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
struct ServerFnBody {
|
||||
pub struct ServerFnBody {
|
||||
pub attrs: Vec<Attribute>,
|
||||
pub vis: syn::Visibility,
|
||||
pub async_token: Token![async],
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::attribute_value;
|
||||
use leptos_hot_reload::parsing::is_component_node;
|
||||
use crate::{attribute_value, is_component_node};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::spanned::Spanned;
|
||||
@@ -88,9 +87,7 @@ fn root_element_to_tokens(
|
||||
leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
|
||||
#[cfg(debug_assertions)]
|
||||
name: #tag_name.into(),
|
||||
element: root.unchecked_into(),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None
|
||||
element: root.unchecked_into()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -269,8 +266,8 @@ fn attr_to_tokens(
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
) {
|
||||
let name = node.key.to_string();
|
||||
let name = name.strip_prefix('_').unwrap_or(&name);
|
||||
let name = name.strip_prefix("attr:").unwrap_or(name);
|
||||
let name = name.strip_prefix("_").unwrap_or(&name);
|
||||
let name = name.strip_prefix("attr:").unwrap_or(&name);
|
||||
|
||||
let value = match &node.value {
|
||||
Some(expr) => match expr.as_ref() {
|
||||
@@ -302,7 +299,7 @@ fn attr_to_tokens(
|
||||
}
|
||||
// Properties
|
||||
else if let Some(name) = name.strip_prefix("prop:") {
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(&node);
|
||||
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos_dom::property(#cx, #el_id.unchecked_ref(), #name, #value.into_property(#cx))
|
||||
@@ -310,7 +307,7 @@ fn attr_to_tokens(
|
||||
}
|
||||
// Classes
|
||||
else if let Some(name) = name.strip_prefix("class:") {
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(&node);
|
||||
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos::leptos_dom::class_helper(#el_id.unchecked_ref(), #name.into(), #value.into_class(#cx))
|
||||
@@ -321,14 +318,14 @@ fn attr_to_tokens(
|
||||
match value {
|
||||
AttributeValue::Empty => {
|
||||
template.push(' ');
|
||||
template.push_str(name);
|
||||
template.push_str(&name);
|
||||
}
|
||||
|
||||
// Static attributes (i.e., just a literal given as value, not an expression)
|
||||
// are just set in the template — again, nothing programmatic
|
||||
AttributeValue::Static(value) => {
|
||||
template.push(' ');
|
||||
template.push_str(name);
|
||||
template.push_str(&name);
|
||||
template.push_str("=\"");
|
||||
template.push_str(&value);
|
||||
template.push('"');
|
||||
@@ -466,7 +463,7 @@ fn block_to_tokens(
|
||||
|
||||
let mount_kind = match &next_sib {
|
||||
Some(child) => {
|
||||
quote! { leptos::leptos_dom::MountKind::Before(&#child.clone()) }
|
||||
quote! { leptos::leptos_dom::MountKind::Before(#child.clone()) }
|
||||
}
|
||||
None => {
|
||||
quote! { leptos::leptos_dom::MountKind::Append(&#parent) }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::{attribute_value, Mode};
|
||||
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
|
||||
use crate::{attribute_value, is_component_node, Mode};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
|
||||
@@ -147,7 +146,6 @@ pub(crate) fn render_view(
|
||||
nodes: &[Node],
|
||||
mode: Mode,
|
||||
global_class: Option<&TokenTree>,
|
||||
call_site: Option<String>,
|
||||
) -> TokenStream {
|
||||
if mode == Mode::Ssr {
|
||||
match nodes.len() {
|
||||
@@ -157,15 +155,12 @@ pub(crate) fn render_view(
|
||||
span => leptos::leptos_dom::Unit
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
root_node_to_tokens_ssr(cx, &nodes[0], global_class, call_site)
|
||||
}
|
||||
1 => root_node_to_tokens_ssr(cx, &nodes[0], global_class),
|
||||
_ => fragment_to_tokens_ssr(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
nodes,
|
||||
global_class,
|
||||
call_site,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
@@ -176,13 +171,7 @@ pub(crate) fn render_view(
|
||||
span => leptos::leptos_dom::Unit
|
||||
}
|
||||
}
|
||||
1 => node_to_tokens(
|
||||
cx,
|
||||
&nodes[0],
|
||||
TagType::Unknown,
|
||||
global_class,
|
||||
call_site,
|
||||
),
|
||||
1 => node_to_tokens(cx, &nodes[0], TagType::Unknown, global_class),
|
||||
_ => fragment_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
@@ -190,7 +179,6 @@ pub(crate) fn render_view(
|
||||
true,
|
||||
TagType::Unknown,
|
||||
global_class,
|
||||
call_site,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -200,7 +188,6 @@ fn root_node_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &Node,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
match node {
|
||||
Node::Fragment(fragment) => fragment_to_tokens_ssr(
|
||||
@@ -208,7 +195,6 @@ fn root_node_to_tokens_ssr(
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
|
||||
Node::Text(node) => {
|
||||
@@ -225,7 +211,7 @@ fn root_node_to_tokens_ssr(
|
||||
}
|
||||
}
|
||||
Node::Element(node) => {
|
||||
root_element_to_tokens_ssr(cx, node, global_class, view_marker)
|
||||
root_element_to_tokens_ssr(cx, node, global_class)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,15 +221,9 @@ fn fragment_to_tokens_ssr(
|
||||
_span: Span,
|
||||
nodes: &[Node],
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let nodes = nodes.iter().map(|node| {
|
||||
let node = root_node_to_tokens_ssr(cx, node, global_class, None);
|
||||
let node = root_node_to_tokens_ssr(cx, node, global_class);
|
||||
quote! {
|
||||
#node.into_view(#cx)
|
||||
}
|
||||
@@ -253,7 +233,6 @@ fn fragment_to_tokens_ssr(
|
||||
leptos::Fragment::lazy(|| vec![
|
||||
#(#nodes),*
|
||||
])
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,7 +241,6 @@ fn root_element_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
if is_component_node(node) {
|
||||
component_to_tokens(cx, node, global_class)
|
||||
@@ -287,10 +265,10 @@ fn root_element_to_tokens_ssr(
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
format!(
|
||||
#template,
|
||||
#(#holes)*
|
||||
)
|
||||
format!(
|
||||
#template,
|
||||
#(#holes)*
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -320,15 +298,10 @@ fn root_element_to_tokens_ssr(
|
||||
leptos::leptos_dom::#typed_element_name::default()
|
||||
}
|
||||
};
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
quote! {
|
||||
{
|
||||
#(#exprs_for_compiler)*
|
||||
::leptos::HtmlElement::from_html(cx, #full_name, #template)#view_marker
|
||||
::leptos::HtmlElement::from_html(cx, #full_name, #template)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,6 +429,19 @@ fn element_to_tokens_ssr(
|
||||
}
|
||||
}
|
||||
|
||||
fn value_to_string(value: &syn_rsx::NodeValueExpr) -> Option<String> {
|
||||
match &value.as_ref() {
|
||||
syn::Expr::Lit(lit) => match &lit.lit {
|
||||
syn::Lit::Str(s) => Some(s.value()),
|
||||
syn::Lit::Char(c) => Some(c.value().to_string()),
|
||||
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
|
||||
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// returns `inner_html`
|
||||
fn attribute_to_tokens_ssr<'a>(
|
||||
cx: &Ident,
|
||||
@@ -501,8 +487,6 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
template.push_str(&name);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -648,29 +632,20 @@ fn fragment_to_tokens(
|
||||
lazy: bool,
|
||||
parent_type: TagType,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
let nodes = nodes.iter().map(|node| {
|
||||
let node = node_to_tokens(cx, node, parent_type, global_class, None);
|
||||
let node = node_to_tokens(cx, node, parent_type, global_class);
|
||||
|
||||
quote! {
|
||||
#node.into_view(#cx)
|
||||
}
|
||||
});
|
||||
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
if lazy {
|
||||
quote! {
|
||||
{
|
||||
leptos::Fragment::lazy(|| vec![
|
||||
#(#nodes),*
|
||||
])
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -679,7 +654,6 @@ fn fragment_to_tokens(
|
||||
leptos::Fragment::new(vec![
|
||||
#(#nodes),*
|
||||
])
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -690,7 +664,6 @@ fn node_to_tokens(
|
||||
node: &Node,
|
||||
parent_type: TagType,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
match node {
|
||||
Node::Fragment(fragment) => fragment_to_tokens(
|
||||
@@ -700,7 +673,6 @@ fn node_to_tokens(
|
||||
true,
|
||||
parent_type,
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => quote! {},
|
||||
Node::Text(node) => {
|
||||
@@ -715,7 +687,7 @@ fn node_to_tokens(
|
||||
}
|
||||
Node::Attribute(node) => attribute_to_tokens(cx, node),
|
||||
Node::Element(node) => {
|
||||
element_to_tokens(cx, node, parent_type, global_class, view_marker)
|
||||
element_to_tokens(cx, node, parent_type, global_class)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -725,7 +697,6 @@ fn element_to_tokens(
|
||||
node: &NodeElement,
|
||||
mut parent_type: TagType,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
if is_component_node(node) {
|
||||
component_to_tokens(cx, node, global_class)
|
||||
@@ -777,10 +748,7 @@ fn element_to_tokens(
|
||||
None => quote! {},
|
||||
Some(class) => {
|
||||
quote! {
|
||||
.classes(
|
||||
#[allow(unused_braces)]
|
||||
#class
|
||||
)
|
||||
.class(#class, true)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -793,7 +761,6 @@ fn element_to_tokens(
|
||||
true,
|
||||
parent_type,
|
||||
global_class,
|
||||
None,
|
||||
),
|
||||
Node::Text(node) => {
|
||||
let value = node.value.as_ref();
|
||||
@@ -808,7 +775,7 @@ fn element_to_tokens(
|
||||
}
|
||||
}
|
||||
Node::Element(node) => {
|
||||
element_to_tokens(cx, node, parent_type, global_class, None)
|
||||
element_to_tokens(cx, node, parent_type, global_class)
|
||||
}
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => {
|
||||
quote! {}
|
||||
@@ -818,17 +785,11 @@ fn element_to_tokens(
|
||||
.child((#cx, #child))
|
||||
}
|
||||
});
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
quote! {
|
||||
#name
|
||||
#(#attrs)*
|
||||
#global_class_expr
|
||||
#(#children)*
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -859,7 +820,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
};
|
||||
|
||||
let event_type = if is_custom {
|
||||
quote! { Custom::new(#name) }
|
||||
quote! { leptos::ev::Custom::new(#name) }
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
@@ -1060,7 +1021,6 @@ pub(crate) fn component_to_tokens(
|
||||
true,
|
||||
TagType::Unknown,
|
||||
global_class,
|
||||
None,
|
||||
);
|
||||
|
||||
let clonables = items_to_clone
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
use core::num::NonZeroUsize;
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
fn Component(
|
||||
_cx: Scope,
|
||||
#[prop(optional)] optional: bool,
|
||||
#[prop(optional_no_strip)] optional_no_strip: Option<String>,
|
||||
#[prop(strip_option)] strip_option: Option<u8>,
|
||||
#[prop(default = NonZeroUsize::new(10).unwrap())] default: NonZeroUsize,
|
||||
#[prop(into)] into: String,
|
||||
) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component() {
|
||||
let cp = ComponentProps::builder().into("").strip_option(9).build();
|
||||
assert_eq!(cp.optional, false);
|
||||
assert_eq!(cp.optional_no_strip, None);
|
||||
assert_eq!(cp.strip_option, Some(9));
|
||||
assert_eq!(cp.default, NonZeroUsize::new(10).unwrap());
|
||||
assert_eq!(cp.into, "");
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("tests/ui/*.rs");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user