Compare commits

...

57 Commits

Author SHA1 Message Date
Greg Johnston
059c1bf61c cargo fmt 2023-05-14 06:55:05 -04:00
Matt Crane
add13fd6a4 change: migrate Axum integration to use with_state over layer(Extension) (#1032) 2023-05-14 06:37:39 -04:00
Greg Johnston
904c2e8a67 v0.3.0 2023-05-13 19:44:06 -04:00
Greg Johnston
a5c3be586a docs: tweak new slice docs 2023-05-13 19:43:17 -04:00
Markus Kohlhase
9f5139d929 examples: fix trunk config to run tailwind at the right time (#1040) 2023-05-13 19:39:36 -04:00
sjud
bae305340e change: update create_slice to allow different types on getter and setter (#1036) 2023-05-13 19:39:17 -04:00
Greg Johnston
40c1556f29 change: remove APIs that had been marked deprecated (#1037) 2023-05-12 19:45:48 -04:00
Greg Johnston
0db4f5821f fix: avoid extra { escaping (closes #1035) (#1038) 2023-05-12 16:29:33 -04:00
Greg Johnston
12ebc95800 fix: flickering <Transition/> in release mode (closes #960) (#1030) 2023-05-11 14:51:33 -04:00
Greg Johnston
d7b919032e feat: SsrMode::PartiallyBlocked (#1026) 2023-05-10 13:30:01 -04:00
Greg Johnston
be8bf8b0d6 fix: corrects error-deserialization behavior of ActionForm (closes #1024) (#1025) 2023-05-09 06:40:22 -04:00
Greg Johnston
f84f1422f4 fix: maintain insertion order of meta tags (#1021) 2023-05-08 08:36:54 -04:00
Snêu
b01976e3bb examples: fix indentations (#1017) 2023-05-08 08:36:45 -04:00
agilarity
50b48fb272 chore: build CSS with trunk (#1016)
This configures a hook to run the tailwindcss CLI when a build is triggered or retriggered via Trunk watch. It eliminates the need to run the tailwindcss manually.
2023-05-08 08:36:07 -04:00
agilarity
1617e31d69 CI: clean up examples after verification (#1019)
* build: improve task names

* build: add clean-examples task

Make it easy to clean all the cargo and trunk files in the examples.

* build: clean after verify
2023-05-08 08:35:27 -04:00
Chris
51cd082d4c docs: add examples for manual server integration for router (#1015) 2023-05-08 08:34:43 -04:00
Warre Dujardin
72414b7945 docs: fix link to cargo-leptos README in the book (#1012) 2023-05-06 13:42:44 -04:00
FrankReh
1afa14ccbd docs: adjust Dynamic Attributes page (#1011)
Adjust the intro in the Dynamic Attributes page to include
the recent `dynamic style` feature. Also reorder a little given
the order of the page body that follows.
2023-05-06 13:42:16 -04:00
Warre Dujardin
477c29cdf1 docs: close iframe tag (#1013) 2023-05-06 13:41:50 -04:00
Greg Johnston
49a424314a docs: add a serevr fn section (#1014) 2023-05-06 13:14:16 -04:00
Warre Dujardin
598523cd9d fix: relax Debug trait bounds (#1010) 2023-05-06 12:10:48 -04:00
Greg Johnston
1fdb6f1cdf feat: add style: to view (#1009) 2023-05-06 06:23:20 -04:00
agilarity
9997487a9c test: lint examples with --all-features (#1008)
* test: lint all features

* fix(counter_isomorphic): check-style issues

* fix(errors_axum): check-style issues

* fix(hackernews): check-style issues

* fix(hackernews_axum): check-style issues

* fix(session_auth_axum): check-style issues

* build(session_auth_axum): add common tasks

* fix(ssr_modes): check-style issues

* build(ssr_modes_axum): add common tasks

* fix(ssr_modes_axum): check-style issues

* build(tailwind): add common tasks

* fix(tailwind): check-style issues

* fix(todo_app_sqlite_axum): check-style issues

* fix(todo_app_sqlite_viz): check-style issues
2023-05-05 22:25:29 -04:00
Greg Johnston
b5e94e4054 fix: properly dispose of <Suspense/> scopes (closes #834) (#1006) 2023-05-05 19:08:56 -04:00
Greg Johnston
a5f6e0bac4 docs: document that <ActionForm/> only works with form-encoded server functions (closes #977) (#1005) 2023-05-05 13:37:53 -04:00
Douglas Parsons
2c9de79576 docs: Reduce firmness of overlapping signals warnings (#1004)
Following [discord
question](https://discord.com/channels/1031524867910148188/1049869221636620300/1104043773194928163)
2023-05-05 11:28:36 -04:00
agilarity
63dd00a050 fix: lint issues in todomvc example (#1001)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:28:14 -04:00
agilarity
99823a3d4f fix: lint issues in todo_app_sqlite_viz example (#1000)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:27:38 -04:00
agilarity
c0bdd464f6 fix: lint issues in todo_app_sqlite_axum example (#999)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:27:27 -04:00
agilarity
7e7377f4f7 fix: lint issues in todo_app_sqlite example (#998)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:27:11 -04:00
agilarity
15448765dd fix: lint issues in session_auth_axum example (#997)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:26:44 -04:00
agilarity
f0f1c3144b fix: lint issues in router example (#996)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:26:24 -04:00
agilarity
630da4212d fix: lint issues in login_with_token_csr_only example (#995)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:26:09 -04:00
agilarity
38bc24bb9e fix: lint issues in hackernews_axum example (#992)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:25:24 -04:00
agilarity
012285337b fix: lint issues in hackernews example (#991)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:25:13 -04:00
agilarity
3ba4f62cef fix: lint issues in fetch example (#989)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:24:28 -04:00
agilarity
b4996769c1 fix: lint issues in errors_axum example (#988) 2023-05-05 11:23:59 -04:00
Greg Johnston
9a6b1f53da fix: lint issues in counters example
fix: lint issues in `counters` example
2023-05-05 11:23:38 -04:00
Greg Johnston
ef45828ca7 fix: don't assume OutOfOrder and GET for / 2023-05-05 10:20:36 -04:00
Greg Johnston
ea153e4f26 docs: error when component ends with view! { ... }; (closes #985) (#993) 2023-05-03 18:15:02 -04:00
Greg Johnston
59b8626277 docs: switch from compile errors to runtime warnings for incompatible feature flags (#990) 2023-05-03 16:25:35 -04:00
Greg Johnston
d8e03773f0 feat: allow structs in server function arguments (#987) 2023-05-03 15:26:48 -04:00
Joseph Cruz
5ab799bbf8 test: resolve check-style issues 2023-05-03 12:34:03 -04:00
Greg Johnston
6c763a83cb fix: suppress warning loading local resource without <Suspense/> in hydrate mode (closes #979) (#984) 2023-05-03 11:22:34 -04:00
agilarity
9cf337309d Fix lint issues in counter_isomorphic example (#980) 2023-05-02 20:22:07 -04:00
Greg Johnston
1af35cdd3b feat: add builder syntax for optional event listener (#969) 2023-05-02 15:47:19 -04:00
agilarity
fcb98474b8 examples: fix the lint issues in the counter example (#971) 2023-05-01 17:27:29 -04:00
Greg Johnston
54f7e9366a change/fix: require FromStr errors on Params to be Send + Sync so they are ErrorBoundary compatible (#974) 2023-05-01 17:18:46 -04:00
Matt Crane
ddf9df2b5e change: replace serde_urlencoded with serde_html_form to support Vec<_> in server fn args (#973) 2023-05-01 17:17:45 -04:00
Greg Johnston
7fe9f82d89 v0.3.0-alpha (#968) 2023-04-28 19:30:16 -04:00
Roland Fredenhagen
661adc4027 feat: ```view code block in doc comments for properties (#961) 2023-04-28 16:03:04 -04:00
Roland Fredenhagen
1011c464dc feat: add collect_view(cx) (#956) 2023-04-28 16:02:24 -04:00
Frank Panetta
4b498a3b42 chore: fix typos (#964) 2023-04-28 12:10:48 -04:00
yuuma03
3c90b47e77 fix: allow mounting multiple Leptos apps on same server (#966)
Use a HashMap indexed by base URL to cache route branches on the server.
2023-04-28 12:10:02 -04:00
Greg Johnston
671b1e4a8f docs: note need for serde dependency for server functions (closes #947) (#962) 2023-04-27 17:15:29 -04:00
agilarity
52021be806 tests: add wasm web test and common tasks (#954)
* test: rename web test module

* test: extract wasm-web-test task

* test: introduce common tasks

* test: add web-test and common tasks
2023-04-27 17:00:13 -04:00
Roland Fredenhagen
75a7bd610a fix: escapes in doc comments on component properties (#958) 2023-04-27 16:43:38 -04:00
144 changed files with 2334 additions and 1353 deletions

View File

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

View File

@@ -69,13 +69,31 @@ dependencies = [
[tasks.test]
clear = true
dependencies = ["test-all"]
dependencies = [
"test-all",
"test-leptos_macro-example",
"doc-leptos_macro-example",
]
[tasks.test-all]
command = "cargo"
args = ["+nightly", "test-all-features"]
install_crate = "cargo-all-features"
[tasks.test-leptos_macro-example]
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly", "test", "--doc"]
cwd = "leptos_macro/example"
install_crate = false
[tasks.doc-leptos_macro-example]
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly", "doc"]
cwd = "leptos_macro/example"
install_crate = false
[tasks.test-examples]
description = "Run all unit and web tests for examples"
cwd = "examples"
@@ -88,9 +106,15 @@ cwd = "examples"
command = "cargo"
args = ["make", "verify-flow"]
[tasks.clean-examples]
description = "Clean all example projects"
cwd = "examples"
command = "cargo"
args = ["make", "clean-all"]
[env]
RUSTFLAGS = ""
LEPTOS_OUTPUT_NAME="ci" # allows examples to check/build without cargo-leptos
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
[env.github-actions]
RUSTFLAGS = "-D warnings"

View File

@@ -171,4 +171,4 @@ data flow and of fine-grained reactive updates.
[Click to open CodeSandbox.](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)
<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" style="max-height: 100vh">
<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" style="max-height: 100vh"></iframe>

View File

@@ -34,9 +34,10 @@
- [`cargo-leptos`](./ssr/21_cargo_leptos.md)
- [The Life of a Page Load](./ssr/22_life_cycle.md)
- [Async Rendering and SSR “Modes”](./ssr/23_ssr_modes.md)
- [Hydration Footguns](./ssr/24_hydration_bugs.md)
- [Server Functions]()
- [Request/Response]()
- [Hydration Bugs](./ssr/24_hydration_bugs.md)
- [Working with the Server](./server/README.md)
- [Server Functions](./server/25_server_functions.md)
- [Request/Response]()
- [Extractors]()
- [Axum]()
- [Actix]()
@@ -46,6 +47,6 @@
- [Actions]()
- [Forms]()
- [`<ActionForm/>`s]()
- [Turning off WebAssembly]()
- [Turning off WebAssembly: Progressive Enhancement and Graceful Degradation]()
- [Advanced Reactivity]()
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)

View File

@@ -2,8 +2,8 @@
So far weve only been working with synchronous users interfaces: You provide some input,
the app immediately processes it and updates the interface. This is great, but is a tiny
subset of what web applications do. In particular, most web apps have to deal with some kind
of asynchronous data loading, usually loading something from an API.
subset of what web applications do. In particular, most web apps have to deal with some kind of asynchronous data loading, usually loading something from an API.
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code. Leptos provides a cross-platform [`spawn_local`](https://docs.rs/leptos/latest/leptos/fn.spawn_local.html) function that makes it easy to run a `Future`, but theres much more to it than that.
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code.
In this chapter, well see how Leptos helps smooth out that process for you.

View File

@@ -31,7 +31,7 @@ where
This is pretty straightforward: when the user is logged in, we want to show `children`. Until if the user is not logged in, we want to show `fallback`. And while were waiting to find out, we just render `()`, i.e., nothing.
In other words, we want to pass the children of `<WhenLoaded>/` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
In other words, we want to pass the children of `<WhenLoaded/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
This wont compile.
@@ -40,7 +40,7 @@ error[E0507]: cannot move out of `fallback`, a captured variable in an `Fn` clos
error[E0507]: cannot move out of `children`, a captured variable in an `Fn` closure
```
The problem here is that both `<Suspense/>` and `<Show/>` need to be able to construct their `children` multiple names. The first time you construct `<Suspense/>`s children, it would take ownership of `fallback` and `children` to move them into the invocation of `<Show/>`, but then they're not available for future `<Suspense/>` children construction.
The problem here is that both `<Suspense/>` and `<Show/>` need to be able to construct their `children` multiple times. The first time you construct `<Suspense/>`s children, it would take ownership of `fallback` and `children` to move them into the invocation of `<Show/>`, but then they're not available for future `<Suspense/>` children construction.
## The Details
@@ -80,13 +80,13 @@ Suspense(
)
```
All components own their props; so the `<Show/>` in this case cant be called, because it only has captured references to `fallback` and `children`.
All components own their props; so the `<Show/>` in this case cant be called because it only has captured references to `fallback` and `children`.
## Solution
However, both `<Suspense/>` and `<Show/>` take `ChildrenFn`, i.e., their `children` should implement the `Fn` type so they can be called multiple times with only an immutable reference. This means we dont need to own `children` or `fallback`; we just need to be able to pass `'static` references to them.
We can solve this problem by using the [`store_value`](https://docs.rs/leptos/latest/leptos/fn.store_value.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, and which we can access or modify through certain methods.
We can solve this problem by using the [`store_value`](https://docs.rs/leptos/latest/leptos/fn.store_value.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, which we can access or modify through certain methods.
In this case, its really simple:
@@ -113,7 +113,7 @@ where
}
```
At the top level, we store both `fallback` and `children` in the reactive scope owned by `LoggedIn`. Now we can simply move those references down through the other layers into the `<Show/>` component, and call them there.
At the top level, we store both `fallback` and `children` in the reactive scope owned by `LoggedIn`. Now we can simply move those references down through the other layers into the `<Show/>` component and call them there.
## A Final Note

View File

@@ -0,0 +1,125 @@
# Server Functions
If youre creating anything beyond a toy app, youll need to run code on the server all the time: reading from or writing to a database that only runs on the server, running expensive computations using libraries you dont want to ship down to the client, accessing APIs that need to be called from the server rather than the client for CORS reasons or because you need a secret API key thats stored on the server and definitely shouldnt be shipped down to a users browser.
Traditionally, this is done by separating your server and client code, and by setting up something like a REST API or GraphQL API to allow your client to fetch and mutate data on the server. This is fine, but it requires you to write and maintain your code in multiple separate places (client-side code for fetching, server-side functions to run), as well as creating a third thing to manage, which is the API contract between the two.
Leptos is one of a number of modern frameworks that introduce the concept of **server functions**. Server functions have two key characteristics:
1. Server functions are **co-located** with your component code, so that you can organize your work by feature, not by technology. For example, you might have a “dark mode” feature that should persist a users dark/light mode preference across sessions, and be applied during server rendering so theres no flicker. This requires a component that needs to be interactive on the client, and some work to be done on the server (setting a cookie, maybe even storing a user in a database.) Traditionally, this feature might end up being split between two different locations in your code, one in your “frontend” and one in your “backend.” With server functions, youll probably just write them both in one `dark_mode.rs` and forget about it.
2. Server functions are **isomorphic**, i.e., they can be called either from the server or the browser. This is done by generating code differently for the two platforms. On the server, a server function simply runs. In the browser, the server functions body is replaced with a stub that actually makes a fetch request to the server, serializing the arguments into the request and deserializing the return value from the response. But on either end, the function can simply be called: you can create an `add_todo` function that writes to your database, and simply call it from a click handler on a button in the browser!
## Using Server Functions
Actually, I kind of like that example. What would it look like? Its pretty simple, actually.
```rust
// todo.rs
#[server(AddTodo, "/api")]
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
let mut conn = db().await?;
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
.bind(title)
.execute(&mut conn)
.await
{
Ok(_row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
}
}
#[component]
pub fn BusyButton(cx: Scope) -> impl IntoView {
view! {
cx,
<button on:click=move |_| {
spawn_local(async {
add_todo("So much to do!".to_string()).await;
});
}>
"Add Todo"
</button>
}
}
// somewhere in main.rs
fn main() {
// ...
AddTodo::register();
// ...
}
```
Youll notice a couple things here right away:
- Server functions can use server-only dependencies, like `sqlx`, and can access server-only resources, like our database.
- Server functions are `async`. Even if they only did synchronous work on the server, the function signature would still need to be `async`, because calling them from the browser _must_ be asynchronous.
- Server functions return `Result<T, ServerFnError>`. Again, even if they only do infallible work on the server, this is true, because `ServerFnError`s variants include the various things that can be wrong during the process of making a network request.
- Server functions can be called from the client. Take a look at our click handler. This is code that will _only ever_ run on the client. But it can call the function `add_todo` (using `spawn_local` to run the `Future`) as if it were an ordinary async function:
```rust
move |_| {
spawn_local(async {
add_todo("So much to do!".to_string()).await;
});
}
```
- Server functions are top-level functions defined with `fn`. Unlike event listeners, derived signals, and most everything else in Leptos, they are not closures! As `fn` calls, they have no access to the reactive state of your app or anything else that is not passed in as an argument. And again, this makes perfect sense: When you make a request to the server, the server doesnt have access to client state unless you send it explicitly. (Otherwise wed have to serialize the whole reactive system and send it across the wire with every request, which—while it served classic ASP for a while—is a really bad idea.)
- Server function arguments and return values both need to be serializable with `serde`. Again, hopefully this makes sense: while function arguments in general dont need to be serialized, calling a server function from the browser means serializing the arguments and sending them over HTTP.
There are a few things to note about the way you define a server function, too.
- Server functions are created by using the [`#[server]` macro](https://docs.rs/leptos_server/latest/leptos_server/index.html#server) to annotate a top-level function, which can be defined anywhere.
- We provide the macro a type name. The type name is used to register the server function (in `main.rs`), and its used internally as a container to hold, serialize, and deserialize the arguments.
- We provide the macro a path. This is a prefix for the path at which well mount a server function handler on our server. (See examples for [Actix](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/main.rs#L44) and [Axum](https://github.com/leptos-rs/leptos/blob/598523cd9d0d775b017cb721e41ebae9349f01e2/examples/todo_app_sqlite_axum/src/main.rs#L51).)
- Youll need to have `serde` as a dependency with the `derive` featured enabled for the macro to work properly. You can easily add it to `Cargo.toml` with `cargo add serde --features=derive`.
## Server Function Encodings
By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body of the request. (This means that server functions can be called from HTML forms, which well see in a future chapter.) But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` macro to specify an alternate encoding:
```rust
#[server(AddTodo, "/api", "Url")]
#[server(AddTodo, "/api", "GetJson")]
#[server(AddTodo, "/api", "Cbor")]
#[server(AddTodo, "/api", "GetCbor")]
```
The four options use different combinations of HTTP verbs and encoding methods:
| Name | Method | Request | Response |
| ----------------- | ------ | ----------- | -------- |
| **Url** (default) | POST | URL encoded | JSON |
| **GetJson** | GET | URL encoded | JSON |
| **Cbor** | POST | CBOR | CBOR |
| **GetCbor** | GET | URL encoded | CBOR |
In other words, you have two choices:
- `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached, `GET` requests can be.
- Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64 string)?
**But remember**: Leptos will handle all the details of this encoding and decoding for you. When you use a server function, it looks just like calling any other asynchronous function!
## An Important Note on Security
Server functions are a cool technology, but its very important to remember. **Server functions are not magic; theyre syntax sugar for defining a public API.** The _body_ of a server function is never made public; its just part of your server binary. But the server function is a publicly accessible API endpoint, and its return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function.
## Integrating Server Functions with Leptos
So far, everything Ive said is actually framework agnostic. (And in fact, the Leptos server function crate has been integrated into Dioxus as well!) Server functions are simply a way of defining a function-like RPC call that leans on Web standards like HTTP requests and URL encoding.
But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed [earlier](../async/README.md). So you can easily integrate your server functions with the rest of your applications:
- Create **resources** that call the server function to load data from the server
- Read these resources under `<Suspense/>` or `<Transition/>` to enable streaming SSR and fallback states while data loads.
- Create **actions** that call the server function to mutate data on the server
The final section of this book will make this a little more concrete by introducing patterns that use progressively-enhanced HTML forms to run these server actions.
But in the next few chapters, well actually take a look at some of the details of what you might want to do with your server functions, including the best ways to integrate with the powerful extractors provided by the Actix and Axum server frameworks.

View File

@@ -0,0 +1,11 @@
# Working with the Server
The previous section described the process of server-side rendering, using the server to generate an HTML version of the page that will become interactive in the browser. So far, everything has been “isomorphic” or “universal”; in other words, your app has had the “same (_iso_) shape (_morphe_)” on the client and the server.
But a server can do a lot more than just render HTML! In fact, a server can do a whole bunch of things your browser _cant,_ like reading from and writing to a SQL database.
If youre used to building JavaScript frontend apps, youre probably used to calling out to some kind of REST API to do this sort of server work. If youre used to building sites with PHP or Python or Ruby (or Java or C# or...), this server-side work is your bread and butter, and its the client-side interactivity that tends to be an afterthought.
With Leptos, you can do both: not only in the same language, not only sharing the same types, but even in the same files!
This section will talk about how to build the uniquely-server-side parts of your application.

View File

@@ -32,6 +32,6 @@ cargo leptos watch
Once your app has compiled you can open up your browser to [`http://localhost:3000`](http://localhost:3000) to see it.
`cargo-leptos` has lots of additional features and built in tools. You can learn more [in its `README`](https://github.com/leptos-rs/leptos/blob/main/examples/hackernews/src/api.rs).
`cargo-leptos` has lots of additional features and built in tools. You can learn more [in its `README`](https://github.com/leptos-rs/cargo-leptos/blob/main/README.md).
But what exactly is happening when you open our browser to `localhost:3000`? Well, read on to find out.

View File

@@ -65,7 +65,7 @@ pub fn App(cx: Scope) -> impl IntoView {
};
data.into_iter()
.map(|value| view! { cx, <span>{value}</span> })
.collect::<Vec<_>>()
.collect_view(cx)
}
```

View File

@@ -1,10 +1,10 @@
# `view`: Dynamic Attributes and Classes
# `view`: Dynamic Classes, Styles and Attributes
So far weve seen how to use the `view` macro to create event listeners and to
create dynamic text by passing a function (such as a signal) into the view.
But of course there are other things you might want to update in your user interface.
In this section, well look at how to update attributes and classes dynamically,
In this section, well look at how to update classes, styles and attributes dynamically,
and well introduce the concept of a **derived signal**.
Lets start with a simple component that should be familiar: click a button to
@@ -52,12 +52,42 @@ reactively update when the signal changes.
Now every time I click the button, the text should toggle between red and black as
the number switches between even and odd.
Some CSS class names cant be directly parsed by the `view` macro, especially if they include a mix of dashes and numbers or other characters. In that case, you can use a tuple syntax: `class=("name", value)` still directly updates a single class.
```rust
class=("button-20", move || count() % 2 == 1)
```
> If youre following along, make sure you go into your `index.html` and add something like this:
>
>
> ```html
> <style>.red { color: red; }</style>
> <style>
> .red {
> color: red;
> }
> </style>
> ```
## Dynamic Styles
Individual CSS properties can be directly updated with a similar `style:` syntax.
```rust
let (x, set_x) = create_signal(cx, 0);
let (y, set_y) = create_signal(cx, 0);
view! { cx,
<div
style="position: absolute"
style:left=move || format!("{}px", x() + 100)
style:top=move || format!("{}px", y() + 100)
style:background-color=move || format!("rgb({}, {}, 100)", x(), y())
style=("--columns", x)
>
"Moves when coordinates change"
</div>
}
```
## Dynamic Attributes
The same applies to plain attributes. Passing a plain string or primitive value to

View File

@@ -31,6 +31,22 @@ view! { cx,
}
```
Leptos also provides a `.collect_view(cx)` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
```rust
let values = vec![0, 1, 2];
view! { cx,
// this will just render "012"
<p>{values.clone()}</p>
// or we can wrap them in <li>
<ul>
{values.into_iter()
.map(|n| view! { cx, <li>{n}</li>})
.collect_view(cx)}
</ul>
}
```
The fact that the _list_ is static doesnt mean the interface needs to be static.
You can render dynamic items as part of a static list.
@@ -52,7 +68,7 @@ let counter_buttons = counters
</li>
}
})
.collect::<Vec<_>>();
.collect_view(cx);
view! { cx,
<ul>{counter_buttons}</ul>

View File

@@ -80,7 +80,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
.collect_view(cx)
}
</ul>
</div>

View File

@@ -103,7 +103,7 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
.nodes
.into_iter()
.map(|child| view! { cx, <li>{child}</li> })
.collect::<Vec<_>>();
.collect_view(cx);
view! { cx,
<ul>{children}</ul>

View File

@@ -1,10 +1,9 @@
extend = [{ path = "./cargo-make/common.toml" }]
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
# Emulate workspace
CARGO_MAKE_WORKSPACE_EMULATION = true
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"counter",
"counter_isomorphic",
@@ -24,7 +23,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"ssr_modes_axum",
"tailwind",
"tailwind_csr_trunk",
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
@@ -33,7 +31,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
[tasks.verify-flow]
description = "Provides pre and post hooks for verify"
dependencies = ["pre-verify-flow", "verify", "post-verify-flow"]
dependencies = ["pre-verify", "verify", "post-verify"]
[tasks.verify]
description = "Run all quality checks and tests"
@@ -43,20 +41,17 @@ dependencies = ["check-style", "test-unit-and-web"]
description = "Run all unit and web tests"
dependencies = ["test-flow", "web-test-flow"]
[tasks.check-style]
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.pre-verify]
[tasks.pre-verify-flow]
[tasks.post-verify-flow]
[tasks.post-verify]
dependencies = ["clean-all"]
[tasks.web-test-flow]
description = "Provides pre and post hooks for web-test"
dependencies = ["pre-web-test-flow", "web-test", "post-web-test-flow"]
dependencies = ["pre-web-test", "web-test", "post-web-test"]
[tasks.pre-web-test-flow]
[tasks.pre-web-test]
[tasks.web-test]
[tasks.post-web-test-flow]
[tasks.post-web-test]

View File

@@ -0,0 +1,23 @@
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
[tasks.check-style]
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.verify-local]
description = "Run all quality checks and tests from an example directory"
dependencies = ["check-style", "test-local"]
[tasks.test-local]
description = "Run all tests from an example directory"
dependencies = ["test", "web-test"]
[tasks.clean-trunk]
description = "Runs the trunk clean command."
category = "Cleanup"
command = "trunk"
args = ["clean"]
[tasks.clean-all]
dependencies = ["clean", "clean-trunk"]

View File

@@ -0,0 +1,4 @@
[tasks.web-test]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]

View File

@@ -1,3 +1,8 @@
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/wasm-web-test.toml" },
]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,7 +1,7 @@
use leptos::*;
/// A simple counter component.
///
///
/// You can use doc comments like this to document your component.
#[component]
pub fn SimpleCounter(
@@ -9,7 +9,7 @@ pub fn SimpleCounter(
/// The starting value for the counter
initial_value: i32,
/// The change that should be applied each time the button is clicked.
step: i32
step: i32,
) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);

View File

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

View File

@@ -9,7 +9,7 @@ wasm_bindgen_test_configure!(run_in_browser);
fn clear() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
document.body().unwrap().append_child(&test_wrapper);
let _ = document.body().unwrap().append_child(&test_wrapper);
// start by rendering our counter and mounting it to the DOM
// note that we start at the initial value of 10
@@ -38,7 +38,7 @@ fn clear() {
// test case
run_scope(create_runtime(), |cx| {
// it's as if we're creating it with a value of 0, right?
let (value, set_value) = create_signal(cx, 0);
let (value, _set_value) = create_signal(cx, 0);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
@@ -71,7 +71,7 @@ fn clear() {
fn inc() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
document.body().unwrap().append_child(&test_wrapper);
let _ = document.body().unwrap().append_child(&test_wrapper);
mount_to(
test_wrapper.clone().unchecked_into(),
@@ -79,7 +79,7 @@ fn inc() {
);
// You can do testing with vanilla DOM operations
let document = leptos::document();
let _document = leptos::document();
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = div
.first_child()

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,27 +1,27 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_router::*;
use leptos_meta::*;
use leptos_router::*;
#[cfg(feature = "ssr")]
use std::sync::atomic::{AtomicI32, Ordering};
cfg_if! {
if #[cfg(feature = "ssr")] {
use std::sync::atomic::{AtomicI32, Ordering};
use broadcaster::BroadcastChannel;
static COUNT: AtomicI32 = AtomicI32::new(0);
#[cfg(feature = "ssr")]
use broadcaster::BroadcastChannel;
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
#[cfg(feature = "ssr")]
pub fn register_server_functions() {
_ = GetServerCount::register();
_ = AdjustServerCount::register();
_ = ClearServerCount::register();
pub fn register_server_functions() {
_ = GetServerCount::register();
_ = AdjustServerCount::register();
_ = ClearServerCount::register();
}
}
}
#[cfg(feature = "ssr")]
static COUNT: AtomicI32 = AtomicI32::new(0);
#[cfg(feature = "ssr")]
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
#[server(GetServerCount, "/api")]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
@@ -29,7 +29,10 @@ pub async fn get_server_count() -> Result<i32, ServerFnError> {
}
#[server(AdjustServerCount, "/api")]
pub async fn adjust_server_count(delta: i32, msg: String) -> Result<i32, ServerFnError> {
pub async fn adjust_server_count(
delta: i32,
msg: String,
) -> Result<i32, ServerFnError> {
let new = COUNT.load(Ordering::Relaxed) + delta;
COUNT.store(new, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&new).await;
@@ -46,36 +49,49 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
view! {
cx,
view! { cx,
<Router>
<header>
<h1>"Server-Side Counters"</h1>
<p>"Each of these counters stores its data in the same variable on the server."</p>
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
<p>
"The value is shared across connections. Try opening this is another browser tab to see what I mean."
</p>
</header>
<nav>
<ul>
<li><A href="">"Simple"</A></li>
<li><A href="form">"Form-Based"</A></li>
<li><A href="multi">"Multi-User"</A></li>
<li>
<A href="">"Simple"</A>
</li>
<li>
<A href="form">"Form-Based"</A>
</li>
<li>
<A href="multi">"Multi-User"</A>
</li>
</ul>
</nav>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<main>
<Routes>
<Route path="" view=|cx| view! {
cx,
<Counter/>
}/>
<Route path="form" view=|cx| view! {
cx,
<FormCounter/>
}/>
<Route path="multi" view=|cx| view! {
cx,
<MultiuserCounter/>
}/>
<Route
path=""
view=|cx| {
view! { cx, <Counter/> }
}
/>
<Route
path="form"
view=|cx| {
view! { cx, <FormCounter/> }
}
/>
<Route
path="multi"
view=|cx| {
view! { cx, <MultiuserCounter/> }
}
/>
</Routes>
</main>
</Router>
@@ -93,33 +109,47 @@ pub fn Counter(cx: Scope) -> impl IntoView {
let clear = create_action(cx, |_| clear_server_count());
let counter = create_resource(
cx,
move || (dec.version().get(), inc.version().get(), clear.version().get()),
move || {
(
dec.version().get(),
inc.version().get(),
clear.version().get(),
)
},
|_| get_server_count(),
);
let value = move || counter.read(cx).map(|count| count.unwrap_or(0)).unwrap_or(0);
let error_msg = move || {
let value = move || {
counter
.read(cx)
.map(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
})
.flatten()
.map(|count| count.unwrap_or(0))
.unwrap_or(0)
};
let error_msg = move || {
counter.read(cx).and_then(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
})
};
view! {
cx,
view! { cx,
<div>
<h2>"Simple Counter"</h2>
<p>"This counter sets the value on the server and automatically reloads the new value."</p>
<p>
"This counter sets the value on the server and automatically reloads the new value."
</p>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
{move || error_msg().map(|msg| view! { cx, <p>"Error: " {msg.to_string()}</p>})}
{move || {
error_msg()
.map(|msg| {
view! { cx, <p>"Error: " {msg.to_string()}</p> }
})
}}
</div>
}
}
@@ -142,19 +172,15 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
);
let value = move || {
log::debug!("FormCounter looking for value");
counter
.read(cx)
.map(|n| n.ok())
.flatten()
.map(|n| n)
.unwrap_or(0)
counter.read(cx).and_then(|n| n.ok()).unwrap_or(0)
};
view! {
cx,
view! { cx,
<div>
<h2>"Form Counter"</h2>
<p>"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"</p>
<p>
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
</p>
<div>
// calling a server function is the same as POSTing to its API URL
// so we can just do that with a form and button
@@ -185,26 +211,32 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
// This is the primitive pattern for live chat, collaborative editing, etc.
#[component]
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
let dec =
create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
let inc =
create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(cx, |_| clear_server_count());
#[cfg(not(feature = "ssr"))]
let multiplayer_value = {
use futures::StreamExt;
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect("couldn't connect to SSE stream");
let mut source =
gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect("couldn't connect to SSE stream");
let s = create_signal_from_stream(
cx,
source.subscribe("message").unwrap().map(|value| {
match value {
Ok(value) => {
value.1.data().as_string().expect("expected string value")
},
source
.subscribe("message")
.unwrap()
.map(|value| match value {
Ok(value) => value
.1
.data()
.as_string()
.expect("expected string value"),
Err(_) => "0".to_string(),
}
})
}),
);
on_cleanup(cx, move || source.close());
@@ -212,18 +244,20 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
};
#[cfg(feature = "ssr")]
let (multiplayer_value, _) =
create_signal(cx, None::<i32>);
let (multiplayer_value, _) = create_signal(cx, None::<i32>);
view! {
cx,
view! { cx,
<div>
<h2>"Multi-User Counter"</h2>
<p>"This one uses server-sent events (SSE) to live-update when other users make changes."</p>
<p>
"This one uses server-sent events (SSE) to live-update when other users make changes."
</p>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default().to_string()}</span>
<span>
"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default()}
</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
</div>

View File

@@ -1,10 +1,10 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod counters;
// 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 leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::counters::*;

View File

@@ -1,11 +1,11 @@
use cfg_if::cfg_if;
use leptos::*;
mod counters;
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use leptos::*;
use actix_files::{Files};
use actix_web::*;
use crate::counters::*;
@@ -37,7 +37,7 @@ cfg_if! {
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr.clone();
let addr = conf.leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
HttpServer::new(move || {
@@ -48,7 +48,7 @@ cfg_if! {
.service(counter_events)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <Counters/> })
.service(Files::new("/", &site_root))
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?

View File

@@ -1,12 +1,7 @@
[env]
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
[tasks.test-all]
dependencies = ["test", "web-test"]
[tasks.web-test]
command = "cargo"
args = ["make", "wasm-pack-test"]
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/wasm-web-test.toml" },
]
[tasks.build]
command = "cargo"

View File

@@ -1,3 +1,8 @@
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/wasm-web-test.toml" },
]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,4 +1,4 @@
use leptos::{For, ForProps, *};
use leptos::{For, *};
const MANY_COUNTERS: usize = 1000;

View File

@@ -1,68 +0,0 @@
use wasm_bindgen_test::*;
use wasm_bindgen::JsCast;
wasm_bindgen_test_configure!(run_in_browser);
use leptos::*;
use web_sys::HtmlElement;
use counters::Counters;
#[wasm_bindgen_test]
fn inc() {
mount_to_body(|cx| view! { cx, <Counters/> });
let document = leptos::document();
let div = document.query_selector("div").unwrap().unwrap();
let add_counter = div
.first_child()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
// add 3 counters
add_counter.click();
add_counter.click();
add_counter.click();
// check HTML
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->0<!-- </DynChild> --></span> from <span><!-- <DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
let counters = div
.query_selector("ul")
.unwrap()
.unwrap()
.unchecked_into::<HtmlElement>()
.children();
// click first counter once, second counter twice, etc.
// `NodeList` isn't a `Vec` so we iterate over it in this slightly awkward way
for idx in 0..counters.length() {
let counter = counters.item(idx).unwrap();
let inc_button = counter
.first_child()
.unwrap()
.next_sibling()
.unwrap()
.next_sibling()
.unwrap()
.next_sibling()
.unwrap()
.unchecked_into::<HtmlElement>();
for _ in 0..=idx {
inc_button.click();
}
}
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->6<!-- </DynChild> --></span> from <span><!-- <DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->1<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->2<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->3<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
// remove the first counter
counters
.item(0)
.unwrap()
.last_child()
.unwrap()
.unchecked_into::<HtmlElement>()
.click();
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->5<!-- </DynChild> --></span> from <span><!-- <DynChild> -->2<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->2<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->3<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
}

View File

@@ -0,0 +1,120 @@
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counters::Counters;
use leptos::*;
use web_sys::HtmlElement;
#[wasm_bindgen_test]
fn inc() {
mount_to_body(|cx| view! { cx, <Counters/> });
let document = leptos::document();
let div = document.query_selector("div").unwrap().unwrap();
let add_counter = div
.first_child()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
// add 3 counters
add_counter.click();
add_counter.click();
add_counter.click();
// check HTML
assert_eq!(
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
<DynChild> -->0<!-- </DynChild> --></span> from <span><!-- \
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
<Each> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->0<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->0<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->0<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- </Each> --></ul>"
);
let counters = div
.query_selector("ul")
.unwrap()
.unwrap()
.unchecked_into::<HtmlElement>()
.children();
// click first counter once, second counter twice, etc.
// `NodeList` isn't a `Vec` so we iterate over it in this slightly awkward way
for idx in 0..counters.length() {
let counter = counters.item(idx).unwrap();
let inc_button = counter
.first_child()
.unwrap()
.next_sibling()
.unwrap()
.next_sibling()
.unwrap()
.next_sibling()
.unwrap()
.unchecked_into::<HtmlElement>();
for _ in 0..=idx {
inc_button.click();
}
}
assert_eq!(
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
<DynChild> -->6<!-- </DynChild> --></span> from <span><!-- \
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
<Each> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->1<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->2<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->3<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- </Each> --></ul>"
);
// remove the first counter
counters
.item(0)
.unwrap()
.last_child()
.unwrap()
.unchecked_into::<HtmlElement>()
.click();
assert_eq!(
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
<DynChild> -->5<!-- </DynChild> --></span> from <span><!-- \
<DynChild> -->2<!-- </DynChild> --></span> counters.</p><ul><!-- \
<Each> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->2<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->3<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- </Each> --></ul>"
);
}

View File

@@ -26,7 +26,7 @@ pub fn App(cx: Scope) -> impl IntoView {
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
.collect_view(cx)
}
</ul>
</div>

View File

@@ -10,12 +10,12 @@ crate-type = ["cdylib", "rlib"]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4.17"
serde = { version = "1", features = ["derive"] }
simple_logger = "4.0.0"

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -11,10 +11,10 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::landing::{App, AppProps};
use leptos::{LeptosOptions, view};
use crate::landing::App;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, State(options): State<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();

View File

@@ -1,7 +1,4 @@
use crate::{
error_template::{ErrorTemplate, ErrorTemplateProps},
errors::AppError,
};
use crate::{error_template::ErrorTemplate, errors::AppError};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
@@ -54,7 +51,8 @@ pub fn App(cx: Scope) -> impl IntoView {
#[component]
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
let generate_internal_error = create_server_action::<CauseInternalServerError>(cx);
let generate_internal_error =
create_server_action::<CauseInternalServerError>(cx);
view! { cx,
<p>

View File

@@ -1,5 +1,4 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
@@ -8,6 +7,7 @@ pub mod landing;
// 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 leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::landing::*;

View File

@@ -5,7 +5,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use crate::landing::*;
use axum::body::Body as AxumBody;
use axum::{
extract::{Extension, Path},
extract::{State, Path},
http::Request,
response::{IntoResponse, Response},
routing::{get, post},
@@ -21,7 +21,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
Extension(options): Extension<Arc<LeptosOptions>>,
State(options): State<Arc<LeptosOptions>>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
@@ -37,13 +37,14 @@ async fn custom_handler(
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
simple_logger::init_with_level(log::Level::Debug)
.expect("couldn't initialize logging");
crate::landing::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 leptos_options = Arc::new(conf.leptos_options);
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
@@ -51,9 +52,13 @@ async fn main() {
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
.leptos_routes(
leptos_options.clone(),
routes,
|cx| view! { cx, <App/> },
)
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -14,7 +14,7 @@ pub enum FetchError {
#[error("Error loading data from serving.")]
Request,
#[error("Error deserializaing cat data from request.")]
Json
Json,
}
async fn fetch_cats(count: u32) -> Result<Vec<String>, FetchError> {
@@ -55,7 +55,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
errors
.iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
.collect::<Vec<_>>()
.collect_view(cx)
})
};
@@ -76,7 +76,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
data.map(|data| {
data.iter()
.map(|s| view! { cx, <span>{s}</span> })
.collect::<Vec<_>>()
.collect_view(cx)
})
})
};

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -4,10 +4,7 @@ use leptos_meta::*;
use leptos_router::*;
mod api;
mod routes;
use routes::nav::*;
use routes::stories::*;
use routes::story::*;
use routes::users::*;
use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App(cx: Scope) -> impl IntoView {

View File

@@ -7,7 +7,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use actix_files::{Files};
use actix_web::*;
use hackernews::{App,AppProps};
use hackernews::{App};
use leptos_actix::{LeptosRoutes, generate_route_list};
#[get("/style.css")]
@@ -24,7 +24,7 @@ cfg_if! {
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr.clone();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> });
@@ -37,7 +37,7 @@ cfg_if! {
.service(favicon)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <App/> })
.service(Files::new("/", &site_root))
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
@@ -46,7 +46,7 @@ cfg_if! {
}
} else {
fn main() {
use hackernews::{App, AppProps};
use hackernews::{App};
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -1,8 +1,7 @@
use crate::api;
use leptos::*;
use leptos_router::*;
use crate::api;
fn category(from: &str) -> &'static str {
match from {
"new" => "newest",
@@ -37,8 +36,10 @@ pub fn Stories(cx: Scope) -> impl IntoView {
);
let (pending, set_pending) = create_signal(cx, false);
let hide_more_link =
move |cx| pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
let hide_more_link = move |cx| {
pending()
|| stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28
};
view! {
cx,

View File

@@ -13,11 +13,20 @@ pub fn Story(cx: Scope) -> impl IntoView {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(cx, &api::story(&format!("item/{id}"))).await
api::fetch_api::<api::Story>(
cx,
&api::story(&format!("item/{id}")),
)
.await
}
},
);
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
let meta_description = move || {
story
.read(cx)
.and_then(|story| story.map(|story| story.title))
.unwrap_or_else(|| "Loading story...".to_string())
};
view! { cx,
<>

View File

@@ -31,7 +31,7 @@ pub fn User(cx: Scope) -> impl IntoView {
<li>
<span class="label">"Karma: "</span> {user.karma}
</li>
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
<li inner_html={user.about} class="about"></li>
</ul>
<p class="links">
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,4 +1,4 @@
use leptos::{on_cleanup, Scope, Serializable};
use leptos::{Scope, Serializable};
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
@@ -29,7 +29,7 @@ where
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
on_cleanup(cx, move || {
leptos::on_cleanup(cx, move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
@@ -38,7 +38,7 @@ where
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
pub async fn fetch_api<T>(_cx: Scope, path: &str) -> Option<T>
where
T: Serializable,
{

View File

@@ -1,7 +1,4 @@
use leptos::{
signal_prelude::*, view, Errors, For, ForProps, IntoView, RwSignal, Scope,
View,
};
use leptos::{view, Errors, For, IntoView, RwSignal, Scope, View};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them

View File

@@ -4,7 +4,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -15,13 +15,13 @@ if #[cfg(feature = "ssr")] {
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, State(options): State<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()
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
handler(req).await.into_response()

View File

@@ -7,10 +7,7 @@ pub mod error_template;
pub mod fallback;
pub mod handlers;
mod routes;
use routes::nav::*;
use routes::stories::*;
use routes::story::*;
use routes::users::*;
use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App(cx: Scope) -> impl IntoView {

View File

@@ -7,7 +7,6 @@ if #[cfg(feature = "ssr")] {
use axum::{
Router,
routing::get,
extract::Extension,
};
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
@@ -18,8 +17,8 @@ if #[cfg(feature = "ssr")] {
use hackernews_axum::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr.clone();
let leptos_options = Arc::new(conf.leptos_options);
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
@@ -29,7 +28,7 @@ if #[cfg(feature = "ssr")] {
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -1,4 +1,4 @@
use leptos::{component, Scope, IntoView, view};
use leptos::{component, view, IntoView, Scope};
use leptos_router::*;
#[component]

View File

@@ -1,8 +1,7 @@
use crate::api;
use leptos::*;
use leptos_router::*;
use crate::api;
fn category(from: &str) -> &'static str {
match from {
"new" => "newest",
@@ -37,8 +36,10 @@ pub fn Stories(cx: Scope) -> impl IntoView {
);
let (pending, set_pending) = create_signal(cx, false);
let hide_more_link =
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
let hide_more_link = move || {
pending()
|| stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28
};
view! {
cx,

View File

@@ -13,11 +13,20 @@ pub fn Story(cx: Scope) -> impl IntoView {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(cx, &api::story(&format!("item/{id}"))).await
api::fetch_api::<api::Story>(
cx,
&api::story(&format!("item/{id}")),
)
.await
}
},
);
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
let meta_description = move || {
story
.read(cx)
.and_then(|story| story.map(|story| story.title))
.unwrap_or_else(|| "Loading story...".to_string())
};
view! { cx,
<>

View File

@@ -1,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -0,0 +1 @@
extend = { path = "../../cargo-make/common.toml" }

View File

@@ -0,0 +1 @@
extend = { path = "../../cargo-make/common.toml" }

View File

@@ -1,9 +1,8 @@
use api_boundary::*;
use gloo_net::http::{Request, Response};
use serde::de::DeserializeOwned;
use thiserror::Error;
use api_boundary::*;
#[derive(Clone, Copy)]
pub struct UnauthorizedApi {
url: &'static str,

View File

@@ -20,54 +20,56 @@ pub fn CredentialsForm(
});
view! { cx,
<form on:submit=|ev|ev.prevent_default()>
<p>{ title }</p>
{move || error.get().map(|err| view!{ cx,
<p style ="color:red;" >{ err }</p>
})}
<input
type = "email"
required
placeholder = "Email address"
prop:disabled = move || disabled.get()
on:keyup = move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev);
set_email.update(|v|*v = val);
}
// The `change` event fires when the browser fills the form automatically,
on:change = move |ev| {
let val = event_target_value(&ev);
set_email.update(|v|*v = val);
}
/>
<input
type = "password"
required
placeholder = "Password"
prop:disabled = move || disabled.get()
on:keyup = move |ev: ev::KeyboardEvent| {
match &*ev.key() {
"Enter" => {
dispatch_action();
<form on:submit=|ev| ev.prevent_default()>
<p>{title}</p>
{move || {
error
.get()
.map(|err| {
view! { cx, <p style="color:red;">{err}</p> }
})
}}
<input
type="email"
required
placeholder="Email address"
prop:disabled=move || disabled.get()
on:keyup=move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev);
set_email.update(|v| *v = val);
}
_=> {
let val = event_target_value(&ev);
set_password.update(|p|*p = val);
on:change=move |ev| {
let val = event_target_value(&ev);
set_email.update(|v| *v = val);
}
}
}
// The `change` event fires when the browser fills the form automatically,
on:change = move |ev| {
let val = event_target_value(&ev);
set_password.update(|p|*p = val);
}
/>
<button
prop:disabled = move || button_is_disabled.get()
on:click = move |_| dispatch_action()
>
{ action_label }
</button>
</form>
/>
<input
type="password"
required
placeholder="Password"
prop:disabled=move || disabled.get()
on:keyup=move |ev: ev::KeyboardEvent| {
match &*ev.key() {
"Enter" => {
dispatch_action();
}
_ => {
let val = event_target_value(&ev);
set_password.update(|p| *p = val);
}
}
}
on:change=move |ev| {
let val = event_target_value(&ev);
set_password.update(|p| *p = val);
}
/>
<button
prop:disabled=move || button_is_disabled.get()
on:click=move |_| dispatch_action()
>
{action_label}
</button>
</form>
}
}

View File

@@ -1,8 +1,7 @@
use crate::Page;
use leptos::*;
use leptos_router::*;
use crate::Page;
#[component]
pub fn NavBar<F>(
cx: Scope,
@@ -13,20 +12,27 @@ where
F: Fn() + 'static + Clone,
{
view! { cx,
<nav>
<Show
when = move || logged_in.get()
fallback = |cx| view! { cx,
<A href=Page::Login.path() >"Login"</A>
" | "
<A href=Page::Register.path() >"Register"</A>
}
>
<a href="#" on:click={
let on_logout = on_logout.clone();
move |_| on_logout()
}>"Logout"</a>
</Show>
</nav>
<nav>
<Show
when=move || logged_in.get()
fallback=|cx| {
view! { cx,
<A href=Page::Login.path()>"Login"</A>
" | "
<A href=Page::Register.path()>"Register"</A>
}
}
>
<a
href="#"
on:click={
let on_logout = on_logout.clone();
move |_| on_logout()
}
>
"Logout"
</a>
</Show>
</nav>
}
}

View File

@@ -1,9 +1,8 @@
use api_boundary::*;
use gloo_storage::{LocalStorage, Storage};
use leptos::*;
use leptos_router::*;
use api_boundary::*;
mod api;
mod components;
mod pages;
@@ -86,45 +85,51 @@ pub fn App(cx: Scope) -> impl IntoView {
.expect("LocalStorage::set");
}
None => {
log::debug!("API is no longer authorized: delete token from LocalStorage");
log::debug!(
"API is no longer authorized: delete token from \
LocalStorage"
);
LocalStorage::delete(API_TOKEN_STORAGE_KEY);
}
}
});
view! { cx,
<Router>
<NavBar logged_in on_logout />
<main>
<Routes>
<Route
path=Page::Home.path()
view=move |cx| view! { cx,
<Home user_info = user_info.into() />
}
/>
<Route
path=Page::Login.path()
view=move |cx| view! { cx,
<Login
api = unauthorized_api
on_success = move |api| {
log::info!("Successfully logged in");
authorized_api.update(|v| *v = Some(api));
let navigate = use_navigate(cx);
navigate(Page::Home.path(), Default::default()).expect("Home route");
fetch_user_info.dispatch(());
} />
}
/>
<Route
path=Page::Register.path()
view=move |cx| view! { cx,
<Register api = unauthorized_api />
}
/>
</Routes>
</main>
</Router>
<Router>
<NavBar logged_in on_logout/>
<main>
<Routes>
<Route
path=Page::Home.path()
view=move |cx| {
view! { cx, <Home user_info=user_info.into()/> }
}
/>
<Route
path=Page::Login.path()
view=move |cx| {
view! { cx,
<Login
api=unauthorized_api
on_success=move |api| {
log::info!("Successfully logged in");
authorized_api.update(|v| *v = Some(api));
let navigate = use_navigate(cx);
navigate(Page::Home.path(), Default::default()).expect("Home route");
fetch_user_info.dispatch(());
}
/>
}
}
/>
<Route
path=Page::Register.path()
view=move |cx| {
view! { cx, <Register api=unauthorized_api/> }
}
/>
</Routes>
</main>
</Router>
}
}

View File

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

View File

@@ -6,15 +6,19 @@ use leptos_router::*;
#[component]
pub fn Home(cx: Scope, user_info: Signal<Option<UserInfo>>) -> impl IntoView {
view! { cx,
<h2>"Leptos Login example"</h2>
{move || match user_info.get() {
Some(info) => view!{ cx,
<p>"You are logged in with "{ info.email }"."</p>
}.into_view(cx),
None => view!{ cx,
<p>"You are not logged in."</p>
<A href=Page::Login.path() >"Login now."</A>
}.into_view(cx)
}}
<h2>"Leptos Login example"</h2>
{move || match user_info.get() {
Some(info) => {
view! { cx, <p>"You are logged in with " {info.email} "."</p> }
.into_view(cx)
}
None => {
view! { cx,
<p>"You are not logged in."</p>
<A href=Page::Login.path()>"Login now."</A>
}
.into_view(cx)
}
}}
}
}

View File

@@ -1,13 +1,11 @@
use leptos::*;
use leptos_router::*;
use api_boundary::*;
use crate::{
api::{self, AuthorizedApi, UnauthorizedApi},
components::credentials::*,
Page,
};
use api_boundary::*;
use leptos::*;
use leptos_router::*;
#[component]
pub fn Login<F>(cx: Scope, api: UnauthorizedApi, on_success: F) -> impl IntoView
@@ -53,14 +51,14 @@ where
let disabled = Signal::derive(cx, move || wait_for_response.get());
view! { cx,
<CredentialsForm
title = "Please login to your account"
action_label = "Login"
action = login_action
error = login_error.into()
disabled
/>
<p>"Don't have an account?"</p>
<A href=Page::Register.path()>"Register"</A>
<CredentialsForm
title="Please login to your account"
action_label="Login"
action=login_action
error=login_error.into()
disabled
/>
<p>"Don't have an account?"</p>
<A href=Page::Register.path()>"Register"</A>
}
}

View File

@@ -1,13 +1,11 @@
use leptos::*;
use leptos_router::*;
use api_boundary::*;
use crate::{
api::{self, UnauthorizedApi},
components::credentials::*,
Page,
};
use api_boundary::*;
use leptos::*;
use leptos_router::*;
#[component]
pub fn Register(cx: Scope, api: UnauthorizedApi) -> impl IntoView {
@@ -52,26 +50,24 @@ pub fn Register(cx: Scope, api: UnauthorizedApi) -> impl IntoView {
let disabled = Signal::derive(cx, move || wait_for_response.get());
view! { cx,
<Show
when = move || register_response.get().is_some()
fallback = move |_| view!{ cx,
<CredentialsForm
title = "Please enter the desired credentials"
action_label = "Register"
action = register_action
error = register_error.into()
disabled
/>
<p>"Your already have an account?"</p>
<A href=Page::Login.path()>"Login"</A>
}
>
<p>"You have successfully registered."</p>
<p>
"You can now "
<A href=Page::Login.path()>"login"</A>
" with your new account."
</p>
</Show>
<Show
when=move || register_response.get().is_some()
fallback=move |_| {
view! { cx,
<CredentialsForm
title="Please enter the desired credentials"
action_label="Register"
action=register_action
error=register_error.into()
disabled
/>
<p>"Your already have an account?"</p>
<A href=Page::Login.path()>"Login"</A>
}
}
>
<p>"You have successfully registered."</p>
<p>"You can now " <A href=Page::Login.path()>"login"</A> " with your new account."</p>
</Show>
}
}

View File

@@ -0,0 +1 @@
extend = { path = "../../cargo-make/common.toml" }

View File

@@ -2,8 +2,7 @@ use crate::{application::*, Error};
use api_boundary as json;
use axum::{
http::StatusCode,
response::Json,
response::{IntoResponse, Response},
response::{IntoResponse, Json, Response},
};
use thiserror::Error;

View File

@@ -1,5 +1,4 @@
use std::{env, sync::Arc};
use api_boundary as json;
use axum::{
extract::{State, TypedHeader},
headers::{authorization::Bearer, Authorization},
@@ -8,10 +7,9 @@ use axum::{
routing::{get, post},
Router,
};
use std::{env, sync::Arc};
use tower_http::cors::{Any, CorsLayer};
use api_boundary as json;
mod adapters;
mod application;
@@ -25,7 +23,10 @@ async fn main() -> anyhow::Result<()> {
env::set_var("RUST_LOG", "debug");
}
env::VarError::NotUnicode(_) => {
return Err(anyhow::anyhow!("The value of 'RUST_LOG' does not contain valid unicode data."));
return Err(anyhow::anyhow!(
"The value of 'RUST_LOG' does not contain valid unicode \
data."
));
}
}
}

View File

@@ -1,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -97,7 +97,7 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
<li><A href=contact.id.to_string()><span>{&contact.first_name} " " {&contact.last_name}</span></A></li>
}
})
.collect::<Vec<_>>()
.collect_view(cx)
})
};
@@ -107,7 +107,7 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>
{move || view! { cx, <ul>{contacts}</ul>}}
</Suspense>
<AnimatedOutlet
<AnimatedOutlet
class="outlet"
outro="fadeOut"
intro="fadeIn"

View File

@@ -1,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,9 +1,7 @@
use std::collections::HashSet;
use cfg_if::cfg_if;
use leptos::*;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
cfg_if! {
if #[cfg(feature = "ssr")] {
@@ -168,7 +166,9 @@ pub async fn login(
.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()))? {
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());

View File

@@ -13,7 +13,9 @@ impl TodoAppError {
pub fn status_code(&self) -> StatusCode {
match self {
TodoAppError::NotFound => StatusCode::NOT_FOUND,
TodoAppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
TodoAppError::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}

View File

@@ -4,7 +4,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -13,16 +13,16 @@ if #[cfg(feature = "ssr")] {
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::error_template::ErrorTemplate;
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, State(options): State<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()
res.into_response()
} else{
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);

View File

@@ -5,8 +5,8 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
response::{Response, IntoResponse},
routing::{post, get},
extract::{Path, Extension, RawQuery},
routing::get,
extract::{Path, State, Extension, RawQuery},
http::{Request, header::HeaderMap},
body::Body as AxumBody,
Router,
@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
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 leptos::{log, view, provide_context, LeptosOptions, get_configuration};
use std::sync::Arc;
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};
@@ -33,7 +33,7 @@ if #[cfg(feature = "ssr")] {
}, request).await
}
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, State(options): State<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());
@@ -68,7 +68,7 @@ if #[cfg(feature = "ssr")] {
// 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 leptos_options = Arc::new(conf.leptos_options);
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
@@ -80,8 +80,8 @@ if #[cfg(feature = "ssr")] {
.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));
.layer(Extension(pool))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -1,5 +1,4 @@
use crate::auth::*;
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::{auth::*, error_template::ErrorTemplate};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
@@ -21,15 +20,15 @@ if #[cfg(feature = "ssr")] {
use sqlx::SqlitePool;
pub fn pool(cx: Scope) -> Result<SqlitePool, ServerFnError> {
Ok(use_context::<SqlitePool>(cx)
use_context::<SqlitePool>(cx)
.ok_or("Pool missing.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
pub fn auth(cx: Scope) -> Result<AuthSession, ServerFnError> {
Ok(use_context::<AuthSession>(cx)
use_context::<AuthSession>(cx)
.ok_or("Auth session missing.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
pub fn register_server_functions() {
@@ -73,7 +72,8 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
let pool = pool(cx)?;
let mut todos = Vec::new();
let mut rows = sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
let mut rows =
sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
while let Some(row) = rows
.try_next()
@@ -111,11 +111,13 @@ pub async fn add_todo(cx: Scope, title: String) -> Result<(), ServerFnError> {
// 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
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())),
@@ -241,11 +243,11 @@ pub fn Todos(cx: Scope) -> impl IntoView {
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
}
Ok(todos) => {
if todos.is_empty() {
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
} else {
todos
.into_iter()
@@ -266,9 +268,8 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</ActionForm>
</li>
}
.into_any()
})
.collect::<Vec<_>>()
.collect_view(cx)
}
}
})
@@ -287,7 +288,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect::<Vec<_>>()
.collect_view(cx)
};
view! {
@@ -305,7 +306,10 @@ pub fn Todos(cx: Scope) -> impl IntoView {
}
#[component]
pub fn Login(cx: Scope, action: Action<Login, Result<(), ServerFnError>>) -> impl IntoView {
pub fn Login(
cx: Scope,
action: Action<Login, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
cx,
<ActionForm action=action>
@@ -331,7 +335,10 @@ pub fn Login(cx: Scope, action: Action<Login, Result<(), ServerFnError>>) -> imp
}
#[component]
pub fn Signup(cx: Scope, action: Action<Signup, Result<(), ServerFnError>>) -> impl IntoView {
pub fn Signup(
cx: Scope,
action: Action<Signup, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
cx,
<ActionForm action=action>
@@ -363,7 +370,10 @@ pub fn Signup(cx: Scope, action: Action<Signup, Result<(), ServerFnError>>) -> i
}
#[component]
pub fn Logout(cx: Scope, action: Action<Logout, Result<(), ServerFnError>>) -> impl IntoView {
pub fn Logout(
cx: Scope,
action: Action<Logout, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
cx,
<div id="loginbox">

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -44,7 +44,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
.map(|posts| {
posts.iter()
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a></li>})
.collect::<Vec<_>>()
.collect_view(cx)
})
)
};
@@ -109,7 +109,7 @@ fn Post(cx: Scope) -> impl IntoView {
{move || errors.get()
.into_iter()
.map(|(_, error)| view! { cx, <li>{error.to_string()} </li> })
.collect::<Vec<_>>()
.collect_view(cx)
}
</ul>
</div>

View File

@@ -12,8 +12,8 @@ async fn main() -> std::io::Result<()> {
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> });
GetPost::register();
ListPostMetadata::register();
let _ = GetPost::register();
let _ = ListPostMetadata::register();
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -49,7 +49,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
.map(|posts| {
posts.iter()
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a> "|" <a href=format!("/post_in_order/{}", post.id)>{&post.title}"(in order)"</a></li>})
.collect::<Vec<_>>()
.collect_view(cx)
})
)
};
@@ -114,7 +114,7 @@ fn Post(cx: Scope) -> impl IntoView {
{move || errors.get()
.into_iter()
.map(|(_, error)| view! { cx, <li>{error.to_string()} </li> })
.collect::<Vec<_>>()
.collect_view(cx)
}
</ul>
</div>

View File

@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -11,16 +11,16 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::app::{App, AppProps};
use leptos::{LeptosOptions, view};
use crate::app::App;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, State(options): State<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()
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),

View File

@@ -1,11 +1,7 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{
extract::{Extension, Path},
routing::{get, post},
Router,
};
use axum::{routing::post, Router};
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
@@ -13,12 +9,12 @@ async fn main() {
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
let leptos_options = Arc::new(conf.leptos_options);
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
GetPost::register();
ListPostMetadata::register();
let _ = GetPost::register();
let _ = ListPostMetadata::register();
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
@@ -28,7 +24,7 @@ async fn main() {
|cx| view! { cx, <App/> },
)
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -20,7 +20,7 @@ cfg_if! {
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr.clone();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> });
@@ -32,7 +32,7 @@ cfg_if! {
App::new()
.service(css)
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <App/> })
.service(Files::new("/", &site_root))
.service(Files::new("/", site_root))
.wrap(middleware::Compress::default())
})
.bind(&addr)?

View File

@@ -5,7 +5,7 @@ This is a template demonstrating how to integrate [TailwindCSS](https://tailwind
Install Tailwind and build the CSS:
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
`Trunk.toml` is configured to build the CSS automatically.
Install trunk to client side render this bundle.

View File

@@ -0,0 +1,4 @@
[[hooks]]
stage = "pre_build"
command = "sh"
command_arguments = ["-c", "npx tailwindcss -i input.css -o style/output.css"]

View File

@@ -1,5 +1,5 @@
/*
! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com
*/
/*
@@ -31,6 +31,7 @@
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.
6. Use the user's configured `sans` font-variation-settings by default.
*/
html {
@@ -47,6 +48,8 @@ html {
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
}
/*
@@ -433,6 +436,9 @@ video {
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
@@ -480,6 +486,9 @@ video {
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;

View File

@@ -1,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,10 +1,10 @@
use cfg_if::cfg_if;
use leptos::*;
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 leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;

View File

@@ -29,7 +29,7 @@ cfg_if! {
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr.clone();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> });
@@ -43,7 +43,7 @@ cfg_if! {
.service(css)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <TodoApp/> })
.service(Files::new("/", &site_root))
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(addr)?

View File

@@ -9,7 +9,7 @@ cfg_if! {
use sqlx::{Connection, SqliteConnection};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
pub fn register_server_functions() {
@@ -37,18 +37,18 @@ cfg_if! {
#[server(GetTodos, "/api")]
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
let req =
use_context::<actix_web::HttpRequest>(cx);
if let Some(req) = req{
println!("req.path = {:#?}", req.path());
let req = use_context::<actix_web::HttpRequest>(cx);
if let Some(req) = req {
println!("req.path = {:#?}", req.path());
}
use futures::TryStreamExt;
let mut conn = db().await?;
let mut todos = Vec::new();
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
let mut rows =
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await
@@ -73,7 +73,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
.execute(&mut conn)
.await
{
Ok(row) => Ok(()),
Ok(_row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
}
}
@@ -130,7 +130,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
cx,
<div>
<MultiActionForm
// we can handle client-side validation in the on:submit event
// 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| {
@@ -176,8 +176,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</li>
}
})
.collect::<Vec<_>>()
.into_view(cx)
.collect_view(cx)
}
}
})
@@ -196,7 +195,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect::<Vec<_>>()
.collect_view(cx)
};
view! {

View File

@@ -1,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -4,7 +4,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -13,16 +13,16 @@ if #[cfg(feature = "ssr")] {
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::error_template::ErrorTemplate;
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, State(options): State<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()
res.into_response()
} else{
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);

View File

@@ -1,5 +1,4 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
@@ -8,6 +7,7 @@ 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 leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;

View File

@@ -1,11 +1,11 @@
use cfg_if::cfg_if;
use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
if #[cfg(feature = "ssr")] {
use leptos::*;
use axum::{
routing::{post, get},
extract::{Extension, Path},
extract::{State, Path},
http::Request,
response::{IntoResponse, Response},
Router,
@@ -18,7 +18,7 @@ if #[cfg(feature = "ssr")] {
use std::sync::Arc;
//Define a handler to test extractor with state
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
async fn custom_handler(Path(id): Path<String>, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
move |cx| {
provide_context(cx, id.clone());
@@ -32,7 +32,7 @@ if #[cfg(feature = "ssr")] {
async fn main() {
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
let conn = db().await.expect("couldn't connect to DB");
let _conn = db().await.expect("couldn't connect to DB");
/* sqlx::migrate!()
.run(&mut conn)
.await
@@ -42,7 +42,7 @@ if #[cfg(feature = "ssr")] {
// 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 leptos_options = Arc::new(conf.leptos_options);
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
@@ -52,7 +52,7 @@ if #[cfg(feature = "ssr")] {
.route("/special/:id", get(custom_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -1,4 +1,4 @@
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::error_template::ErrorTemplate;
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
@@ -229,8 +229,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</li>
}
})
.collect::<Vec<_>>()
.into_view(cx)
.collect_view(cx)
}
}
})
@@ -249,7 +248,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect::<Vec<_>>()
.collect_view(cx)
};
view! {

View File

@@ -1,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use crate::{
error_template::{ErrorTemplate, ErrorTemplateProps},
error_template::ErrorTemplate,
errors::TodoAppError,
};
use http::Uri;
@@ -22,7 +22,7 @@ if #[cfg(feature = "ssr")] {
Error::Responder(Response::text("missing state type LeptosOptions")),
)?;
let root = &options.site_root;
let resp = get_static_file(uri, &root, headers, route_info).await?;
let resp = get_static_file(uri, root, headers, route_info).await?;
let status = resp.status();
if status.is_success() || status.is_redirection() {

View File

@@ -1,5 +1,4 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
@@ -8,6 +7,7 @@ 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 leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;

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