Compare commits

..

120 Commits

Author SHA1 Message Date
Greg Johnston
a94e739725 Small changes to fix CI and clean up 2022-11-14 22:19:21 -05:00
Ben Wishovich
067b52f731 Fix missing import that is needed sometime 2022-11-14 17:18:11 -08:00
Ben Wishovich
40ed424116 Add denylist for optional Cargo features 2022-11-14 16:57:08 -08:00
Ben Wishovich
9c59564e16 Update cfg to make CI happy and remove some extra imports 2022-11-14 16:08:34 -08:00
Ben Wishovich
11f375fdaa Fix another booboo in js imports 2022-11-14 15:02:58 -08:00
Ben Wishovich
bf301c2266 Fix issue with old import 2022-11-14 14:57:47 -08:00
Ben Wishovich
107f7c05c6 Fix README typo 2022-11-14 14:56:12 -08:00
Ben Wishovich
e9c1846470 counter-isomorphic mostly works now 2022-11-14 14:52:05 -08:00
Ben Wishovich
b9e0255016 Commit working version of Router with render bug 2022-11-14 13:45:22 -08:00
Ben Wishovich
74b2889e8a Add provide_context to router example 2022-11-14 13:37:58 -08:00
Ben Wishovich
482c84dc73 Add README to router example 2022-11-14 13:19:11 -08:00
Ben Wishovich
8c0385d94c Add context to router example 2022-11-14 13:17:56 -08:00
Ben Wishovich
1c81337024 Merge branch 'main' into example-improvements 2022-11-14 13:17:39 -08:00
Ben Wishovich
992983efd9 Commit WIP version of isomorphic counter 2022-11-14 12:04:26 -08:00
Ben Wishovich
d8c2cab64d Fix crate name 2022-11-14 09:15:56 -08:00
Ben Wishovich
0f2715290c Updated hacker news example to new SFA format and added a README 2022-11-14 09:03:44 -08:00
Greg Johnston
22eaa92355 Use serde_urlencoded for server functions (making it easier to use normal text inputs for forms) 2022-11-14 08:18:01 -05:00
Greg Johnston
f8de0fff81 Allow prefixes for server function routes 2022-11-14 07:21:05 -05:00
Greg Johnston
d9f07111e0 Address issue #69 by adding caller filename to server fn URLs 2022-11-13 20:42:56 -05:00
Greg Johnston
d4da7e0c25 Allow returning <template> from view 2022-11-13 15:58:49 -05:00
Greg Johnston
876aa0f0f4 Fixing ch04 reactivity example (incomplete) 2022-11-13 09:25:22 -05:00
Greg Johnston
05f635f4ac Don't require specific subminor versions in dev-dependencies 2022-11-13 09:04:25 -05:00
Greg Johnston
ba3156c878 0.0.16 2022-11-13 09:02:04 -05:00
Greg Johnston
e24fb3b294 Ongoing work on book 2022-11-13 08:58:39 -05:00
Greg Johnston
c347e85de7 Fix doctest to reflect the fact that create_effect doesn't run in SSR mode 2022-11-13 07:08:42 -05:00
Greg Johnston
a298bc73dd Fix hydration example 2022-11-12 21:57:37 -05:00
Greg Johnston
2c894f6a1d Remove tests for dash prefixes now that they're removed 2022-11-12 20:40:40 -05:00
Greg Johnston
50cc2f5eac Don't run DOM functions in doctests 2022-11-12 20:16:21 -05:00
Greg Johnston
aacc2fc902 Merge pull request #75 from gbj/update-syn-rsx
Update to `syn-rsx` 0.9
2022-11-12 20:13:32 -05:00
Greg Johnston
48e934cd40 Remove references to dash-pattern in docs 2022-11-12 20:13:12 -05:00
Greg Johnston
8d67aa1ff0 Fix issues when rendering adjacent blocks/text 2022-11-12 20:03:16 -05:00
Greg Johnston
0920cc0cef Remove extraneous code from counter example 2022-11-12 19:38:20 -05:00
Greg Johnston
12fc1ca7a1 Clear warnings 2022-11-12 19:30:47 -05:00
Greg Johnston
a22dc69729 Prevent Suspense test from running in SSR (because no local Tokio thread found) 2022-11-12 15:17:30 -05:00
Greg Johnston
f5ae5f4fff Final TodoMVC fix 2022-11-12 08:39:30 -05:00
Greg Johnston
8042a7002b Add fake event listeners for SSR again 2022-11-12 07:48:59 -05:00
Greg Johnston
c7826e0bc9 Continuing work 2022-11-12 07:47:57 -05:00
Greg Johnston
19ac14cf62 Figuring out the right way to handle refs/event listeners in SSR mode is hard... 2022-11-12 07:37:07 -05:00
Greg Johnston
8f88b50d34 Merge branch 'main' of https://github.com/gbj/leptos 2022-11-12 07:34:44 -05:00
Greg Johnston
281b303c80 Initial work to upgrade to 0.9 2022-11-12 07:30:37 -05:00
Greg Johnston
8315cb2dd7 Remove excluded Gtk example 2022-11-12 06:48:38 -05:00
Greg Johnston
3fe1c6ccda Merge pull request #61 from gbj/doc-patrol
Adding missing docs
2022-11-12 06:40:14 -05:00
Greg Johnston
7c8ffa9314 Merge pull request #70 from gbj/todomvc-fix
Fix toggle_all panic
2022-11-11 22:21:53 -05:00
Greg Johnston
0b5657564d Restore but hide ssr_event_listener 2022-11-11 22:17:09 -05:00
Greg Johnston
0f89e64eda Hide ssr_event_listener 2022-11-11 22:13:26 -05:00
Greg Johnston
33bbfa6f75 Merge branch 'main' into doc-patrol 2022-11-11 22:02:39 -05:00
Greg Johnston
7832b59cdd Fix pub here 2022-11-11 21:57:51 -05:00
Greg Johnston
ebf4d1308b #![deny(missing_docs)] on main package 2022-11-11 21:49:26 -05:00
Greg Johnston
0e093d2c23 Complete leptos_dom docs 2022-11-11 21:37:46 -05:00
Greg Johnston
f093a24d1a Consistency in new SSR choices 2022-11-11 21:08:08 -05:00
Greg Johnston
07e6b361e1 Fix errors that can arise from using JsCast on Element types in SSR mode 2022-11-11 19:56:30 -05:00
Greg Johnston
183745f319 Class docs 2022-11-11 17:03:54 -05:00
Greg Johnston
9a59c371fd Docs for all DOM operation wrappers 2022-11-10 22:09:28 -05:00
Greg Johnston
75354517bf Fix failing test 2022-11-10 21:45:54 -05:00
Greg Johnston
01807ea514 Fix toggle_all panic 2022-11-10 21:43:40 -05:00
Greg Johnston
e19dd0a226 Fix tests 2022-11-10 07:45:58 -05:00
Greg Johnston
8e20ab28e0 leptos_dom docs 2022-11-10 07:40:16 -05:00
Greg Johnston
3dfcf99a4c Unused file 2022-11-10 07:31:52 -05:00
Greg Johnston
1e04442f97 Fix broken links in docs 2022-11-10 07:29:22 -05:00
Greg Johnston
7799479364 Begin leptos_dom docs 2022-11-10 07:27:08 -05:00
Greg Johnston
c23bc0ef90 Add server functions to main docs 2022-11-09 21:20:14 -05:00
Greg Johnston
e3c1291942 Merge branch 'main' of https://github.com/gbj/leptos 2022-11-09 07:47:43 -05:00
Greg Johnston
3d0a8f574e Fix misplaced ref in TodoMVC 2022-11-09 07:47:17 -05:00
Greg Johnston
3730427789 Fix misplaced ref in TodoMVC 2022-11-09 07:47:05 -05:00
Greg Johnston
f94be99246 Merge pull request #67 from dglsparsons/main
Amend docs on feature flags
2022-11-09 07:38:47 -05:00
Douglas Parsons
7b7ff492fc Amend docs on feature flags
It looked a bit confusing that there were 3 `serde` feature flags and
all of them were defaults.
2022-11-09 08:03:45 +00:00
Greg Johnston
ba158bbd0f Merge pull request #66 from gbj/fix-immediate-redirect-panic
Fix immediate redirect panic
2022-11-08 23:41:32 -05:00
Greg Johnston
7352151744 Update README to clarify "isomorphic" and "Web" 2022-11-08 22:26:58 -05:00
Greg Johnston
3ad2129a4c Remove unused import 2022-11-08 22:00:25 -05:00
Greg Johnston
4e4b513c1b Fix panic if you redirect immediately in a route component 2022-11-08 21:59:25 -05:00
Greg Johnston
21d73463b0 Merge pull request #62 from gbj/fix-mixed-block-and-element
Fix issues with mixing blocks and elements
2022-11-08 11:39:33 -05:00
Greg Johnston
d5554082f9 More accurate description of r/w segregation in README 2022-11-08 09:20:17 -05:00
Greg Johnston
8ff7b4c11b #![deny(missing_docs)] on leptos_server 2022-11-08 07:30:09 -05:00
Greg Johnston
0e313b3938 Fix tests in leptos_core 2022-11-08 07:26:08 -05:00
Greg Johnston
92f4ea5888 Fixes issue #60 2022-11-08 07:15:26 -05:00
Greg Johnston
acd20a24ac Add leptos_core docs 2022-11-07 21:49:34 -05:00
Greg Johnston
63a2199405 Oops this was an accident 2022-11-07 21:26:57 -05:00
Greg Johnston
9da3c66683 #![deny(missing_docs)] on leptos_reactive 2022-11-07 21:16:44 -05:00
Greg Johnston
6b82a37dea Merge pull request #58 from gbj/fix-component-and-element-order
Fix the out-of-order component/element rendering in #53.
2022-11-06 22:22:47 -05:00
Greg Johnston
9edd8a3c74 Merge pull request #56 from gbj/fix-non-bubbling-events
Fix `focus`, `blur`, and other non-bubbling events
2022-11-06 22:09:21 -05:00
Greg Johnston
33fdc3eae1 Fix leptos important for doctests with on: 2022-11-06 20:45:50 -05:00
Greg Johnston
10e01bf989 Remove logs I reintroduced 2022-11-06 20:43:05 -05:00
Greg Johnston
49820ccba6 This should fix the out-of-order component/element rendering in #53. 2022-11-06 20:37:09 -05:00
Greg Johnston
36be004ef2 Avoid manual delegation for all the DOM events that don't bubble by default. (This is technically too conservative, as one or two of these only don't bubble on certain elements, but it's simpler than passing in the element name and only a very small deopt in those cases.) 2022-11-06 20:00:35 -05:00
Greg Johnston
b9ca0b11a2 Fix breaking CI on leptos_server 2022-11-06 07:08:57 -05:00
Greg Johnston
296e27cd4a Add notes on types that can be accepted as attributes. 2022-11-06 06:54:20 -05:00
Greg Johnston
fd3443b129 Fix TodoMVC example 2022-11-05 23:27:36 -04:00
Greg Johnston
aa3dd356c1 Fix issues with action integration with forms 2022-11-05 22:47:33 -04:00
Greg Johnston
35ca30fbab 0.0.14 2022-11-05 22:39:06 -04:00
Greg Johnston
132f0839c6 Fix 0.0.13 leptos_core 2022-11-05 22:37:29 -04:00
Greg Johnston
e9c1799a11 0.0.13 2022-11-05 22:24:59 -04:00
Greg Johnston
4577313cca Include create_action in root docs 2022-11-05 22:24:54 -04:00
Greg Johnston
f75d49fe4c Router 0.0.2 2022-11-05 22:23:12 -04:00
Greg Johnston
6a38375c66 Complete docs 2022-11-05 22:18:01 -04:00
Greg Johnston
f9f4fb0fef Remove partial create_action docs from crate level 2022-11-05 22:14:28 -04:00
Greg Johnston
42cd3f1d69 Make sure server-only stuff appears in docs 2022-11-05 22:14:19 -04:00
Greg Johnston
ade2eda26d Add docs for leptos_server 2022-11-05 20:08:03 -04:00
Greg Johnston
680b6ecc20 Remove todomvc-ssr from workspace 2022-11-05 19:55:58 -04:00
Greg Johnston
4e9d0354c6 Use localStorage for initial state of todo list 2022-11-05 19:48:12 -04:00
Greg Johnston
cccd7068f9 Remove : I have bigger plans for this 2022-11-05 19:47:17 -04:00
Greg Johnston
8f56a52615 Simplify and add comments on TodoMVC 2022-11-05 19:14:45 -04:00
Greg Johnston
6c04e91088 Fix broken class: and prop: 2022-11-05 19:11:02 -04:00
Greg Johnston
9ef350c2d6 Merge pull request #50 from gbj/remove-loaders 2022-11-05 10:25:19 -04:00
Greg Johnston
f559d47714 Merge pull request #49 from gbj/router-docs 2022-11-05 10:24:56 -04:00
Greg Johnston
2483616d0d Remove route data loaders for now. 2022-11-05 09:32:12 -04:00
Greg Johnston
2595ffe10e Fix doctests 2022-11-05 09:22:02 -04:00
Greg Johnston
221cdf2685 Doc updates, cleanups 2022-11-05 09:12:42 -04:00
Greg Johnston
1cb278f520 Merge pull request #48 from mrjoe7/actions
Add `test` action configuration #19
2022-11-04 21:10:42 -04:00
Tomas Sedlak
5cfd44474d Add test action configuration 2022-11-05 00:14:30 +01:00
Greg Johnston
bd652ec542 Adding docs 2022-11-04 16:50:03 -04:00
Greg Johnston
d8852f909e Remove action 2022-11-03 21:31:32 -04:00
Greg Johnston
e16cc4fc4a Remove actions 2022-11-03 21:11:59 -04:00
Greg Johnston
d5e3661bcf Remove actions (moved to leptos_server) 2022-11-03 21:07:05 -04:00
Greg Johnston
8873ddc40a Require docs 2022-11-03 21:06:46 -04:00
Greg Johnston
b7e2e983f0 Update main docs 2022-11-03 21:06:23 -04:00
Greg Johnston
3701f65693 Add missing leptos_server metadata 2022-11-03 20:05:44 -04:00
Greg Johnston
a5712d3e17 0.0.12 2022-11-03 20:00:26 -04:00
Greg Johnston
4fba035f19 Merge pull request #47 from gbj/allow-on-dash-syntax-in-macro
Allow on-, class-, prop-, and attr- as equivalent to on:, class:, pro…
2022-11-03 19:57:56 -04:00
Greg Johnston
47fad9a042 Allow on-, class-, prop-, and attr- as equivalent to on:, class:, prop:, and attr: to get around a syn-rsx parsing limitation on mixing colons and dashes in an attribute name 2022-11-03 19:57:27 -04:00
Greg Johnston
c8545f47cb Enable cargo make build and cargo make test by removing mutually exclusive features
Remove mutually exclusive features
2022-11-03 18:32:09 -04:00
125 changed files with 2661 additions and 1721 deletions

51
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- name: Cargo cache
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ matrix.rust }}-${{ hashFiles('**/Cargo.lock') }}
- name: Run tests with all features
run: cargo make ci

View File

@@ -14,29 +14,27 @@ members = [
# examples
"examples/counter",
"examples/counter-isomorphic/client",
"examples/counter-isomorphic/server",
"examples/counter-isomorphic/counter",
"examples/counter-isomorphic",
"examples/counters",
"examples/counters-stable",
"examples/fetch",
"examples/hackernews/hackernews-app",
"examples/hackernews/hackernews-client",
"examples/hackernews/hackernews-server",
"examples/hackernews",
"examples/parent-child",
"examples/router",
"examples/todomvc",
"examples/todomvc-ssr/todomvc-ssr-client",
"examples/todomvc-ssr/todomvc-ssr-server",
]
exclude = [
"benchmarks",
# not gonna lie, this is because my arm64 mac fails when linking a GTK binary
"examples/gtk",
]
"examples/view-tests",
# book
"docs/book/project/ch02_getting_started",
"docs/book/project/ch03_building_ui",
"docs/book/project/ch04_reactivity",
]
exclude = ["benchmarks"]
[profile.release]
codegen-units = 1
lto = true
opt-level = 'z'
[workspace.metadata.cargo-all-features]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@@ -48,8 +48,8 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
## What does that mean?
- **Full-stack**: Leptos can be used to build apps that run in the browser (_client-side rendering_), on the server (_server-side rendering_), or by rendering HTML on the server and then adding interactivity in the browser (_hydration_). This includes support for _HTTP streaming_ of both data (`Resource`s) and HTML (out-of-order streaming of `<Suspense/>` components.)
- **Isomorphic**: The same application code and business logic are compiled to run on the client and server, with seamless integration. You can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and let Leptos manage the data loading without the need to manually create APIs to consume.
- **Web**: Leptos is built on the Web platform and Web standards. Whenever possible, we use Web essentials (like links and forms) and build on top of them rather than trying to replace them.
- **Isomorphic**: Leptos provides primitives to write isomorphic server functions, i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser.
- **Web**: Leptos is built on the Web platform and Web standards. The router is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
- **Fine-grained reactivity**: The entire framework is build from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signals value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
@@ -131,7 +131,7 @@ There are some practical differences that make a significant difference:
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
- **Template node cloning:** Leptos's `view` macro compiles to a static HTML string and a set of instructions of how to assign its reactive values. This means that at runtime, Leptos can clone a `<template>` node rather than calling `document.createElement()` to create DOM nodes. This is a _significantly_ faster way of rendering components.
- **Read-write segregation:** Leptos, like Solid, enforces read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);`
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` *(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)*
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
```rust

View File

@@ -1,12 +0,0 @@
[package]
name = "ch01_getting_started"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { version = "0.0", features = ["csr"] }
[profile.release]
codegen-units = 1
lto = true
opt-level = 'z'

View File

@@ -1,5 +0,0 @@
use leptos::*;
fn main() {
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
}

View File

@@ -0,0 +1,7 @@
[package]
name = "ch02_getting_started"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0"

View File

@@ -4,7 +4,11 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Leptos • Todos</title>
<!-- This custom link tag with `data-trunk` tells Trunk to insert code here to load our Rust/Wasm code -->
<!-- `data-wasm-opt=z` tells the compiler to optimize for binary size in a release build -->
<link data-trunk rel="rust" data-wasm-opt="z" />
</head>
<body></body>
</html>

View File

@@ -0,0 +1,5 @@
use leptos::*;
fn main() {
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
}

View File

@@ -0,0 +1,7 @@
[package]
name = "ch03_building_ui"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0"

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Leptos • Todos</title>
<!-- This custom link tag with `data-trunk` tells Trunk to insert code here to load our Rust/Wasm code -->
<!-- `data-wasm-opt=z` tells the compiler to optimize for binary size in a release build -->
<link data-trunk rel="rust" data-wasm-opt="z" />
</head>
<body></body>
</html>

View File

@@ -0,0 +1,39 @@
use leptos::*;
fn main() {
mount_to_body(|cx| {
let name = "gbj";
let userid = 0;
let _input_element: Element;
view! {
cx,
<main>
<h1>"My Tasks"</h1> // text nodes are wrapped in quotation marks
<h2>"by " {name}</h2>
<input
type="text" // attributes work just like they do in HTML
name="new-todo"
prop:value="todo" // `prop:` lets you set a property on a DOM node
value="initial" // side note: the DOM `value` attribute only sets *initial* value
// this is very important when working with forms!
_ref=_input_element // `_ref` stores tis element in a variable
/>
<ul data-user=userid> // attributes can take expressions as values
<li class="todo my-todo" // here we set the `class` attribute
class:completed=true // `class:` also lets you toggle individual classes
on:click=|_| todo!() // `on:` adds an event listener
>
"Buy milk."
</li>
<li class="todo my-todo" class:completed=false>
"???"
</li>
<li class="todo my-todo" class:completed=false>
"Profit!!!"
</li>
</ul>
</main>
}
})
}

View File

@@ -0,0 +1,7 @@
[package]
name = "ch04_reactivity"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0"

View File

@@ -0,0 +1,20 @@
use leptos::*;
fn main() {
run_scope(|cx| {
let (count, set_count) = create_signal(cx, 1);
let double_count = move || count() * 2;
create_effect(cx, move |_| {
println!(
"count =\t\t{}\ndouble_count = \t{}",
count(),
double_count(),
);
});
set_count(1);
set_count(2);
set_count(3);
});
}

View File

@@ -1,8 +1,8 @@
# Getting Started
> The code for this chapter can be found [here](https://github.com/gbj/leptos/tree/main/docs/book/project/ch01_getting_started).
> The code for this chapter can be found [here](https://github.com/gbj/leptos/tree/main/docs/book/project/ch02_getting_started).
The easiest way to get started using Leptos is to use [Trunk](https://trunkrs.dev/), as many of our [examples](https://github.com/gbj/leptos/tree/main/examples) do.
The easiest way to get started using Leptos is to use [Trunk](https://trunkrs.dev/), as many of our [examples](https://github.com/gbj/leptos/tree/main/examples) do. (Trunk is a simple build tool that includes a dev server.)
If you dont already have it installed, you can install Trunk by running
@@ -19,19 +19,19 @@ cargo init leptos-todo
Add `leptos` as a dependency to your `Cargo.toml` with the `csr` featured enabled. (That stands for “client-side rendering.” Well talk more about Leptoss support for server-side rendering and hydration later.)
```toml
leptos = { version = "0.0", features = ["csr"] }
leptos = "0.0"
```
Youll want to set up a basic `index.html` with the following content:
```html
{{#include ../project/ch01_getting_started/index.html}}
{{#include ../project/ch02_getting_started/index.html}}
```
Lets start with a very simple `main.rs`
```rust
{{#include ../project/ch01_getting_started/src/main.rs}}
{{#include ../project/ch02_getting_started/src/main.rs}}
```
Now run `trunk serve --open`. Trunk should automatically compile your app and open it in your default browser.
Now run `trunk serve --open`. Trunk should automatically compile your app and open it in your default browser. If you make edits to `main.rs`, Trunk will recompile your source code and live-reload the page.

View File

@@ -0,0 +1,49 @@
# Templating: Building User Interfaces
> The code for this chapter can be found [here](https://github.com/gbj/leptos/tree/main/docs/book/project/ch03_building_ui).
## RSX and the `view!` macro
Okay, that “Hello, world!” was a little boring. Were going to be building a todo app, so lets look at something a little more complicated.
As you noticed in the first example, Leptos lets you describe your user interface with a declarative `view!` macro. It looks something like this:
```
view! {
cx, // this is the "reactive scope": more on that in the next chapter
<p>"..."</p> // this is some HTML-ish stuff
}
```
The “HTML-ish stuff” is what we call “RSX”: XML in Rust. (You may recognize the similarity to JSX, which is the mixed JavaScript/XML syntax used by frameworks like React.)
Heres a more in-depth example:
```rust
{{#include ../project/ch03_building_ui/src/main.rs}}
```
Youll probably notice a few things right away:
1. Elements without children need to be explicit closed with a `/` (`<input/>`, not `<input>`)
2. Text nodes are formatted as strings, i.e., wrapped in quotation marks (`"My Tasks"`)
3. Dynamic blocks can be inserted as children of elements, if wrapped in curly braces (`<h2>"by " {name}</h2>`)
4. Attributes can be given Rust expressions as values. This could be a string literal as in HTML (`<input type="text" .../>)` or a variable or block (`data-user=userid` or `on:click=move |_| { ... }`)
5. Unlike in HTML, whitespace is ignored and should be manually added (its `<h2>"by " {name}</h2>`, not `<h2>"by" {name}</h2>`; the space between `"by"` and `{name}` is ignored.)
6. Normal attributes work exactly like you'd think they would.
7. There are also special, prefixed attributes.
- `class:` lets you make targeted updates to a single class
- `on:` lets you add an event listener
- `prop:` lets you set a property on a DOM element
- `_ref` stores the DOM element youre creating in a variable
> You can find more information in the [reference docs for the `view!` macro](https://docs.rs/leptos/0.0.15/leptos/macro.view.html).
## But, wait...
This example shows some parts of the Leptos templating syntax. But its completely static.
How do you actually make the user interface interactive?
In the next chapter, well talk about “fine-grained reactivity,” which is the core of the Leptos framework.

View File

@@ -1,6 +1,45 @@
# Reactivity
## Signals
## What is reactivity?
A few months ago, I completely baffled a friend by trying to explain what I was working on. “You have two variables, right? Call them `a` and `b`. And then you have a third variable, `c`. And when you update `a` or `b`, the value of `c` just _automatically changes_. And it changes _on the screen_! Automatically!”
“Isnt that just... how computers work?” she asked me, puzzled. If your programming experience is limited to something like spreadsheets, its a reasonable enough assumption. This is, after all, how math works.
But you know this isn't how ordinary imperative programming works.
```rust,should_panic
let mut a = 0;
let mut b = 0;
let c = a + b;
a = 2;
b = 2;
// now c = 4, right?
assert_eq!(c, 4); // nope. we all know this is wrong!
```
But thats _exactly_ how reactive programming works.
```rust
use leptos::*;
run_scope(|cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = move || a() + b();
set_a(2);
set_b(2);
assert_eq!(c(), 4); // ohhhhh yeah.
});
```
## Reactive Primitives
### Signals
A **signal** is a piece of data that may change over time, and notifies other code when it has changed. This is the core primitive of Leptoss reactive system.
@@ -12,7 +51,7 @@ let (value, set_value) = create_signal(cx, 0);
> If youve used signals in Sycamore or Solid, observables in MobX or Knockout, or a similar primitive in reactive library, you probably have a pretty good idea of how signals work in Leptos. If youre familiar with React, Yew, or Dioxus, you may recognize a similar pattern to their `use_state` hooks.
### `ReadSignal<T>`
#### `ReadSignal<T>`
The `ReadSignal` half of this tuple allows you to get the current value of the signal. Reading that value in a reactive context automatically subscribes to any further changes. You can access the value by simply calling the `ReadSignal` as a function.
@@ -43,7 +82,7 @@ let lowercased = move || value.with(|value| value.to_lowercase());
let lowercased = move || value.with(String::to_lowercase);
```
### `WriteSignal<T>`
#### `WriteSignal<T>`
The `WriteSignal` half of this tuple allows you to update the value of the signal, which will automatically notify anything thats listening to the value that something has changed. If you simply call the `WriteSignal` as a function, its value will be set to the argument you pass. If you want to mutate the value in place instead of replacing it, you can call `WriteSignal::update` instead.

View File

@@ -1,6 +1,6 @@
# Summary
- [Introduction](./introduction.md)
- [Getting Started](./getting_started.md)
- [Templating: Building User Interfaces](./building_ui.md)
- [Reactivity: Making Things Interactive](./reactivity.md)
- [Introduction](./01_introduction.md)
- [Getting Started](./02_getting_started.md)
- [Templating: Building User Interfaces](./03_building_ui.md)
- [Reactivity: Making Things Interactive](./04_reactivity.md)

View File

@@ -1,49 +0,0 @@
# Templating: Building User Interfaces
## Views
Leptos uses a simple `view` macro to create the user interface. If youre familiar with JSX, then
## Components
**Components** are the basic building blocks of your application. Each component is simply a function that creates DOM nodes and sets up the reactive system that will update them. The component function runs exactly once per instance of the component.
The `component` macro annotates a function as a component, allowing you to use it within other components.
```rust
use leptos::*;
#[component]
fn Button(cx: Scope, text: &'static str) -> Element {
view! { cx,
<button>{text}</button>
}
}
#[component]
fn BoringButtons(cx: Scope) -> Element {
view! { cx,
<div>
<Button text="These"/>
<Button text="Do"/>
<Button text="Nothing"/>
</div>
}
}
```
## Views
Leptos uses a simple `view` macro to create the user interface. Its much like HTML, with the following differences:
1. Text within elements follows the rules of normal Rust strings (i.e., quotation marks or other string syntax)
```rust
view! { cx, <p>"Hello, world!"</p> }
```
2. Values can be inserted between curly braces. Reactive values
```rust
view! { cx, <p id={non_reactive_variable}>{move || value()}</p> }
```

View File

@@ -0,0 +1,43 @@
[package]
name = "leptos-counter-isomorphic"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
broadcaster = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
serde = { version = "1", features = ["derive"] }
futures = "0.3"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
log = "0.4"
simple_logger = "2"
gloo = { git = "https://github.com/rustwasm/gloo" }
#counter = { path = "../counter", default-features = false}
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

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

View File

@@ -0,0 +1,18 @@
# Leptos Counter Isomorphic Example
This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result.
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
If for some reason you want to run it as a fully client side app, that can be done with the instructions below.

View File

@@ -1,16 +0,0 @@
[package]
name = "counter-client"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2"
leptos = { path = "../../../leptos", default-features = false, features = ["hydrate", "serde"] }
counter-isomorphic = { path = "../counter", default-features = false, features = ["hydrate"] }
log = "0.4"
wasm-bindgen = "0.2"
console_error_panic_hook = "0.1.7"

View File

@@ -1 +0,0 @@
wasm-pack build --target=web --release

View File

@@ -1,14 +0,0 @@
use counter_isomorphic::*;
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), |cx| {
view! { cx, <Counters/> }
});
}

View File

@@ -1,25 +0,0 @@
[package]
name = "counter-isomorphic"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../../leptos", default-features = false, features = ["serde"] }
leptos_router = { path = "../../../router", default-features = false }
broadcaster = "1"
console_log = "0.2"
futures = "0.3"
gloo = { git = "https://github.com/rustwasm/gloo" }
lazy_static = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
serde = { version = "1", features = ["derive"] }
[dependencies.web-sys]
version = "0.3"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_router/hydrate"]
ssr = ["leptos/ssr", "leptos_router/ssr"]

View File

@@ -1,8 +0,0 @@
pub use counter_isomorphic::*;
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, <Counter/> });
}

View File

@@ -1,13 +0,0 @@
[package]
name = "counter-server"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-files = "0.6"
actix-web = { version = "4" }
futures = "0.3"
leptos = { path = "../../../leptos", default-features = false, features = ["ssr", "serde"] }
leptos_router = { path = "../../../router", default-features = false, features = ["ssr"] }
counter-isomorphic = { path = "../counter", default-features = false, features = ["ssr"] }
lazy_static = "1"

View File

@@ -1,107 +0,0 @@
use actix_files::Files;
use actix_web::*;
use counter_isomorphic::*;
use leptos::*;
use leptos_router::*;
#[get("{tail:.*}")]
async fn render(req: HttpRequest) -> impl Responder {
let path = req.path();
let path = "http://leptos".to_string() + path;
println!("path = {path}");
HttpResponse::Ok().content_type("text/html").body(format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Isomorphic Counter</title>
</head>
<body>
{}
</body>
<script type="module">import init, {{ main }} from './pkg/counter_client.js'; init().then(main);</script>
</html>"#,
run_scope({
move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
view! { cx, <Counters/>}
}
})
))
}
#[post("{tail:.*}")]
async fn handle_server_fns(
req: HttpRequest,
params: web::Path<String>,
body: web::Bytes,
) -> impl Responder {
let path = params.into_inner();
let accept_header = req
.headers()
.get("Accept")
.and_then(|value| value.to_str().ok());
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body: &[u8] = &body;
match server_fn(&body).await {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
if let Some("application/json") = accept_header {
HttpResponse::Ok().body(serialized)
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
HttpResponse::SeeOther()
.insert_header(("Location", "/"))
.content_type("application/json")
.body(serialized)
}
}
Err(e) => {
eprintln!("server function error: {e:#?}");
HttpResponse::InternalServerError().body(e.to_string())
}
}
} else {
HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
}
}
#[get("/api/events")]
async fn counter_events() -> impl Responder {
use futures::StreamExt;
let stream =
futures::stream::once(async { counter_isomorphic::get_server_count().await.unwrap_or(0) })
.chain(COUNT_CHANNEL.clone())
.map(|value| {
Ok(web::Bytes::from(format!(
"event: message\ndata: {value}\n\n"
))) as Result<web::Bytes>
});
HttpResponse::Ok()
.insert_header(("Content-Type", "text/event-stream"))
.streaming(stream)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
counter_isomorphic::register_server_functions();
HttpServer::new(|| {
App::new()
.service(Files::new("/pkg", "../client/pkg"))
.service(counter_events)
.service(handle_server_fns)
.service(render)
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8081))?
.run()
.await
}

View File

@@ -1,16 +1,12 @@
use leptos::*;
use leptos_router::*;
use std::fmt::Debug;
#[cfg(feature = "ssr")]
use std::sync::atomic::{AtomicI32, Ordering};
#[cfg(feature = "ssr")]
use broadcaster::BroadcastChannel;
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
pub fn register_server_functions() {
GetServerCount::register();
@@ -25,13 +21,13 @@ static COUNT: AtomicI32 = AtomicI32::new(0);
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
#[server(GetServerCount)]
// "/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> {
Ok(COUNT.load(Ordering::Relaxed))
}
#[server(AdjustServerCount)]
#[server(AdjustServerCount, "/api")]
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);
@@ -40,13 +36,12 @@ pub async fn adjust_server_count(delta: i32, msg: String) -> Result<i32, ServerF
Ok(new)
}
#[server(ClearServerCount)]
#[server(ClearServerCount, "/api")]
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
COUNT.store(0, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&0).await;
Ok(0)
}
#[component]
pub fn Counters(cx: Scope) -> Element {
view! {
@@ -175,13 +170,13 @@ pub fn FormCounter(cx: Scope) -> Element {
// by including them as input values with the same name
<ActionForm action=adjust>
<input type="hidden" name="delta" value="-1"/>
<input type="hidden" name="msg" value="\"form value down\""/>
<input type="hidden" name="msg" value="form value down"/>
<input type="submit" value="-1"/>
</ActionForm>
<span>"Value: " {move || value().to_string()} "!"</span>
<ActionForm action=adjust2>
<input type="hidden" name="delta" value="1"/>
<input type="hidden" name="msg" value="\"form value up\""/>
<input type="hidden" name="msg" value="form value up"/>
<input type="submit" value="+1"/>
</ActionForm>
</div>

View File

@@ -0,0 +1,22 @@
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 wasm_bindgen::prelude::wasm_bindgen;
use crate::counters::*;
#[wasm_bindgen]
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), |cx| {
view! { cx, <Counters/> }
});
}
}
}

View File

@@ -0,0 +1,127 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_router::*;
mod counters;
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use actix_files::{Files};
use actix_web::*;
use crate::counters::*;
#[get("{tail:.*}")]
async fn render(req: HttpRequest) -> impl Responder {
let path = req.path();
let path = "http://leptos".to_string() + path;
println!("path = {path}");
HttpResponse::Ok().content_type("text/html").body(format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Isomorphic Counter</title>
</head>
<body>
{}
</body>
<script type="module">import init, {{ main }} from './pkg/leptos_counter_isomorphic.js'; init().then(main);</script>
</html>"#,
run_scope({
move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
view! { cx, <Counters/>}
}
})
))
}
#[post("/api/{tail:.*}")]
async fn handle_server_fns(
req: HttpRequest,
params: web::Path<String>,
body: web::Bytes,
) -> impl Responder {
let path = params.into_inner();
let accept_header = req
.headers()
.get("Accept")
.and_then(|value| value.to_str().ok());
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body: &[u8] = &body;
match server_fn(&body).await {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
if let Some("application/json") = accept_header {
HttpResponse::Ok().body(serialized)
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
HttpResponse::SeeOther()
.insert_header(("Location", "/"))
.content_type("application/json")
.body(serialized)
}
}
Err(e) => {
eprintln!("server function error: {e:#?}");
HttpResponse::InternalServerError().body(e.to_string())
}
}
} else {
HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
}
}
#[get("/api/events")]
async fn counter_events() -> impl Responder {
use futures::StreamExt;
let stream =
futures::stream::once(async { crate::counters::get_server_count().await.unwrap_or(0) })
.chain(COUNT_CHANNEL.clone())
.map(|value| {
Ok(web::Bytes::from(format!(
"event: message\ndata: {value}\n\n"
))) as Result<web::Bytes>
});
HttpResponse::Ok()
.insert_header(("Content-Type", "text/event-stream"))
.streaming(stream)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
crate::counters::register_server_functions();
HttpServer::new(|| {
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(counter_events)
.service(handle_server_fns)
.service(render)
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8081))?
.run()
.await
}
}
// client-only stuff for Trunk
else {
use leptos_counter_isomorphic::counters::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <Counter/> });
}
}
}

View File

@@ -0,0 +1,6 @@
# Leptos Counter Example
This example creates a simple counter in a client side rendered app with Rust and WASM!
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.

View File

@@ -0,0 +1,44 @@
[package]
name = "leptos-hackernews"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1"
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
console_log = "0.2"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
log = "0.4"
simple_logger = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
gloo-net = { version = "0.2", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
# openssl = { version = "0.10", features = ["v110"] }
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

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

View File

@@ -0,0 +1,20 @@
# Leptos Hacker News Example
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CRS bundle
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr`
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!

View File

@@ -1,26 +0,0 @@
[package]
name = "hackernews-app"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
console_log = "0.2"
leptos = { path = "../../../leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { path = "../../../meta", default-features = false }
leptos_router = { path = "../../../router", default-features = false }
log = "0.4"
gloo-net = { version = "0.2", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
console_error_panic_hook = "0.1.7"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr"]

View File

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

View File

@@ -1,14 +0,0 @@
[package]
name = "hackernews-client"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2"
console_error_panic_hook = "0.1"
hackernews-app = { path = "../hackernews-app", default-features = false, features = ["hydrate"] }
leptos = { path = "../../../leptos", default-features = false, features = ["hydrate", "serde"] }
log = "0.4"

View File

@@ -1 +0,0 @@
wasm-pack build --target=web --release

View File

@@ -1,12 +0,0 @@
use hackernews_app::*;
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), move |cx| {
view! { cx, <App/> }
});
}

View File

@@ -1,2 +0,0 @@
key.pem
cert.pem

View File

@@ -1,17 +0,0 @@
[package]
name = "hackernews-server"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-files = "0.6"
actix-web = { version = "4", features = ["openssl", "macros"] }
futures = "0.3"
leptos = { path = "../../../leptos", default-features = false, features = ["ssr", "serde"] }
leptos_router = { path = "../../../router", default-features = false, features = ["ssr"] }
leptos_meta = { path = "../../../meta", default-features = false, features = ["ssr"] }
log = "0.4"
hackernews-app = { path = "../hackernews-app", default-features = false, features = ["ssr"] }
openssl = { version = "0.10", features = ["v110"] }
simple_logger = "2"
serde_json = "1.0.85"

View File

@@ -1,111 +0,0 @@
use actix_files::{Files, NamedFile};
use actix_web::*;
use futures::StreamExt;
use hackernews_app::*;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
#[get("/static/style.css")]
async fn css() -> impl Responder {
NamedFile::open_async("../hackernews-app/style.css").await
}
// match every path — our router will handle actual dispatch
#[get("{tail:.*}")]
async fn render_app(req: HttpRequest) -> impl Responder {
let path = req.path();
let query = req.query_string();
let path = if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
};
let app = move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
view! { cx, <App/> }
};
let accepts_type = req.headers().get("Accept").map(|h| h.to_str());
match accepts_type {
// if asks for JSON, send the loader function JSON or 404
Some(Ok("application/json")) => {
let json = loader_to_json(app).await;
let res = if let Some(json) = json {
HttpResponse::Ok()
.content_type("application/json")
.body(json)
} else {
HttpResponse::NotFound().body(())
};
res
}
// otherwise, send HTML
_ => {
let head = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, { main } from '/pkg/hackernews_client.js'; init().then(main);</script>"#;
let tail = "</body></html>";
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async { head.to_string() })
.chain(render_to_stream(move |cx| {
let app = app(cx);
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}))
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
}
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
log::debug!("serving at {host}:{port}");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// uncomment these lines (and .bind_openssl() below) to enable HTTPS, which is sometimes
// necessary for proper HTTP/2 streaming
// load TLS keys
// to create a self-signed temporary cert for testing:
// `openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
// let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
// builder
// .set_private_key_file("key.pem", SslFiletype::PEM)
// .unwrap();
// builder.set_certificate_chain_file("cert.pem").unwrap();
HttpServer::new(|| {
App::new()
.service(css)
.service(
web::scope("/pkg")
.service(Files::new("", "../hackernews-client/pkg"))
.wrap(middleware::Compress::default()),
)
.service(render_app)
})
.bind(("127.0.0.1", 8080))?
// replace .bind with .bind_openssl to use HTTPS
//.bind_openssl(&format!("{}:{}", host, port), builder)?
.run()
.await
}

View File

@@ -1,19 +1,13 @@
// This is essentially a port of a Solid Hacker News demo
// https://github.com/solidjs/solid-hackernews
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod api;
mod nav;
mod stories;
mod story;
mod users;
use nav::*;
use stories::*;
use story::*;
use users::*;
mod routes;
use routes::nav::*;
use routes::stories::*;
use routes::story::*;
use routes::users::*;
#[component]
pub fn App(cx: Scope) -> Element {
@@ -36,3 +30,19 @@ pub fn App(cx: Scope) -> Element {
</div>
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), move |cx| {
view! { cx, <App/> }
});
}
}
}

View File

@@ -0,0 +1,111 @@
use cfg_if::cfg_if;
use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use actix_files::{Files, NamedFile};
use actix_web::*;
use futures::StreamExt;
use leptos_meta::*;
use leptos_router::*;
use leptos_hackernews::*;
#[get("/static/style.css")]
async fn css() -> impl Responder {
NamedFile::open_async("./style.css").await
}
// match every path — our router will handle actual dispatch
#[get("{tail:.*}")]
async fn render_app(req: HttpRequest) -> impl Responder {
let path = req.path();
let query = req.query_string();
let path = if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
};
let app = move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
view! { cx, <App/> }
};
let head = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, { main } from '/pkg/leptos_hackernews.js'; init().then(main);</script>"#;
let tail = "</body></html>";
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async { head.to_string() })
.chain(render_to_stream(move |cx| {
let app = app(cx);
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}))
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
log::debug!("serving at {host}:{port}");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// uncomment these lines (and .bind_openssl() below) to enable HTTPS, which is sometimes
// necessary for proper HTTP/2 streaming
// load TLS keys
// to create a self-signed temporary cert for testing:
// `openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
// let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
// builder
// .set_private_key_file("key.pem", SslFiletype::PEM)
// .unwrap();
// builder.set_certificate_chain_file("cert.pem").unwrap();
HttpServer::new(|| {
App::new()
.service(css)
.service(
web::scope("/pkg")
.service(Files::new("", "./dist"))
.wrap(middleware::Compress::default()),
)
.service(render_app)
})
.bind(("127.0.0.1", 8080))?
// replace .bind with .bind_openssl to use HTTPS
//.bind_openssl(&format!("{}:{}", host, port), builder)?
.run()
.await
}
}
// client-only stuff for Trunk
else {
use leptos_hackernews::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <App/> }
});
}
}
}

View File

@@ -0,0 +1,4 @@
pub mod nav;
pub mod stories;
pub mod story;
pub mod users;

View File

@@ -7,10 +7,11 @@ edition = "2021"
console_log = "0.2"
log = "0.4"
leptos = { path = "../../leptos" }
leptos_router = { path = "../../router" }
leptos_router = { path = "../../router", features=["csr"] }
serde = { version = "1", features = ["derive"] }
futures = "0.3"
console_error_panic_hook = "0.1.7"
leptos_meta = { path = "../../../leptos/meta", default-features = false }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@@ -0,0 +1,8 @@
# Leptos Router Example
This example demonstrates how Leptos' router works
## Run it
```bash
trunk serve --open
```

View File

@@ -2,11 +2,13 @@ mod api;
use api::{Contact, ContactSummary};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use crate::api::{get_contact, get_contacts};
pub fn router_example(cx: Scope) -> Element {
provide_context(cx, MetaContext::default());
view! { cx,
<div id="root">
<Router>

View File

@@ -1,16 +0,0 @@
[package]
name = "todomvc-ssr-client"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2"
leptos = { path = "../../../leptos", default-features = false, features = ["hydrate"] }
todomvc = { path = "../../todomvc", default-features = false, features = ["hydrate"] }
log = "0.4"
wasm-bindgen = "0.2"
console_error_panic_hook = "0.1.7"

View File

@@ -1 +0,0 @@
wasm-pack build --target=web --release

View File

@@ -1,22 +0,0 @@
use leptos::*;
use todomvc::*;
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log::debug!("initialized logging");
leptos::hydrate(body().unwrap(), |cx| {
// initial state — identical to server
let todos = Todos(vec![
Todo::new(cx, 0, "Buy milk".to_string()),
Todo::new(cx, 1, "???".to_string()),
Todo::new(cx, 2, "Profit!".to_string()),
]);
view! { cx, <TodoMVC todos=todos/> }
});
}

View File

@@ -1,10 +0,0 @@
[package]
name = "todomvc-ssr-server"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-files = "0.6"
actix-web = "4"
leptos = { path = "../../../leptos", default-features = false, features = ["ssr"] }
todomvc = { path = "../../todomvc", default-features = false, features = ["ssr"] }

View File

@@ -1,51 +0,0 @@
use actix_files::Files;
use actix_web::*;
use leptos::*;
use todomvc::*;
#[get("/")]
async fn render_todomvc() -> impl Responder {
HttpResponse::Ok().content_type("text/html").body(format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/todomvc-common/base.css"/>
<link rel="stylesheet" href="/static/todomvc-app-css/index.css"/>
<title>"Leptos • TodoMVC"</title>
</head>
<body>
{}
</body>
<script type="module">import init, {{ main }} from './pkg/todomvc_ssr_client.js'; init().then(main);</script>
</html>"#,
run_scope({
|cx| {
let todos = Todos(vec![
Todo::new(cx, 0, "Buy milk".to_string()),
Todo::new(cx, 1, "???".to_string()),
Todo::new(cx, 2, "Profit!".to_string()),
]);
view! { cx,
<TodoMVC todos=todos/>
}
}
})
))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(render_todomvc)
.service(Files::new("/static", "../../todomvc/node_modules"))
.service(Files::new("/pkg", "../todomvc-ssr-client/pkg"))
.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}

View File

@@ -5,15 +5,16 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", default-features = false }
miniserde = "0.1"
log = "0.4"
console_log = "0.2"
console_error_panic_hook = "0.1.7"
uuid = { version = "1", features = ["v4", "js", "serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
[features]
default = ["csr"]
csr = ["leptos/csr"]

View File

@@ -1,6 +1,6 @@
use leptos::{web_sys::HtmlInputElement, *};
use miniserde::json;
use storage::TodoSerialized;
use uuid::Uuid;
mod storage;
@@ -9,6 +9,7 @@ pub struct Todos(pub Vec<Todo>);
const STORAGE_KEY: &str = "todos-leptos";
// Basic operations to manipulate the todo list: nothing really interesting here
impl Todos {
pub fn new(cx: Scope) -> Self {
let starting_todos = if is_server!() {
@@ -18,7 +19,7 @@ impl Todos {
.get_item(STORAGE_KEY)
.ok()
.flatten()
.and_then(|value| json::from_str::<Vec<TodoSerialized>>(&value).ok())
.and_then(|value| serde_json::from_str::<Vec<TodoSerialized>>(&value).ok())
.map(|values| {
values
.into_iter()
@@ -40,31 +41,35 @@ impl Todos {
self.0.push(todo);
}
pub fn remove(&mut self, id: usize) {
pub fn remove(&mut self, id: Uuid) {
self.0.retain(|todo| todo.id != id);
}
pub fn remaining(&self) -> usize {
self.0.iter().filter(|todo| !(todo.completed)()).count()
// `todo.completed` is a signal, so we call .get() to access its value
self.0.iter().filter(|todo| !todo.completed.get()).count()
}
pub fn completed(&self) -> usize {
self.0.iter().filter(|todo| (todo.completed)()).count()
// `todo.completed` is a signal, so we call .get() to access its value
self.0.iter().filter(|todo| todo.completed.get()).count()
}
pub fn toggle_all(&self) {
// if all are complete, mark them all active instead
// if all are complete, mark them all active
if self.remaining() == 0 {
for todo in &self.0 {
if todo.completed.get() {
(todo.set_completed)(false);
}
todo.completed.update(|completed| {
if *completed {
*completed = false
}
});
}
}
// otherwise, mark them all complete
else {
for todo in &self.0 {
(todo.set_completed)(true);
todo.completed.set(true);
}
}
}
@@ -76,33 +81,35 @@ impl Todos {
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Todo {
pub id: usize,
pub title: ReadSignal<String>,
pub set_title: WriteSignal<String>,
pub completed: ReadSignal<bool>,
pub set_completed: WriteSignal<bool>,
pub id: Uuid,
pub title: RwSignal<String>,
pub completed: RwSignal<bool>,
}
impl Todo {
pub fn new(cx: Scope, id: usize, title: String) -> Self {
pub fn new(cx: Scope, id: Uuid, title: String) -> Self {
Self::new_with_completed(cx, id, title, false)
}
pub fn new_with_completed(cx: Scope, id: usize, title: String, completed: bool) -> Self {
let (title, set_title) = create_signal(cx, title);
let (completed, set_completed) = create_signal(cx, completed);
pub fn new_with_completed(cx: Scope, id: Uuid, title: String, completed: bool) -> Self {
// RwSignal combines the getter and setter in one struct, rather than separating
// the getter from the setter. This makes it more convenient in some cases, such
// as when we're putting the signals into a struct and passing it around. There's
// no real difference: you could use `create_signal` here, or use `create_rw_signal`
// everywhere.
let title = create_rw_signal(cx, title);
let completed = create_rw_signal(cx, completed);
Self {
id,
title,
set_title,
completed,
set_completed,
}
}
pub fn toggle(&self) {
self.set_completed
.update(|completed| *completed = !*completed);
// A signal's `update()` function gives you a mutable reference to the current value
// You can use that to modify the value in place, which will notify any subscribers.
self.completed.update(|completed| *completed = !*completed);
}
}
@@ -110,24 +117,25 @@ const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
#[component]
pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
let mut next_id = todos
.0
.iter()
.map(|todo| todo.id)
.max()
.map(|last| last + 1)
.unwrap_or(0);
pub fn TodoMVC(cx: Scope) -> Element {
// The `todos` are a signal, since we need to reactively update the list
let (todos, set_todos) = create_signal(cx, Todos::new(cx));
let (todos, set_todos) = create_signal(cx, todos);
// We provide a context that each <Todo/> component can use to update the list
// Here, I'm just passing the `WriteSignal`; a <Todo/> doesn't need to read the whole list
// (and shouldn't try to, as that would cause each individual <Todo/> to re-render when
// a new todo is added! This kind of hygiene is why `create_signal` defaults to read-write
// segregation.)
provide_context(cx, set_todos);
// Handle the three filter modes: All, Active, and Completed
let (mode, set_mode) = create_signal(cx, Mode::All);
window_event_listener("hashchange", move |_| {
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);
});
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
let add_todo = move |ev: web_sys::Event| {
let target = event_target::<HtmlInputElement>(&ev);
ev.stop_propagation();
@@ -136,15 +144,16 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
let title = event_target_value(&ev);
let title = title.trim();
if !title.is_empty() {
let new = Todo::new(cx, next_id, title.to_string());
let new = Todo::new(cx, Uuid::new_v4(), title.to_string());
set_todos.update(|t| t.add(new));
next_id += 1;
target.set_value("");
}
}
};
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
// A derived signal that filters the list of the todos depending on the filter mode
// This doesn't need to be a `Memo`, because we're only reading it in one place
let filtered_todos = move || {
todos.with(|todos| match mode.get() {
Mode::All => todos.0.to_vec(),
Mode::Active => todos
@@ -160,10 +169,15 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
.cloned()
.collect(),
})
});
};
// effect to serialize to JSON
// this does reactive reads, so it will automatically serialize on any relevant change
// Serialization
//
// the effect reads the `todos` signal, and each `Todo`'s title and completed
// status, so it will automatically re-run on any change to the list of tasks
//
// this is the main point of `create_effect`: to synchronize reactive state
// with something outside the reactive system (like localStorage)
create_effect(cx, move |_| {
if let Ok(Some(storage)) = window().local_storage() {
let objs = todos
@@ -172,7 +186,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
.iter()
.map(TodoSerialized::from)
.collect::<Vec<_>>();
let json = json::to_string(&objs);
let json = serde_json::to_string(&objs).expect("couldn't serialize Todos");
if storage.set_item(STORAGE_KEY, &json).is_err() {
log::error!("error while trying to set item in localStorage");
}
@@ -184,12 +198,20 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus on:keydown=add_todo />
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
on:keydown=add_todo
/>
</header>
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
<section
class="main"
class:hidden={move || todos.with(|t| t.is_empty())}
>
<input id="toggle-all" class="toggle-all" type="checkbox"
prop:checked={move || todos.with(|t| t.remaining() > 0)}
on:input=move |_| set_todos.update(|t| t.toggle_all())
on:input=move |_| todos.with(|t| t.toggle_all())
/>
<label for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
@@ -198,7 +220,10 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
</For>
</ul>
</section>
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
<footer
class="footer"
class:hidden={move || todos.with(|t| t.is_empty())}
>
<span class="todo-count">
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
{move || if todos.with(|t| t.remaining()) == 1 {
@@ -235,6 +260,8 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
pub fn Todo(cx: Scope, todo: Todo) -> Element {
let (editing, set_editing) = create_signal(cx, false);
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
// this will be filled by _ref=input below
let input: Element;
let save = move |value: &str| {
@@ -242,29 +269,37 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
if value.is_empty() {
set_todos.update(|t| t.remove(todo.id));
} else {
(todo.set_title)(value.to_string());
todo.title.set(value.to_string());
}
set_editing(false);
};
let tpl = view! { cx,
view! { cx,
<li
class="todo"
class:editing={editing}
class:completed={move || (todo.completed)()}
_ref=input
class:completed={move || todo.completed.get()}
>
<div class="view">
<input
_ref=input
class="toggle"
type="checkbox"
prop:checked={move || (todo.completed)()}
on:input={move |ev| {
let checked = event_target_checked(&ev);
(todo.set_completed)(checked);
todo.completed.set(checked);
}}
/>
<label on:dblclick=move |_| set_editing(true)>
<label on:dblclick=move |_| {
set_editing(true);
// guard against the fact that in SSR mode, that ref is actually to a String
#[cfg(any(feature = "csr", feature = "hydrate"))]
if let Some(input) = input.dyn_ref::<HtmlInputElement>() {
input.focus();
}
}>
{move || todo.title.get()}
</label>
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
@@ -287,16 +322,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
})
}
</li>
};
#[cfg(any(feature = "csr", feature = "hydrate"))]
create_effect(cx, move |_| {
if editing() {
_ = input.unchecked_ref::<HtmlInputElement>().focus();
}
});
tpl
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@@ -4,5 +4,5 @@ fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <TodoMVC todos=Todos::new(cx)/> })
mount_to_body(|cx| view! { cx, <TodoMVC/> })
}

View File

@@ -1,10 +1,11 @@
use crate::Todo;
use leptos::Scope;
use miniserde::{Deserialize, Serialize};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize, Deserialize)]
pub struct TodoSerialized {
pub id: usize,
pub id: Uuid,
pub title: String,
pub completed: bool,
}
@@ -20,7 +21,7 @@ impl From<&Todo> for TodoSerialized {
Self {
id: todo.id,
title: todo.title.get(),
completed: (todo.completed)(),
completed: todo.completed.get(),
}
}
}

View File

@@ -0,0 +1,14 @@
[package]
name = "view-tests"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
log = "0.4"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
</head>
<body></body>
</html>

View File

@@ -0,0 +1,35 @@
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, <TemplateConsumer/> })
}
#[component]
fn TemplateConsumer(cx: Scope) -> Element {
let tpl = view! { cx, <TemplateExample/> };
let cloned_tpl = tpl
.unchecked_ref::<web_sys::HtmlTemplateElement>()
.content()
.clone_node_with_deep(true)
.expect("couldn't clone template node");
view! {
cx,
<div id="template">
<h1>"Template Consumer"</h1>
{cloned_tpl}
</div>
}
}
#[component]
fn TemplateExample(cx: Scope) -> Element {
view! {
cx,
<template>
<div>"Template contents"</div>
</template>
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos"
version = "0.0.11"
version = "0.0.16"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -9,11 +9,11 @@ description = "Leptos is a full-stack, isomorphic Rust web framework leveraging
readme = "../README.md"
[dependencies]
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.11" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.11" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.11" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.11" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.11" }
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.16" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.16" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.16" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.16" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.16" }
[features]
default = ["csr", "serde"]

View File

@@ -1,3 +1,5 @@
#![deny(missing_docs)]
//! # About Leptos
//!
//! Leptos is a full-stack framework for building web applications in Rust. You can use it to build
@@ -20,7 +22,7 @@
//! for examples of the correct API.
//!
//! # Learning by Example
//!
//!
//! These docs are a work in progress. If you want to see what Leptos is capable of, check out
//! the [examples](https://github.com/gbj/leptos/tree/main/examples):
//! - [`counter`](https://github.com/gbj/leptos/tree/main/examples/counter) is the classic
@@ -53,12 +55,14 @@
//!
//! Here are links to the most important sections of the docs:
//! - **Reactivity**: the [leptos_reactive] overview, and more details in
//! - [create_signal], [ReadSignal], and [WriteSignal] (and [create_rw_signal] and [RwSignal])
//! - [create_memo] and [Memo]
//! - [create_resource] and [Resource]
//! - [create_effect]
//! - signals: [create_signal], [ReadSignal], and [WriteSignal] (and [create_rw_signal] and [RwSignal])
//! - computations: [create_memo] and [Memo]
//! - `async` interop: [create_resource] and [Resource] for loading data using `async` functions,
//! and [create_action] and [Action] to mutate data or imperatively call `async` functions.
//! - reactions: [create_effect]
//! - **Templating/Views**: the [view] macro
//! - **Routing**: the [leptos_router](https://docs.rs/leptos_router/latest/leptos_router/) crate
//! - **Server Functions**: the [server](crate::leptos_server) macro, [create_action], and [create_server_action]
//!
//! # Feature Flags
//! - `csr` (*Default*) Client-side rendering: Generate DOM nodes in the browser
@@ -69,9 +73,9 @@
//! and `.set()` manually.
//! - `serde` (*Default*) In SSR/hydrate mode, uses [serde] to serialize resources and send them
//! from the server to the client.
//! - `serde-lite` (*Default*) In SSR/hydrate mode, uses [serde-lite] to serialize resources and send them
//! - `serde-lite` In SSR/hydrate mode, uses [serde-lite] to serialize resources and send them
//! from the server to the client.
//! - `serde` (*Default*) In SSR/hydrate mode, uses [miniserde] to serialize resources and send them
//! - `miniserde` In SSR/hydrate mode, uses [miniserde] to serialize resources and send them
//! from the server to the client.
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_core"
version = "0.0.11"
version = "0.0.16"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -8,20 +8,39 @@ repository = "https://github.com/gbj/leptos"
description = "Core functionality for the Leptos web framework."
[dependencies]
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.11" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.11" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.11" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.16" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.16" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.16" }
log = "0.4"
[dev-dependencies]
leptos = { path = "../leptos", default-features = false, version = "0.0" }
[features]
csr = ["leptos_dom/csr", "leptos_macro/csr", "leptos_reactive/csr"]
csr = [
"leptos/csr",
"leptos_dom/csr",
"leptos_macro/csr",
"leptos_reactive/csr",
]
hydrate = [
"leptos/hydrate",
"leptos_dom/hydrate",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
]
ssr = ["leptos_dom/ssr", "leptos_macro/ssr", "leptos_reactive/ssr"]
stable = ["leptos_dom/stable", "leptos_macro/stable", "leptos_reactive/stable"]
ssr = [
"leptos/ssr",
"leptos_dom/ssr",
"leptos_macro/ssr",
"leptos_reactive/ssr",
]
stable = [
"leptos/stable",
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

@@ -7,7 +7,7 @@ use std::hash::Hash;
use crate as leptos;
use crate::map::map_keyed;
/// Properties for the [For](crate::For) component.
/// Properties for the [For](crate::For) component, a keyed list.
#[derive(Props)]
pub struct ForProps<E, T, G, I, K>
where
@@ -17,18 +17,58 @@ where
K: Eq + Hash,
T: Eq + 'static,
{
/// Items over which the component should iterate.
pub each: E,
/// A key function that will be applied to each item
pub key: I,
/// Should provide a single child function, which takes
pub children: Box<dyn Fn() -> Vec<G>>,
}
/// Iterates over children and displays them, keyed by `PartialEq`.
/// Iterates over children and displays them, keyed by the `key` function given.
///
/// This is much more efficient than naively iterating over nodes with `.iter().map(|n| view! { cx, ... })...`,
/// as it avoids re-creating DOM nodes that are not being changed.
///
/// ```
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_core::*;
/// # use leptos_dom::*; use leptos::*;
///
/// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// struct Counter {
/// id: usize,
/// count: RwSignal<i32>
/// }
///
/// fn Counters(cx: Scope) -> Element {
/// let (counters, set_counters) = create_signal::<Vec<Counter>>(cx, vec![]);
///
/// view! {
/// cx,
/// <div>
/// <For
/// // a function that returns the items we're iterating over; a signal is fine
/// each=counters
/// // a unique key for each item
/// key=|counter| counter.id
/// >
/// {|cx: Scope, counter: &Counter| {
/// let count = counter.count;
/// view! {
/// cx,
/// <button>"Value: " {move || count.get()}</button>
/// }
/// }
/// }
/// </For>
/// </div>
/// }
/// }
/// ```
#[allow(non_snake_case)]
pub fn For<E, T, G, I, K>(cx: Scope, props: ForProps<E, T, G, I, K>) -> Memo<Vec<Element>>
//-> impl FnMut() -> Vec<Element>
where
E: Fn() -> Vec<T> + 'static,
G: Fn(Scope, &T) -> Element + 'static,

View File

@@ -1,3 +1,8 @@
#![deny(missing_docs)]
//! This crate contains several utility pieces that depend on multiple crates.
//! They are all re-exported in the main `leptos` crate.
mod for_component;
mod map;
mod suspense;
@@ -6,7 +11,10 @@ pub use for_component::*;
pub use map::*;
pub use suspense::*;
/// Describes the properties of a component. This is typically generated by the `Prop` derive macro
/// as part of the `#[component]` macro.
pub trait Prop {
/// Builder type, automatically generated.
type Builder;
/// The builder should be automatically generated using the `Prop` derive macro.

View File

@@ -168,7 +168,7 @@ mod tests {
let keyed = map_keyed(
cx,
rows,
move || rows.get(),
|cx, row| {
let read = row.1;
create_effect(cx, move |_| println!("row value = {}", read.get()));

View File

@@ -3,6 +3,8 @@ use leptos_dom::{Child, IntoChild};
use leptos_macro::Props;
use leptos_reactive::{provide_context, Scope, SuspenseContext};
/// Props for the [Suspense](crate::Suspense) component, which shows a fallback
/// while [Resource](leptos_reactive::Resource)s are being read.
#[derive(Props)]
pub struct SuspenseProps<F, E, G>
where
@@ -10,10 +12,59 @@ where
E: IntoChild,
G: Fn() -> E,
{
fallback: F,
children: Box<dyn Fn() -> Vec<G>>,
/// Will be displayed while resources are pending.
pub fallback: F,
/// Will be displayed once all resources have resolved.
pub children: Box<dyn Fn() -> Vec<G>>,
}
/// If any [Resource](leptos_reactive::Resource)s are read in the `children` of this
/// component, it will show the `fallback` while they are loading. Once all are resolved,
/// it will render the `children`.
///
/// Note that the `children` will be rendered initially (in order to capture the fact that
/// those resources are read under the suspense), so you cannot assume that resources have
/// `Some` value in `children`.
///
/// ```
/// # use leptos_reactive::*;
/// # use leptos_core::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(|cx| {
/// # if cfg!(not(any(feature = "csr", feature = "hydrate", feature = "ssr"))) {
/// async fn fetch_cats(how_many: u32) -> Result<Vec<String>, ()> { Ok(vec![]) }
///
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
///
/// let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
///
/// view! { cx,
/// <div>
/// <Suspense fallback={"Loading (Suspense Fallback)...".to_string()}>
/// {move || {
/// cats.read().map(|data| match data {
/// Err(_) => view! { cx, <pre>"Error"</pre> },
/// Ok(cats) => view! { cx,
/// <div>{
/// cats.iter()
/// .map(|src| {
/// view! { cx,
/// <img src={src}/>
/// }
/// })
/// .collect::<Vec<_>>()
/// }</div>
/// },
/// })
/// }
/// }
/// </Suspense>
/// </div>
/// };
/// # }
/// # });
/// ```
#[allow(non_snake_case)]
pub fn Suspense<F, E, G>(cx: Scope, props: SuspenseProps<F, E, G>) -> impl Fn() -> Child
where

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_dom"
version = "0.0.11"
version = "0.0.16"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -12,7 +12,7 @@ cfg-if = "1"
futures = "0.3"
html-escape = "0.2"
js-sys = "0.3"
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.11" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.16" }
serde_json = "1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4.31"
@@ -55,8 +55,12 @@ features = [
"Window",
]
[dev-dependencies]
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0" }
[features]
csr = ["leptos_reactive/csr"]
hydrate = ["leptos_reactive/hydrate"]
ssr = ["leptos_reactive/ssr"]
stable = ["leptos_reactive/stable"]
csr = ["leptos_reactive/csr", "leptos_macro/csr"]
hydrate = ["leptos_reactive/hydrate", "leptos_macro/hydrate"]
ssr = ["leptos_reactive/ssr", "leptos_macro/ssr"]
stable = ["leptos_reactive/stable", "leptos_macro/stable"]

View File

@@ -2,15 +2,24 @@ use std::rc::Rc;
use leptos_reactive::Scope;
/// Represents the different possible values an attribute node could have.
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly.
#[derive(Clone)]
pub enum Attribute {
/// A plain string value.
String(String),
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
Fn(Rc<dyn Fn() -> Attribute>),
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
Option(Option<String>),
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
Bool(bool),
}
impl Attribute {
/// Converts the attribute to its HTML value at that moment so it can be rendered on the server.
pub fn as_value_string(&self, attr_name: &'static str) -> String {
match self {
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
@@ -59,7 +68,11 @@ impl std::fmt::Debug for Attribute {
}
}
/// Converts some type into an [Attribute].
///
/// This is implemented by default for Rust primitive and string types.
pub trait IntoAttribute {
/// Converts the object into an [Attribute].
fn into_attribute(self, cx: Scope) -> Attribute;
}

View File

@@ -6,16 +6,26 @@ use leptos_reactive::Scope;
use crate::Node;
/// Represents the different possible values an element child node could have.
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly.
#[derive(Clone)]
pub enum Child {
/// Nothingness. Emptiness. The void.
Null,
/// A text node.
Text(String),
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the node.
Fn(Rc<RefCell<dyn FnMut() -> Child>>),
/// A generic node (a text node, comment, or element.)
Node(Node),
/// A list of nodes (text nodes, comments, or elements.)
Nodes(Vec<Node>),
}
impl Child {
/// Converts the attribute to its HTML value at that moment so it can be rendered on the server.
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
pub fn as_child_string(&self) -> String {
match self {
@@ -58,7 +68,9 @@ impl PartialEq for Child {
}
}
/// Converts some type into a [Child].
pub trait IntoChild {
/// Converts the object into a [Child].
fn into_child(self, cx: Scope) -> Child;
}

View File

@@ -1,11 +1,21 @@
use leptos_reactive::Scope;
/// Represents the different possible values a single class on an element could have,
/// allowing you to do fine-grained updates to single items
/// in [`Element.classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList).
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly.
pub enum Class {
/// Whether the class is present.
Value(bool),
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
Fn(Box<dyn Fn() -> bool>),
}
/// Converts some type into a [Class].
pub trait IntoClass {
/// Converts the object into a [Class].
fn into_class(self, cx: Scope) -> Class;
}
@@ -26,6 +36,7 @@ where
}
impl Class {
/// Converts the class to its HTML value at that moment so it can be rendered on the server.
pub fn as_value_string(&self, class_name: &'static str) -> &'static str {
match self {
Class::Value(value) => {

View File

@@ -1 +0,0 @@
pub trait Prop {}

View File

@@ -1,31 +1,74 @@
#![deny(missing_docs)]
//! DOM operations and rendering for Leptos.
//!
//! This crate mostly includes utilities and types used by the templating system, and utility
//! functions to make it easier for you to interact with the DOM, including with events.
//!
//! It also includes functions to support rendering HTML to strings, which is the server-side
//! equivalent of DOM operations.
//!
//! Note that the types [Element] and [Node] are type aliases, handled differently depending on the
//! target:
//! - Browser (features `csr` and `hydrate`): they alias [web_sys::Element] and [web_sys::Node],
//! since the renderer works directly with actual DOM nodes.
//! - Server: they both alias [String], since the templating system directly generates HTML strings.
use cfg_if::cfg_if;
pub mod attribute;
pub mod child;
pub mod class;
pub mod event_delegation;
pub mod logging;
pub mod mount;
pub mod operations;
pub mod property;
mod attribute;
mod child;
mod class;
mod event_delegation;
mod logging;
mod mount;
mod operations;
mod property;
cfg_if! {
// can only include this if we're *only* enabling SSR, as it's the lowest-priority feature
// if either `csr` or `hydrate` is enabled, `Element` is a `web_sys::Element` and can't be rendered
if #[cfg(not(any(feature = "hydrate", feature = "csr")))] {
pub type Element = String;
pub type Node = String;
pub mod render_to_string;
pub use render_to_string::*;
pub struct Marker { }
} else {
if #[cfg(doc)] {
/// The type of an HTML or DOM element. When server rendering, this is a `String`. When rendering in a browser,
/// this is a DOM `Element`.
pub type Element = web_sys::Element;
/// The type of an HTML or DOM node. When server rendering, this is a `String`. When rendering in a browser,
/// this is a DOM `Node`.
pub type Node = web_sys::Node;
pub mod reconcile;
pub mod render;
mod render_to_string;
pub use render_to_string::*;
mod reconcile;
mod render;
pub use reconcile::*;
pub use render::*;
} else if #[cfg(not(any(feature = "hydrate", feature = "csr")))] {
/// The type of an HTML or DOM element. When server rendering, this is a `String`. When rendering in a browser,
/// this is a DOM `Element`.
pub type Element = String;
/// The type of an HTML or DOM node. When server rendering, this is a `String`. When rendering in a browser,
/// this is a DOM `Node`.
pub type Node = String;
mod render_to_string;
pub use render_to_string::*;
#[doc(hidden)]
pub struct Marker { }
} else {
/// The type of an HTML or DOM element. When server rendering, this is a `String`. When rendering in a browser,
/// this is a DOM `Element`.
pub type Element = web_sys::Element;
/// The type of an HTML or DOM node. When server rendering, this is a `String`. When rendering in a browser,
/// this is a DOM `Node`.
pub type Node = web_sys::Node;
mod reconcile;
mod render;
pub use reconcile::*;
pub use render::*;
@@ -47,6 +90,8 @@ pub use web_sys;
use leptos_reactive::Scope;
pub use wasm_bindgen::UnwrapThrowExt;
// Hidden because this is primarily used by the `view` macro, not by library users.
#[doc(hidden)]
pub fn create_component<F, T>(cx: Scope, f: F) -> T
where
F: FnOnce() -> T,
@@ -60,6 +105,21 @@ where
}
}
/// Shorthand to test for whether an `ssr` feature is enabled.
///
/// In the past, this was implemented by checking whether `not(target_arch = "wasm32")`.
/// Now that some cloud platforms are moving to run Wasm on the edge, we really can't
/// guarantee that compiling to Wasm means browser APIs are available, or that not compiling
/// to Wasm means we're running on the server.
///
/// ```
/// # use leptos_dom::is_server;
/// let todos = if is_server!() {
/// // if on the server, load from DB
/// } else {
/// // if on the browser, do something else
/// };
/// ```
#[macro_export]
macro_rules! is_server {
() => {
@@ -67,6 +127,13 @@ macro_rules! is_server {
};
}
/// A shorthand macro to test whether this is a debug build.
/// ```
/// # use leptos_dom::is_dev;
/// if is_dev!() {
/// // log something or whatever
/// }
/// ```
#[macro_export]
macro_rules! is_dev {
() => {

View File

@@ -1,22 +1,7 @@
use wasm_bindgen::JsValue;
use crate::is_server;
#[macro_export]
macro_rules! log {
($($t:tt)*) => ($crate::console_log(&format_args!($($t)*).to_string()))
}
#[macro_export]
macro_rules! warn {
($($t:tt)*) => ($crate::console_warn(&format_args!($($t)*).to_string()))
}
#[macro_export]
macro_rules! error {
($($t:tt)*) => ($crate::console_error(&format_args!($($t)*).to_string()))
}
/// Uses `println!()`-style formatting to log warnings to the console (in the browser)
/// or via `eprintln!()` (if not in the browser), but only if it's a debug build.
#[macro_export]
macro_rules! debug_warn {
($($x:tt)*) => {
@@ -33,38 +18,18 @@ macro_rules! debug_warn {
}
}
pub fn console_log(s: &str) {
if is_server!() {
println!("{}", s);
} else {
web_sys::console::log_1(&JsValue::from_str(s));
}
/// Uses `println!()`-style formatting to log warnings to the console (in the browser)
/// or via `eprintln!()` (if not in the browser).
#[macro_export]
macro_rules! warn {
($($t:tt)*) => ($crate::console_warn(&format_args!($($t)*).to_string()))
}
/// Logs a string to the console (in the browser) or via `eprintln!()` (if not in the browser).
pub fn console_warn(s: &str) {
if is_server!() {
eprintln!("{}", s);
} else {
if cfg!(any(feature = "csr", feature = "hydrate")) {
web_sys::console::warn_1(&JsValue::from_str(s));
} else {
eprintln!("{}", s);
}
}
pub fn console_error(s: &str) {
if is_server!() {
eprintln!("{}", s);
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
}
#[cfg(debug_assertions)]
pub fn console_debug_warn(s: &str) {
if is_server!() {
eprintln!("{}", s);
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
}
#[cfg(not(debug_assertions))]
pub fn console_debug_warn(_s: &str) {}

View File

@@ -3,7 +3,9 @@ use cfg_if::cfg_if;
use leptos_reactive::Scope;
use wasm_bindgen::UnwrapThrowExt;
/// Describes a type that can be mounted to a parent element in the DOM.
pub trait Mountable {
/// Injects the element into the parent as its next child.
fn mount(&self, parent: &web_sys::Element);
}
@@ -31,6 +33,16 @@ impl Mountable for Vec<Element> {
}
}
/// Runs the given function to mount something to the `<body>` element in the DOM.
///
/// ```
/// // the simplest Leptos application
/// # use leptos_dom::*; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos_macro::view;
/// # if false { // can't actually run as a doctest on any feature
/// mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> });
/// # }
/// ```
pub fn mount_to_body<T, F>(f: F)
where
F: Fn(Scope) -> T + 'static,
@@ -39,6 +51,19 @@ where
mount(document().body().unwrap_throw(), f)
}
/// Runs the given function to mount something to the given element in the DOM.
///
/// ```
/// // a very simple Leptos application
/// # use leptos_dom::*; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos_macro::view;
/// # if false { // can't actually run as a doctest on any feature
/// mount(
/// document().get_element_by_id("root").unwrap().unchecked_into(),
/// |cx| view! { cx, <p>"Hello, world!"</p> }
/// );
/// # }
/// ```
pub fn mount<T, F>(parent: web_sys::HtmlElement, f: F)
where
F: Fn(Scope) -> T + 'static,
@@ -53,6 +78,20 @@ where
});
}
/// “Hydrates” server-rendered HTML, attaching event listeners and setting up reactivity
/// while reusing the existing DOM nodes, by running the given function beginning with
/// the parent node.
///
/// ```
/// // rehydrate a very simple Leptos application
/// # use leptos_dom::*; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos_macro::view;
/// # if false { // can't actually run as a doctest on any feature
/// if let Some(body) = body() {
/// hydrate(body, |cx| view! { cx, <p>"Hello, world!"</p> });
/// }
/// # }
/// ```
#[cfg(feature = "hydrate")]
pub fn hydrate<T, F>(parent: web_sys::HtmlElement, f: F)
where

View File

@@ -5,45 +5,66 @@ use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
use crate::{debug_warn, event_delegation, is_server};
thread_local! {
pub static WINDOW: web_sys::Window = web_sys::window().unwrap_throw();
pub(crate) static WINDOW: web_sys::Window = web_sys::window().unwrap_throw();
pub static DOCUMENT: web_sys::Document = web_sys::window().unwrap_throw().document().unwrap_throw();
pub(crate) static DOCUMENT: web_sys::Document = web_sys::window().unwrap_throw().document().unwrap_throw();
}
/// Returns the [`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window).
///
/// This is cached as a thread-local variable, so calling `window()` multiple times
/// requires only one call out to JavaScript.
pub fn window() -> web_sys::Window {
WINDOW.with(|window| window.clone())
}
/// Returns the [`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document).
///
/// This is cached as a thread-local variable, so calling `window()` multiple times
/// requires only one call out to JavaScript.
pub fn document() -> web_sys::Document {
DOCUMENT.with(|document| document.clone())
}
/// Returns the `<body>` elements of the current HTML document, if it exists.
pub fn body() -> Option<web_sys::HtmlElement> {
document().body()
}
/// Creates a DOM [`Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element). See
/// [`Document.createElement`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement).
pub fn create_element(tag_name: &str) -> web_sys::Element {
document().create_element(tag_name).unwrap_throw()
}
/// Creates a DOM [`Text`](https://developer.mozilla.org/en-US/docs/Web/API/Text) node. See
/// [`Document.createTextNode`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode).
pub fn create_text_node(data: &str) -> web_sys::Text {
document().create_text_node(data)
}
/// Creates a [`DocumentFragment`](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment). See
/// [`Document.createElement`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createDocumentFragment).
pub fn create_fragment() -> web_sys::DocumentFragment {
document().create_document_fragment()
}
/// Creates a [`Comment`](https://developer.mozilla.org/en-US/docs/Web/API/Comment) node.
/// See [`Document.createCommentNode`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createComment).
pub fn create_comment_node() -> web_sys::Node {
document().create_comment("").unchecked_into()
}
/// Creates an [`HTMLTemplateElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement)
/// and sets its `innerHTML` to the given HTML string.
pub fn create_template(html: &str) -> web_sys::HtmlTemplateElement {
let template = create_element("template");
template.set_inner_html(html);
template.unchecked_into()
}
/// Clones an an [`HTMLTemplateElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement)
/// and returns its first element child.
pub fn clone_template(template: &web_sys::HtmlTemplateElement) -> web_sys::Element {
template
.content()
@@ -54,18 +75,26 @@ pub fn clone_template(template: &web_sys::HtmlTemplateElement) -> web_sys::Eleme
.unchecked_into()
}
/// Appends a child node to the parent element.
/// See [`Node.appendChild`](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild).
pub fn append_child(parent: &web_sys::Element, child: &web_sys::Node) -> web_sys::Node {
parent.append_child(child).unwrap_throw()
}
/// Removes the child node from its parent element.
/// See [`Node.removeChild`](https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild).
pub fn remove_child(parent: &web_sys::Element, child: &web_sys::Node) {
_ = parent.remove_child(child);
}
/// Replaces the old node with the new one, within the parent element.
/// See [`Node.replaceChild`](https://developer.mozilla.org/en-US/docs/Web/API/Node/replaceChild).
pub fn replace_child(parent: &web_sys::Element, new: &web_sys::Node, old: &web_sys::Node) {
_ = parent.replace_child(new, old);
}
/// Inserts the new node before the existing node (or, if `None`, at the end of the parent's children.)
/// See [`Node.insertBefore`](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore).
pub fn insert_before(
parent: &web_sys::Element,
new: &web_sys::Node,
@@ -75,39 +104,43 @@ pub fn insert_before(
debug_warn!("insert_before: trying to insert on a parent node that is not an element");
new.clone()
} else if let Some(existing) = existing {
if existing.parent_node().as_ref() == Some(parent.unchecked_ref()) {
match parent.insert_before(new, Some(existing)) {
Ok(c) => c,
Err(e) => {
debug_warn!("{:?}", e.as_string());
new.clone()
}
let parent = existing.parent_node().unwrap_throw();
match parent.insert_before(new, Some(existing)) {
Ok(c) => c,
Err(e) => {
debug_warn!("{:?}", e.as_string());
new.clone()
}
} else {
debug_warn!("insert_before: existing node is not a child of parent node");
parent.append_child(new).unwrap_throw()
}
} else {
parent.append_child(new).unwrap_throw()
}
}
/// Replace the old node with the new node in the DOM.
/// See [`Element.replaceWith`](https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceWith).
pub fn replace_with(old_node: &web_sys::Element, new_node: &web_sys::Node) {
_ = old_node.replace_with_with_node_1(new_node);
}
/// Sets the text of a DOM text node.
pub fn set_data(node: &web_sys::Text, value: &str) {
node.set_data(value);
}
/// Sets the value of an attribute on a DOM element.
/// See [`Element.setAttribute`](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute).
pub fn set_attribute(el: &web_sys::Element, attr_name: &str, value: &str) {
_ = el.set_attribute(attr_name, value);
}
/// Removes an attribute from a DOM element.
/// See [`Element.removeAttribute`](https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute).
pub fn remove_attribute(el: &web_sys::Element, attr_name: &str) {
_ = el.remove_attribute(attr_name);
}
/// Sets a property on a DOM element.
pub fn set_property(el: &web_sys::Element, prop_name: &str, value: &Option<JsValue>) {
let key = JsValue::from_str(prop_name);
match value {
@@ -116,33 +149,13 @@ pub fn set_property(el: &web_sys::Element, prop_name: &str, value: &Option<JsVal
};
}
/// Returns the current [`window.location`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
pub fn location() -> web_sys::Location {
window().location()
}
pub fn descendants(el: &web_sys::Element) -> impl Iterator<Item = web_sys::Node> {
let children = el.child_nodes();
(0..children.length()).flat_map({
move |idx| {
let child = children.get(idx);
if let Some(child) = child {
// if an Element, send children
if child.node_type() == 1 {
Box::new(descendants(&child.unchecked_into()))
as Box<dyn Iterator<Item = web_sys::Node>>
}
// otherwise, just the node
else {
Box::new(std::iter::once(child)) as Box<dyn Iterator<Item = web_sys::Node>>
}
} else {
Box::new(std::iter::empty()) as Box<dyn Iterator<Item = web_sys::Node>>
}
}
})
}
/// Current window.location.hash without the beginning #
/// Current [`window.location.hash`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location)
/// without the beginning #.
pub fn location_hash() -> Option<String> {
if is_server!() {
None
@@ -151,10 +164,13 @@ pub fn location_hash() -> Option<String> {
}
}
/// Current [`window.location.pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
pub fn location_pathname() -> Option<String> {
location().pathname().ok()
}
/// Helper function to extract [`Event.target`](https://developer.mozilla.org/en-US/docs/Web/API/Event/target)
/// from any event.
pub fn event_target<T>(event: &web_sys::Event) -> T
where
T: JsCast,
@@ -162,6 +178,9 @@ where
event.target().unwrap_throw().unchecked_into::<T>()
}
/// Helper function to extract `event.target.value` from an event.
///
/// This is useful in the `on:input` or `on:change` listeners for an `<input>` element.
pub fn event_target_value(event: &web_sys::Event) -> String {
event
.target()
@@ -170,6 +189,9 @@ pub fn event_target_value(event: &web_sys::Event) -> String {
.value()
}
/// Helper function to extract `event.target.checked` from an event.
///
/// This is useful in the `on:change` listeners for an `<input type="checkbox">` element.
pub fn event_target_checked(ev: &web_sys::Event) -> bool {
ev.target()
.unwrap()
@@ -177,27 +199,22 @@ pub fn event_target_checked(ev: &web_sys::Event) -> bool {
.checked()
}
pub fn event_target_selector(ev: &web_sys::Event, selector: &str) -> bool {
matches!(
ev.target().and_then(|target| {
target
.dyn_ref::<web_sys::Element>()
.map(|el| el.closest(selector))
}),
Some(Ok(Some(_)))
)
}
/// Runs the given function between the next repaint
/// using [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
pub fn request_animation_frame(cb: impl Fn() + 'static) {
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
}
/// Queues the given function during an idle period
/// using [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
pub fn request_idle_callback(cb: impl Fn() + 'static) {
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
}
/// Executes the given function after the given duration of time has passed.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
@@ -206,15 +223,20 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
);
}
/// Handle that is generated by [set_interval] and can be used to clear the interval.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IntervalHandle(i32);
impl IntervalHandle {
/// Cancels the repeating event to which this refers.
/// See [`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)
pub fn clear(&self) {
window().clear_interval_with_handle(self.0);
}
}
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
pub fn set_interval(
cb: impl Fn() + 'static,
duration: Duration,
@@ -227,6 +249,7 @@ pub fn set_interval(
Ok(IntervalHandle(handle))
}
/// Adds an event listener to the target DOM element using implicit event delegation.
pub fn add_event_listener(
target: &web_sys::Element,
event_name: &'static str,
@@ -238,11 +261,23 @@ pub fn add_event_listener(
event_delegation::add_event_listener(event_name);
}
#[doc(hidden)]
pub fn add_event_listener_undelegated(
target: &web_sys::Element,
event_name: &'static str,
cb: impl FnMut(web_sys::Event) + 'static,
) {
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(web_sys::Event)>).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
#[doc(hidden)]
#[inline(always)]
pub fn ssr_event_listener(_cb: impl FnMut(web_sys::Event) + 'static) {
// this function exists only for type inference in templates for SSR
}
/// Adds an event listener to the `Window`.
pub fn window_event_listener(event_name: &str, cb: impl Fn(web_sys::Event) + 'static) {
if !is_server!() {
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
@@ -252,6 +287,7 @@ pub fn window_event_listener(event_name: &str, cb: impl Fn(web_sys::Event) + 'st
}
}
/// Removes all event listeners from an element.
pub fn remove_event_listeners(el: &web_sys::Element) {
let clone = el.clone_node().unwrap_throw();
replace_with(el, clone.unchecked_ref());

View File

@@ -1,12 +1,23 @@
use leptos_reactive::Scope;
use wasm_bindgen::JsValue;
/// Represents the different possible values an element property could have,
/// allowing you to do fine-grained updates to single fields.
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly.
pub enum Property {
/// A static JavaScript value.
Value(JsValue),
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
Fn(Box<dyn Fn() -> JsValue>),
}
/// Converts some type into a [Property].
///
/// This is implemented by default for Rust primitive types, [String] and friends, and [JsValue].
pub trait IntoProperty {
/// Converts the object into a [Property].
fn into_property(self, cx: Scope) -> Property;
}

View File

@@ -18,6 +18,10 @@ impl<'a> PartialEq for NodeWrapper<'a> {
impl<'a> Eq for NodeWrapper<'a> {}
/// Diffs two sets of DOM nodes and patches the parent element to match the new entry.
///
/// This is used by [insert](crate::insert). You probably don't need to use it directly.
///
// See Sycamore implementation: https://github.com/sycamore-rs/sycamore/blob/5f58fe37599e125fdc4a85cbd51e4e1c3d359791/packages/sycamore-core/src/render.rs#L237
// Copyright © 2021-2022 Luke Chu
pub fn reconcile_arrays(parent: &web_sys::Element, a: &mut [web_sys::Node], b: &[web_sys::Node]) {

View File

@@ -7,14 +7,19 @@ use crate::{
Class, Property,
};
/// Marks the node relative to which an operation should occur.
#[derive(Clone, PartialEq, Eq)]
pub enum Marker {
/// The parent has no children.
NoChildren,
/// This node is the last child of its parent.
LastChild,
/// This operation should be applied before the given node.
BeforeChild(web_sys::Node),
}
impl Marker {
/// If the marker is relative to a node, returns that node. Otherwise, returns `None`.
fn as_some_node(&self) -> Option<&web_sys::Node> {
match &self {
Self::BeforeChild(node) => Some(node),
@@ -36,6 +41,12 @@ impl std::fmt::Debug for Marker {
}
}
/// Binds the `value` to the attribute `attr_name` on this `el`. If the attribute is reactive,
/// it will create an [Effect](leptos_reactive::Effect) to make fine-grained reactive updates
/// to the attribute value.
///
/// This is used by the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html) macro.
/// You usually won't need to interact with it directly.
pub fn attribute(cx: Scope, el: &web_sys::Element, attr_name: &'static str, value: Attribute) {
match value {
Attribute::Fn(f) => {
@@ -82,6 +93,12 @@ fn attribute_expression(el: &web_sys::Element, attr_name: &str, value: Attribute
}
}
/// Binds the `value` to the property `prop_name` on this `el`. If the property is reactive,
/// it will create an [Effect](leptos_reactive::Effect) to make fine-grained reactive updates
/// to the property.
///
/// This is used by the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html) macro.
/// You usually won't need to interact with it directly.
pub fn property(cx: Scope, el: &web_sys::Element, prop_name: &'static str, value: Property) {
match value {
Property::Fn(f) => {
@@ -102,6 +119,12 @@ fn property_expression(el: &web_sys::Element, prop_name: &str, value: JsValue) {
js_sys::Reflect::set(el, &JsValue::from_str(prop_name), &value).unwrap_throw();
}
/// Binds the `value` to the class `class_name` on this `el`'s `classList`. If the class value is reactive,
/// it will create an [Effect](leptos_reactive::Effect) to make fine-grained reactive updates
/// to the class list.
///
/// This is used by the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html) macro.
/// You usually won't need to interact with it directly.
pub fn class(cx: Scope, el: &web_sys::Element, class_name: &'static str, value: Class) {
match value {
Class::Fn(f) => {
@@ -127,6 +150,12 @@ fn class_expression(el: &web_sys::Element, class_name: &str, value: bool) {
}
}
/// Inserts a child into the DOM, relative to the `before` marker. If the child is reactive,
/// it will create an [Effect](leptos_reactive::Effect) to make fine-grained reactive updates
/// to the DOM value.
///
/// This is used by the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html) macro.
/// You usually won't need to interact with it directly.
pub fn insert(
cx: Scope,
parent: web_sys::Node,
@@ -183,7 +212,7 @@ pub fn insert(
}
}
pub fn insert_expression(
fn insert_expression(
_cx: Scope,
parent: web_sys::Element,
new_value: &Child,
@@ -233,11 +262,7 @@ pub fn insert_expression(
}
Child::Null => match before {
Marker::BeforeChild(before) => {
if before.is_connected() {
Child::Node(insert_before(&parent, node, Some(before)))
} else {
Child::Node(append_child(&parent, node))
}
Child::Node(insert_before(&parent, node, Some(before)))
}
_ => Child::Node(append_child(&parent, node)),
},
@@ -277,7 +302,7 @@ pub fn insert_expression(
Child::Nodes(new_nodes.to_vec())
}
} else {
clean_children(&parent, Child::Null, &Marker::NoChildren, None);
clean_children(&parent, Child::Null, before, None);
append_nodes(parent, new_nodes.to_vec(), before.as_some_node().cloned());
Child::Nodes(new_nodes.to_vec())
}
@@ -293,7 +318,7 @@ pub fn insert_expression(
}
}
pub fn insert_str(
fn insert_str(
parent: &web_sys::Element,
data: &str,
before: &Marker,

View File

@@ -1,10 +1,12 @@
use cfg_if::cfg_if;
use std::borrow::Cow;
/// Encodes strings to be used as text in HTML by escaping `&`, `<`, and `>`.
pub fn escape_text(text: &str) -> Cow<'_, str> {
html_escape::encode_text(text)
}
/// Encodes strings to be used as attribute values in HTML by escaping `&`, `<`, `>`, and `"`.
pub fn escape_attr(text: &str) -> Cow<'_, str> {
html_escape::encode_double_quoted_attribute(text)
}
@@ -16,6 +18,18 @@ cfg_if! {
use crate::Element;
use futures::{stream::FuturesUnordered, Stream, StreamExt};
/// Renders a component to a stream of HTML strings.
///
/// This renders:
/// 1) the application shell
/// a) HTML for everything that is not under a `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
/// 2) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
/// 3) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
pub fn render_to_stream(view: impl Fn(Scope) -> Element + 'static) -> impl Stream<Item = String> {
let ((shell, pending_resources, pending_fragments, serializers), _, disposer) =
run_scope_undisposed({
@@ -54,6 +68,10 @@ cfg_if! {
"#
)
})
// TODO this is wrong: it should merge the next two streams, not chain them
// you may well need to resolve some fragments before some of the resources are resolved
// stream data for each Resource as it resolves
.chain(serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.0.11"
version = "0.0.16"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -11,18 +11,20 @@ description = "view macro for the Leptos web framework."
proc-macro = true
[dependencies]
cfg-if = "1"
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
syn-rsx = "0.8.1"
syn-rsx = "0.9"
uuid = { version = "1", features = ["v4"] }
leptos_dom = { path = "../leptos_dom", version = "0.0.11" }
leptos_reactive = { path = "../leptos_reactive", version = "0.0.11" }
leptos_dom = { path = "../leptos_dom", version = "0.0.16" }
leptos_reactive = { path = "../leptos_reactive", version = "0.0.16" }
[dev-dependencies]
log = "0.4"
typed-builder = "0.10"
leptos = { path = "../leptos", version = "0.0" }
[features]
default = ["ssr"]

View File

@@ -1,8 +1,10 @@
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
use proc_macro::{TokenStream, TokenTree};
use quote::ToTokens;
use server::server_macro_impl;
use syn::{parse_macro_input, DeriveInput};
use syn_rsx::{parse, Node, NodeType};
use syn_rsx::{parse, NodeElement};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum Mode {
@@ -83,9 +85,13 @@ mod server;
/// ```
///
/// 4. Dynamic content can be wrapped in curly braces (`{ }`) to insert text nodes, elements, or set attributes.
/// If you insert signal here, Leptos will create an effect to update the DOM whenever the value changes.
/// If you insert a signal here, Leptos will create an effect to update the DOM whenever the value changes.
/// *(“Signal” here means `Fn() -> T` where `T` is the appropriate type for that node: a `String` in case
/// of text nodes, a `bool` for `class:` attributes, etc.)*
///
/// Attributes can take a wide variety of primitive types that can be converted to strings. They can also
/// take an `Option`, in which case `Some` sets the attribute and `None` removes the attribute.
///
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast; use leptos_dom as leptos; use leptos_dom::Marker;
/// # run_scope(|cx| {
@@ -105,7 +111,7 @@ mod server;
/// # });
/// ```
///
/// 5. Event handlers can be added with `on:` attributes
/// 5. Event handlers can be added with `on:` attributes.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
@@ -124,7 +130,8 @@ mod server;
/// ```
///
/// 6. DOM properties can be set with `prop:` attributes, which take any primitive type or `JsValue` (or a signal
/// that returns a primitive or JsValue).
/// that returns a primitive or JsValue). They can also take an `Option`, in which case `Some` sets the property
/// and `None` deletes the property.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
@@ -152,7 +159,34 @@ mod server;
/// # run_scope(|cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 2);
/// view! { cx, <div class:hidden={move || count() < 3}>"Now you see me, now you dont."</div> }
/// view! { cx, <div class:hidden-div={move || count() < 3}>"Now you see me, now you dont."</div> }
/// # ;
/// # }
/// # });
/// ```
///
/// Class names can include dashes, but cannot (at the moment) include a dash-separated segment of only numbers.
/// ```rust,compile_fail
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 2);
/// // `hidden-div-25` is invalid at the moment
/// view! { cx, <div class:hidden-div-25={move || count() < 3}>"Now you see me, now you dont."</div> }
/// # ;
/// # }
/// # });
/// ```
///
/// 8. You can use the `_ref` attribute to store a reference to its DOM element in a variable to use later.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (value, set_value) = create_signal(cx, 0);
/// let my_input: Element;
/// view! { cx, <input type="text" _ref=my_input/> }
/// // `my_input` now contains an `Element` that we can use anywhere
/// # ;
/// # }
/// # });
@@ -241,13 +275,10 @@ pub fn params_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream
params::impl_params(&ast)
}
pub(crate) fn is_component_node(node: &Node) -> bool {
if let NodeType::Element = node.node_type {
node.name_as_string()
.and_then(|node_name| node_name.chars().next())
.map(|first_char| first_char.is_ascii_uppercase())
.unwrap_or(false)
} else {
false
}
pub(crate) fn is_component_node(node: &NodeElement) -> bool {
let name = node.name.to_string();
let first_char = name.chars().next();
first_char
.map(|first_char| first_char.is_ascii_uppercase())
.unwrap_or(false)
}

View File

@@ -356,7 +356,7 @@ mod struct_info {
{
field.type_from_inside_option().ok_or_else(|| {
Error::new_spanned(
&field_type,
field_type,
"can't `strip_option` - field is not `Option<...>`",
)
})?

View File

@@ -1,6 +1,6 @@
// Credit to Dioxus: https://github.com/DioxusLabs/dioxus/blob/master/packages/core-macro/src/Server.rs
use proc_macro2::{TokenStream as TokenStream2};
use cfg_if::cfg_if;
use proc_macro::Span;
use proc_macro2::{TokenStream as TokenStream2, Literal};
use quote::{quote};
use syn::{
parse::{Parse, ParseStream},
@@ -9,13 +9,24 @@ use syn::{
};
pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Result<TokenStream2> {
let ServerFnName { struct_name } = syn::parse::<ServerFnName>(args)?;
let ServerFnName { struct_name, prefix, .. } = syn::parse::<ServerFnName>(args)?;
let prefix = prefix.unwrap_or_else(|| Literal::string(""));
let body = syn::parse::<ServerFnBody>(s.into())?;
let fn_name = &body.ident;
let fn_name_as_str = body.ident.to_string();
let vis = body.vis;
let block = body.block;
cfg_if! {
if #[cfg(not(feature = "stable"))] {
let span = Span::call_site();
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("/", "-");
} else {
let url = fn_name_as_str;
}
}
let fields = body.inputs.iter().map(|f| {
let typed_arg = match f {
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
@@ -96,7 +107,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
};
Ok(quote::quote! {
#[derive(Clone)]
#[derive(Clone, ::serde::Serialize, ::serde::Deserialize)]
pub struct #struct_name {
#(#fields),*
}
@@ -104,21 +115,12 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
impl ServerFn for #struct_name {
type Output = #output_ty;
fn prefix() -> &'static str {
#prefix
}
fn url() -> &'static str {
#fn_name_as_str
}
fn as_form_data(&self) -> Vec<(&'static str, String)> {
vec![
#(#as_form_data_fields),*
]
}
fn from_form_data(data: &[u8]) -> Result<Self, ServerFnError> {
let data = ::leptos::leptos_server::form_urlencoded::parse(data).collect::<Vec<_>>();
Ok(Self {
#(#from_form_data_fields),*
})
#url
}
#[cfg(feature = "ssr")]
@@ -129,8 +131,8 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
#[cfg(not(feature = "ssr"))]
fn call_fn_client(self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
let #struct_name { #(#field_names_3),* } = self;
Box::pin(async move { #fn_name( #(#field_names_4),*).await })
let #struct_name { #(#field_names_3),* } = self;
Box::pin(async move { #fn_name( #(#field_names_4),*).await })
}
}
@@ -140,20 +142,26 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
}
#[cfg(not(feature = "ssr"))]
#vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty {
::leptos::call_server_fn(#struct_name::url(), #struct_name { #(#field_names_5),* }).await
let prefix = #struct_name::prefix().to_string();
let url = prefix + "/" + #struct_name::url();
::leptos::call_server_fn(&url, #struct_name { #(#field_names_5),* }).await
}
})
}
pub struct ServerFnName {
struct_name: Ident,
comma: Option<Token![,]>,
prefix: Option<Literal>
}
impl Parse for ServerFnName {
fn parse(input: ParseStream) -> syn::Result<Self> {
let struct_name = input.parse()?;
let comma = input.parse()?;
let prefix = input.parse()?;
Ok(Self { struct_name })
Ok(Self { struct_name, comma, prefix })
}
}
@@ -207,97 +215,4 @@ impl Parse for ServerFnBody {
attrs,
})
}
}
/*
/// Serialize the same way, regardless of flavor
impl ToTokens for ServerFnBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let Self {
vis,
ident,
generics,
inputs,
output,
where_clause,
block,
attrs,
..
} = self;
let fields = inputs.iter().map(|f| {
let typed_arg = match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => t,
};
if let Type::Path(pat) = &*typed_arg.ty {
if pat.path.segments[0].ident == "Option" {
quote! {
#[builder(default, setter(strip_option))]
#vis #f
}
} else {
quote! { #vis #f }
}
} else {
quote! { #vis #f }
}
});
let struct_name = Ident::new(&format!("{}Props", ident), Span::call_site());
let field_names = inputs.iter().filter_map(|f| match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => Some(&t.pat),
});
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
Some(lt)
} else {
None
};
out_tokens.append_all(quote! {
#[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
pub struct #struct_name {}
#[async_trait]
impl ServerFn for #struct_name {
type Output = i32;
fn url() -> &'static str {
"get_server_count"
}
fn as_form_data(&self) -> Vec<(&'static str, String)> {
vec![]
}
#[cfg(feature = "ssr")]
async fn call_fn(self) -> Result<Self::Output, ServerFnError> {
get_server_count().await
}
}
#[cfg(feature = "ssr")]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
Ok(COUNT.load(Ordering::Relaxed))
}
#[cfg(not(feature = "ssr"))]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
call_server_fn(#struct_name::url(), #struct_name {}).await
}
#[cfg(not(feature = "ssr"))]
pub async fn get_server_count_helper(args: #struct_name) -> Result<i32, ServerFnError> {
call_server_fn(#struct_name::url(), args).await
}
});
}
}
/*
*/
*/
}

View File

@@ -1,11 +1,13 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, ExprPath};
use syn_rsx::{Node, NodeName, NodeType};
use syn_rsx::{Node, NodeName, NodeElement, NodeAttribute, NodeValueExpr};
use uuid::Uuid;
use crate::{is_component_node, Mode};
const NON_BUBBLING_EVENTS: [&str; 11] = ["load", "unload", "scroll", "focus", "blur", "loadstart", "progress", "error", "abort", "load", "loadend"];
pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream {
let template_uid = Ident::new(
&format!("TEMPLATE_{}", Uuid::new_v4().simple()),
@@ -29,9 +31,9 @@ pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream
}
fn first_node_to_tokens(cx: &Ident, template_uid: &Ident, node: &Node, mode: Mode) -> TokenStream {
match node.node_type {
NodeType::Doctype | NodeType::Comment => quote! {},
NodeType::Fragment => {
match node {
Node::Doctype(_) | Node::Comment(_) => quote! {},
Node::Fragment(node) => {
let nodes = node
.children
.iter()
@@ -44,14 +46,15 @@ fn first_node_to_tokens(cx: &Ident, template_uid: &Ident, node: &Node, mode: Mod
}
}
}
NodeType::Element => root_element_to_tokens(cx, template_uid, node, mode),
NodeType::Block => node
.value
.as_ref()
.map(|value| quote! { #value })
.expect("root Block node with no value"),
NodeType::Text => {
let value = node.value_as_string().unwrap();
Node::Element(node) => root_element_to_tokens(cx, template_uid, node, mode),
Node::Block(node) => {
let value = node.value.as_ref();
quote! {
#value
}
}
Node::Text(node) => {
let value = node.value.as_ref();
quote! {
#value
}
@@ -63,7 +66,7 @@ fn first_node_to_tokens(cx: &Ident, template_uid: &Ident, node: &Node, mode: Mod
fn root_element_to_tokens(
cx: &Ident,
template_uid: &Ident,
node: &Node,
node: &NodeElement,
mode: Mode,
) -> TokenStream {
let mut template = String::new();
@@ -85,6 +88,7 @@ fn root_element_to_tokens(
&mut expressions,
true,
mode,
false
);
match mode {
@@ -116,7 +120,7 @@ fn root_element_to_tokens(
}
};
let span = node.name_span().unwrap();
let span = node.name.span();
let navigations = if navigations.is_empty() {
quote! {}
@@ -152,14 +156,21 @@ fn root_element_to_tokens(
#[derive(Clone, Debug)]
enum PrevSibChange {
Sib(Ident),
//Parent,
Parent,
Skip,
}
fn attributes(node: &NodeElement) -> impl Iterator<Item = &NodeAttribute> {
node
.attributes
.iter()
.filter_map(|node| if let Node::Attribute(attribute) = node { Some(attribute )} else { None })
}
#[allow(clippy::too_many_arguments)]
fn element_to_tokens(
cx: &Ident,
node: &Node,
node: &NodeElement,
parent: &Ident,
prev_sib: Option<Ident>,
next_el_id: &mut usize,
@@ -169,14 +180,16 @@ fn element_to_tokens(
expressions: &mut Vec<TokenStream>,
is_root_el: bool,
mode: Mode,
parent_is_template: bool,
) -> Ident {
// create this element
*next_el_id += 1;
let this_el_ident = child_ident(*next_el_id, node);
let this_el_ident = child_ident(*next_el_id, node.name.span());
// Open tag
let name_str = node.name_as_string().unwrap();
let span = node.name_span().unwrap();
let name_str = node.name.to_string();
let is_template = name_str.to_lowercase() == "template";
let span = node.name.span();
if mode == Mode::Ssr {
// SSR, push directly to buffer
@@ -200,32 +213,31 @@ fn element_to_tokens(
}
// for SSR: merge all class: attributes and class attribute
if mode == Mode::Ssr {
let class_attr = node
.attributes
.iter()
.find(|a| a.name_as_string() == Some("class".into()))
if mode == Mode::Ssr {
let class_attr = attributes(node).find(|a| a.key.to_string() == "class")
.map(|node| {
(node.name_span().expect("no span for class attribute node"), node.value_as_string().unwrap_or_default().trim().to_string())
(node.key.span(), node.value.as_ref().and_then(|n| String::try_from(n).ok()).unwrap_or_default().trim().to_string())
});
let class_attrs = node
.attributes
.iter()
.filter_map(|node| {
node.name_as_string().and_then(|name| {
if name.starts_with("class:") {
let name = name.replacen("class:", "", 1);
let value = node.value.as_ref().expect("class: attributes need values");
let span = node.name_span().expect("missing span for class name");
Some(quote_spanned! {
span => leptos_buffer.push(' ');
leptos_buffer.push_str(&{#value}.into_class(#cx).as_value_string(#name));
})
let class_attrs = attributes(node).filter_map(|node| {
let name = node.key.to_string();
if name.starts_with("class:") || name.starts_with("class-") {
let name = if name.starts_with("class:") {
name.replacen("class:", "", 1)
} else if name.starts_with("class-") {
name.replacen("class-", "", 1)
} else {
None
}
})
name
};
let value = node.value.as_ref().expect("class: attributes need values").as_ref();
let span = node.key.span();
Some(quote_spanned! {
span => leptos_buffer.push(' ');
leptos_buffer.push_str(&{#value}.into_class(#cx).as_value_string(#name));
})
} else {
None
}
})
.collect::<Vec<_>>();
@@ -248,9 +260,9 @@ fn element_to_tokens(
}
// attributes
for attr in &node.attributes {
for attr in attributes(node) {
// SSR class attribute has just been handled
if !(mode == Mode::Ssr && attr.name_as_string().unwrap() == "class") {
if !(mode == Mode::Ssr && attr.key.to_string() == "class") {
attr_to_tokens(
cx,
attr,
@@ -264,13 +276,13 @@ fn element_to_tokens(
}
// navigation for this el
let debug_name = debug_name(node);
let debug_name = node.name.to_string();
if mode != Mode::Ssr {
let this_nav = if is_root_el {
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident = #parent.clone().unchecked_into::<web_sys::Node>();
//log::debug!("=> got {}", #this_el_ident.node_name());
//debug!("=> got {}", #this_el_ident.node_name());
}
} else if let Some(prev_sib) = &prev_sib {
quote_spanned! {
@@ -279,6 +291,13 @@ fn element_to_tokens(
let #this_el_ident = #prev_sib.next_sibling().unwrap_throw();
//log::debug!("=> got {}", #this_el_ident.node_name());
}
} else if parent_is_template {
quote_spanned! {
span => let #this_el_ident = #debug_name;
//log::debug!("first_child ({})", #debug_name);
let #this_el_ident = #parent.unchecked_ref::<web_sys::HtmlTemplateElement>().content().first_child().unwrap_throw();
//log::debug!("=> got {}", #this_el_ident.node_name());
}
} else {
quote_spanned! {
span => let #this_el_ident = #debug_name;
@@ -345,11 +364,13 @@ fn element_to_tokens(
expressions,
multi,
mode,
idx == 0,
is_template
);
prev_sib = match curr_id {
PrevSibChange::Sib(id) => Some(id),
//PrevSibChange::Parent => None,
PrevSibChange::Parent => None,
PrevSibChange::Skip => prev_sib,
};
}
@@ -375,36 +396,54 @@ fn next_sibling_node(children: &[Node], idx: usize, next_el_id: &mut usize) -> O
None
} else {
let sibling = &children[idx];
if is_component_node(sibling) {
next_sibling_node(children, idx + 1, next_el_id)
} else {
Some(child_ident(*next_el_id + 1, sibling))
match sibling {
Node::Element(sibling) => {
if is_component_node(sibling) {
next_sibling_node(children, idx + 1, next_el_id)
} else {
Some(child_ident(*next_el_id + 1, sibling.name.span()))
}
},
Node::Block(sibling) => if idx > 0 {
Some(child_ident(*next_el_id + 2, sibling.value.span()))
} else {
Some(child_ident(*next_el_id + 1, sibling.value.span()))
}
Node::Text(sibling) => if idx > 0 {
Some(child_ident(*next_el_id + 2, sibling.value.span()))
} else {
Some(child_ident(*next_el_id + 1, sibling.value.span()))
},
_ => panic!("expected either an element or a block")
}
}
}
fn attr_to_tokens(
cx: &Ident,
node: &Node,
node: &NodeAttribute,
el_id: &Ident,
template: &mut String,
expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
mode: Mode,
) {
let name = node
.name_as_string()
.expect("Attribute nodes must have strings as names.");
let name = node.key.to_string();
let name = if name.starts_with('_') {
name.replacen('_', "", 1)
} else {
name
};
let name = if name.starts_with("attr:") {
name.replacen("attr:", "", 1)
} else {
name
};
let value = match &node.value {
Some(expr) => match expr {
Some(expr) => match expr.as_ref() {
syn::Expr::Lit(expr_lit) => {
if matches!(expr_lit.lit, syn::Lit::Str(_)) {
AttributeValue::Static(node.value_as_string().unwrap())
if let syn::Lit::Str(s) = &expr_lit.lit {
AttributeValue::Static(s.value())
} else {
AttributeValue::Dynamic(expr)
}
@@ -414,7 +453,7 @@ fn attr_to_tokens(
None => AttributeValue::Empty,
};
let span = node.name_span().unwrap();
let span = node.key.span();
// refs
if name == "ref" {
@@ -457,16 +496,23 @@ fn attr_to_tokens(
}
// Event Handlers
else if name.starts_with("on:") {
let handler = node
.value
.as_ref()
.expect("event listener attributes need a value");
let handler = node
.value
.as_ref()
.expect("event listener attributes need a value")
.as_ref();
if mode != Mode::Ssr {
let event_name = name.replacen("on:", "", 1);
expressions.push(quote_spanned! {
span => add_event_listener(#el_id.unchecked_ref(), #event_name, #handler);
});
let name = name.replacen("on:", "", 1);
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener_undelegated(#el_id.unchecked_ref(), #name, #handler);
});
} else {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener(#el_id.unchecked_ref(), #name, #handler);
});
}
} else {
// this is here to avoid warnings about unused signals
// that are used in event listeners. I'm open to better solutions.
@@ -476,23 +522,23 @@ fn attr_to_tokens(
}
}
// Properties
else if name.starts_with("prop:") {
else if name.starts_with("prop:") {
let name = name.replacen("prop:", "", 1);
// can't set properties in SSR
if mode != Mode::Ssr {
let name = name.replacen("prop:", "", 1);
let value = node.value.as_ref().expect("prop: blocks need values");
if mode != Mode::Ssr {
let value = node.value.as_ref().expect("prop: blocks need values").as_ref();
expressions.push(quote_spanned! {
span => leptos_dom::property(#cx, #el_id.unchecked_ref(), #name, #value.into_property(#cx))
});
span => leptos_dom::property(#cx, #el_id.unchecked_ref(), #name, #value.into_property(#cx))
});
}
}
// Classes
else if name.starts_with("class:") {
else if name.starts_with("class:") {
let name = name.replacen("class:", "", 1);
if mode == Mode::Ssr {
// handled separately because they need to be merged
} else {
let name = name.replacen("class:", "", 1);
let value = node.value.as_ref().expect("class: attributes need values");
let value = node.value.as_ref().expect("class: attributes need values").as_ref();
expressions.push(quote_spanned! {
span => leptos_dom::class(#cx, #el_id.unchecked_ref(), #name, #value.into_class(#cx))
});
@@ -571,9 +617,11 @@ fn child_to_tokens(
expressions: &mut Vec<TokenStream>,
multi: bool,
mode: Mode,
is_first_child: bool,
parent_is_template: bool,
) -> PrevSibChange {
match node.node_type {
NodeType::Element => {
match node {
Node::Element(node) => {
if is_component_node(node) {
component_to_tokens(
cx,
@@ -588,6 +636,7 @@ fn child_to_tokens(
next_co_id,
multi,
mode,
is_first_child
)
} else {
PrevSibChange::Sib(element_to_tokens(
@@ -602,138 +651,172 @@ fn child_to_tokens(
expressions,
false,
mode,
parent_is_template
))
}
}
NodeType::Text | NodeType::Block => {
let str_value = node.value.as_ref().and_then(|expr| match expr {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(s) => Some(s.value()),
syn::Lit::Char(c) => Some(c.value().to_string()),
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
_ => None,
},
_ => None,
});
let current: Option<Ident> = None;
// code to navigate to this text node
let span = node
.value
.as_ref()
.map(|val| val.span())
.unwrap_or_else(Span::call_site);
*next_el_id += 1;
let name = child_ident(*next_el_id, node);
let location = if let Some(sibling) = &prev_sib {
quote_spanned! {
span => //log::debug!("-> next sibling");
let #name = #sibling.next_sibling().unwrap_throw();
//log::debug!("\tnext sibling = {}", #name.node_name());
}
} else {
quote_spanned! {
span => //log::debug!("\\|/ first child on {}", #parent.node_name());
let #name = #parent.first_child().unwrap_throw();
//log::debug!("\tfirst child = {}", #name.node_name());
}
};
let before = match &next_sib {
Some(child) => quote! { leptos::Marker::BeforeChild(#child.clone()) },
None => {
/* if multi {
quote! { leptos::Marker::LastChild }
} else {
quote! { leptos::Marker::LastChild }
} */
quote! { leptos::Marker::LastChild }
}
};
let value = node.value.as_ref().expect("no block value");
if let Some(v) = str_value {
if mode == Mode::Ssr {
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push_str(&leptos_dom::escape_text(&#v));
});
} else {
navigations.push(location);
template.push_str(&v);
}
PrevSibChange::Sib(name)
} else {
// these markers are one of the primary templating differences across modes
match mode {
// in CSR, simply insert a comment node: it will be picked up and replaced with the value
Mode::Client => {
template.push_str("<!>");
navigations.push(location);
let current = match current {
Some(i) => quote! { Some(#i.into_child(#cx)) },
None => quote! { None },
};
expressions.push(quote! {
leptos::insert(
#cx,
#parent.clone(),
#value.into_child(#cx),
#before,
#current,
);
});
}
// when hydrating, a text node will be generated by SSR; in the hydration/CSR template,
// wrap it with comments that mark where it begins and ends
Mode::Hydrate => {
//*next_el_id += 1;
let el = child_ident(*next_el_id, node);
*next_co_id += 1;
let co = comment_ident(*next_co_id, node);
//next_sib = Some(el.clone());
template.push_str("<!#><!/>");
navigations.push(quote! {
#location;
let (#el, #co) = #cx.get_next_marker(&#name);
//log::debug!("get_next_marker => {}", #el.node_name());
});
expressions.push(quote! {
leptos::insert(
#cx,
#parent.clone(),
#value.into_child(#cx),
#before,
Some(Child::Nodes(#co)),
);
});
//current = Some(el);
}
// in SSR, it needs to insert the value, wrapped in comments
Mode::Ssr => expressions.push(quote::quote_spanned! {
span => leptos_buffer.push_str("<!--#-->");
leptos_buffer.push_str(&#value.into_child(#cx).as_child_string());
leptos_buffer.push_str("<!--/-->");
}),
}
PrevSibChange::Sib(name)
}
Node::Text(node) => {
block_to_tokens(cx, &node.value, node.value.span(), parent, prev_sib, next_sib, next_el_id, next_co_id, template, expressions, navigations, mode, is_first_child)
}
Node::Block(node) => {
block_to_tokens(cx, &node.value, node.value.span(), parent, prev_sib, next_sib, next_el_id, next_co_id, template, expressions, navigations, mode, is_first_child)
}
_ => panic!("unexpected child node type"),
}
}
#[allow(clippy::too_many_arguments)]
fn block_to_tokens(
cx: &Ident,
value: &NodeValueExpr,
span: Span,
parent: &Ident,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
mode: Mode,
is_first_child: bool
) -> PrevSibChange {
let value = value.as_ref();
let str_value = match value {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(s) => Some(s.value()),
syn::Lit::Char(c) => Some(c.value().to_string()),
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
_ => None,
},
_ => None
};
let current: Option<Ident> = None;
// code to navigate to this text node
let (name, location) = if is_first_child && mode == Mode::Client {
(None, quote! { })
}
else {
*next_el_id += 1;
let name = child_ident(*next_el_id, span);
let location = if let Some(sibling) = &prev_sib {
quote_spanned! {
span => //log::debug!("-> next sibling");
let #name = #sibling.next_sibling().unwrap_throw();
//log::debug!("\tnext sibling = {}", #name.node_name());
}
} else {
quote_spanned! {
span => //log::debug!("\\|/ first child on {}", #parent.node_name());
let #name = #parent.first_child().unwrap_throw();
//log::debug!("\tfirst child = {}", #name.node_name());
}
};
(Some(name), location)
};
let before = match &next_sib {
Some(child) => quote! { leptos::Marker::BeforeChild(#child.clone()) },
None => {
/* if multi {
quote! { leptos::Marker::LastChild }
} else {
quote! { leptos::Marker::LastChild }
} */
quote! { leptos::Marker::LastChild }
}
};
if let Some(v) = str_value {
if mode == Mode::Ssr {
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push_str(&leptos_dom::escape_text(&#v));
});
} else {
navigations.push(location);
template.push_str(&v);
}
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
} else {
// these markers are one of the primary templating differences across modes
match mode {
// in CSR, simply insert a comment node: it will be picked up and replaced with the value
Mode::Client => {
if !is_first_child {
template.push_str("<!>");
}
navigations.push(location);
let current = match current {
Some(i) => quote! { Some(#i.into_child(#cx)) },
None => quote! { None },
};
expressions.push(quote! {
leptos::insert(
#cx,
#parent.clone(),
#value.into_child(#cx),
#before,
#current,
);
});
}
// when hydrating, a text node will be generated by SSR; in the hydration/CSR template,
// wrap it with comments that mark where it begins and ends
Mode::Hydrate => {
//*next_el_id += 1;
let el = child_ident(*next_el_id, span);
*next_co_id += 1;
let co = comment_ident(*next_co_id, span);
//next_sib = Some(el.clone());
template.push_str("<!#><!/>");
navigations.push(quote! {
#location;
let (#el, #co) = #cx.get_next_marker(&#name);
//log::debug!("get_next_marker => {}", #el.node_name());
});
expressions.push(quote! {
leptos::insert(
#cx,
#parent.clone(),
#value.into_child(#cx),
#before,
Some(Child::Nodes(#co)),
);
});
//current = Some(el);
}
// in SSR, it needs to insert the value, wrapped in comments
Mode::Ssr => expressions.push(quote::quote_spanned! {
span => leptos_buffer.push_str("<!--#-->");
leptos_buffer.push_str(&#value.into_child(#cx).as_child_string());
leptos_buffer.push_str("<!--/-->");
}),
}
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
}
}
#[allow(clippy::too_many_arguments)]
fn component_to_tokens(
cx: &Ident,
node: &Node,
node: &NodeElement,
parent: Option<&Ident>,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
@@ -744,9 +827,10 @@ fn component_to_tokens(
next_co_id: &mut usize,
multi: bool,
mode: Mode,
is_first_child: bool
) -> PrevSibChange {
let create_component = create_component(cx, node, mode);
let span = node.name_span().unwrap();
let span = node.name.span();
let mut current = None;
@@ -773,9 +857,9 @@ fn component_to_tokens(
} else if mode == Mode::Hydrate {
//let name = child_ident(*next_el_id, node);
*next_el_id += 1;
let el = child_ident(*next_el_id, node);
let el = child_ident(*next_el_id, node.name.span());
*next_co_id += 1;
let co = comment_ident(*next_co_id, node);
let co = comment_ident(*next_co_id, node.name.span());
//next_sib = Some(el.clone());
let starts_at = if let Some(prev_sib) = prev_sib {
@@ -831,13 +915,17 @@ fn component_to_tokens(
match current {
Some(el) => PrevSibChange::Sib(el),
None => PrevSibChange::Skip,
None => if is_first_child {
PrevSibChange::Parent
} else {
PrevSibChange::Skip
},
}
}
fn create_component(cx: &Ident, node: &Node, mode: Mode) -> TokenStream {
let component_name = ident_from_tag_name(node.name.as_ref().unwrap());
let span = node.name_span().unwrap();
fn create_component(cx: &Ident, node: &NodeElement, mode: Mode) -> TokenStream {
let component_name = ident_from_tag_name(&node.name);
let span = node.name.span();
let component_props_name = Ident::new(&format!("{component_name}Props"), span);
let (initialize_children, children) = if node.children.is_empty() {
@@ -872,8 +960,8 @@ fn create_component(cx: &Ident, node: &Node, mode: Mode) -> TokenStream {
}
};
let props = node.attributes.iter().filter_map(|attr| {
let attr_name = attr.name_as_string().unwrap_or_default();
let props = attributes(node).filter_map(|attr| {
let attr_name = attr.key.to_string();
if attr_name.starts_with("on:")
|| attr_name.starts_with("prop:")
|| attr_name.starts_with("class:")
@@ -881,12 +969,15 @@ fn create_component(cx: &Ident, node: &Node, mode: Mode) -> TokenStream {
{
None
} else {
let name = ident_from_tag_name(attr.name.as_ref().unwrap());
let span = attr.name_span().unwrap();
let name = ident_from_tag_name(&attr.key);
let span = attr.key.span();
let value = attr
.value
.as_ref()
.map(|v| quote_spanned! { span => #v })
.map(|v| {
let v = v.as_ref();
quote_spanned! { span => #v }
})
.unwrap_or_else(|| quote_spanned! { span => #name });
Some(quote_spanned! {
span => .#name(#value)
@@ -894,43 +985,43 @@ fn create_component(cx: &Ident, node: &Node, mode: Mode) -> TokenStream {
}
});
let mut other_attrs = node.attributes.iter().filter_map(|attr| {
let attr_name = attr.name_as_string().unwrap_or_default();
let mut other_attrs = attributes(node).filter_map(|attr| {
let attr_name = attr.key.to_string();
let span = attr.key.span();
let value = attr.value.as_ref().map(|e| e.as_ref());
// Event Listeners
if let Some(event_name) = attr_name.strip_prefix("on:") {
let span = attr.name_span().unwrap();
let handler = attr
.value
.as_ref()
.expect("event listener attributes need a value");
Some(quote_spanned! {
span => add_event_listener(#component_name.unchecked_ref(), #event_name, #handler)
})
.expect("on: event listener attributes need a value")
.as_ref();
if NON_BUBBLING_EVENTS.contains(&event_name) {
Some(quote_spanned! {
span => ::leptos::add_event_listener_undelegated(#component_name.unchecked_ref(), #event_name, #handler);
})
} else {
Some(quote_spanned! {
span => ::leptos::add_event_listener(#component_name.unchecked_ref(), #event_name, #handler)
})
}
}
// Properties
else if let Some(name) = attr_name.strip_prefix("prop:") {
let value = attr.value.as_ref().expect("prop: attributes need values");
Some(quote_spanned! {
span => leptos_dom::property(#cx, #component_name.unchecked_ref(), #name, #value.into_property(#cx))
})
}
// Classes
else if let Some(name) = attr_name.strip_prefix("class:") {
let value = attr.value.as_ref().expect("class: attributes need values");
Some(quote_spanned! {
span => leptos_dom::class(#cx, #component_name.unchecked_ref(), #name, #value.into_class(#cx))
})
}
// Attributes
else if let Some(name) = attr_name.strip_prefix("attr:") {
let value = attr.value.as_ref().expect("attr: attributes need values");
let name = name.replace('_', "-");
Some(quote_spanned! {
else { attr_name.strip_prefix("attr:").map(|name| quote_spanned! {
span => leptos_dom::attribute(#cx, #component_name.unchecked_ref(), #name, #value.into_attribute(#cx))
})
}
else {
None
}
}) }
}).peekable();
if other_attrs.peek().is_none() {
@@ -964,34 +1055,19 @@ fn create_component(cx: &Ident, node: &Node, mode: Mode) -> TokenStream {
}
}
fn debug_name(node: &Node) -> String {
node.name_as_string().unwrap_or_else(|| {
node.value_as_string()
.expect("expected either node name or value")
})
}
/* fn span(node: &Node) -> Span {
node.name_span()
.unwrap_or_else(|| node.value.as_ref().unwrap().span())
} */
fn child_ident(el_id: usize, node: &Node) -> Ident {
fn child_ident(el_id: usize, span: Span) -> Ident {
let id = format!("_el{el_id}");
match node.node_type {
NodeType::Element => Ident::new(&id, node.name_span().unwrap()),
NodeType::Text | NodeType::Block => Ident::new(&id, node.value.as_ref().unwrap().span()),
_ => panic!("invalid child node type"),
}
Ident::new(&id, span)
}
fn comment_ident(co_id: usize, node: &Node) -> Ident {
fn comment_ident(co_id: usize, span: Span) -> Ident {
let id = format!("_co{co_id}");
match node.node_type {
NodeType::Element => Ident::new(&id, node.name_span().unwrap()),
NodeType::Text | NodeType::Block => Ident::new(&id, node.value.as_ref().unwrap().span()),
_ => panic!("invalid child node type"),
}
Ident::new(&id, span)
}
fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
@@ -1005,7 +1081,7 @@ fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
.expect("element needs to have a name"),
NodeName::Block(_) => panic!("blocks not allowed in tag-name position"),
_ => Ident::new(
&tag_name.to_string().replace('-', "_").replace(':', "_"),
&tag_name.to_string().replace(['-', ':'], "_"),
tag_name.span(),
),
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_reactive"
version = "0.0.11"
version = "0.0.16"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -1,4 +1,5 @@
use crate::{debug_warn, Runtime, Scope, ScopeProperty};
use cfg_if::cfg_if;
use std::fmt::Debug;
/// Effects run a certain chunk of code whenever the signals they depend on change.
@@ -39,21 +40,22 @@ use std::fmt::Debug;
/// // and easily lead to problems like infinite loops
/// set_b(a() + 1);
/// });
/// # if !cfg!(feature = "ssr") {
/// # assert_eq!(b(), 2);
/// # }
/// # }).dispose();
/// ```
#[cfg(not(feature = "ssr"))]
pub fn create_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
where
T: Debug + 'static,
{
create_isomorphic_effect(cx, f);
}
#[cfg(feature = "ssr")]
pub fn create_effect<T>(_cx: Scope, _f: impl FnMut(Option<T>) -> T + 'static)
where
T: Debug + 'static,
{
cfg_if! {
if #[cfg(not(feature = "ssr"))] {
create_isomorphic_effect(cx, f);
} else {
{ }
}
}
}
/// Creates an effect; unlike effects created by [create_effect], isomorphic effects will run on
@@ -98,7 +100,10 @@ where
create_effect(cx, f);
}
slotmap::new_key_type! { pub struct EffectId; }
slotmap::new_key_type! {
/// Unique ID assigned to an [Effect](crate::Effect).
pub(crate) struct EffectId;
}
pub(crate) struct Effect<T, F>
where

View File

@@ -1,3 +1,4 @@
#![deny(missing_docs)]
#![cfg_attr(not(feature = "stable"), feature(fn_traits))]
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
@@ -20,7 +21,8 @@
//! a signal [RwSignal](crate::RwSignal) without this read-write segregation.
//! 2. *Derived Signals:* any function that relies on another signal.
//! 3. *Memos:* [create_memo](crate::create_memo), which returns a [Memo](crate::Memo).
//! 4. *Resources:* [create_resource], which converts an `async` [Future] into a synchronous [Resource](crate::Resource) signal.
//! 4. *Resources:* [create_resource], which converts an `async` [std::future::Future] into a
//! synchronous [Resource](crate::Resource) signal.
//!
//! ### Effects
//! 1. Use [create_effect](crate::create_effect) when you need to synchronize the reactive system

View File

@@ -61,6 +61,59 @@ where
cx.runtime.create_memo(f)
}
/// An efficient derived reactive value based on other reactive values.
///
/// Unlike a "derived signal," a memo comes with two guarantees:
/// 1. The memo will only run *once* per change, no matter how many times you
/// access its value.
/// 2. The memo will only notify its dependents if the value of the computation changes.
///
/// This makes a memo the perfect tool for expensive computations.
///
/// Memos have a certain overhead compared to derived signals. In most cases, you should
/// create a derived signal. But if the derivation calculation is expensive, you should
/// create a memo.
///
/// As with [create_effect](crate::create_effect), the argument to the memo function is the previous value,
/// i.e., the current value of the memo, which will be `None` for the initial calculation.
///
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # create_scope(|cx| {
/// let (value, set_value) = create_signal(cx, 0);
///
/// // 🆗 we could create a derived signal with a simple function
/// let double_value = move || value() * 2;
/// set_value(2);
/// assert_eq!(double_value(), 4);
///
/// // but imagine the computation is really expensive
/// let expensive = move || really_expensive_computation(value()); // lazy: doesn't run until called
/// create_effect(cx, move |_| {
/// // 🆗 run #1: calls `really_expensive_computation` the first time
/// log::debug!("expensive = {}", expensive());
/// });
/// create_effect(cx, move |_| {
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
/// let value = expensive();
/// // do something else...
/// });
///
/// // instead, we create a memo
/// // 🆗 run #1: the calculation runs once immediately
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value()));
/// create_effect(cx, move |_| {
/// // 🆗 reads the current value of the memo
/// log::debug!("memoized = {}", memoized());
/// });
/// create_effect(cx, move |_| {
/// // ✅ reads the current value **without re-running the calculation**
/// let value = memoized();
/// // do something else...
/// });
/// # }).dispose();
/// ```
#[derive(Debug, PartialEq, Eq)]
pub struct Memo<T>(pub(crate) ReadSignal<Option<T>>)
where
@@ -81,6 +134,22 @@ impl<T> Memo<T>
where
T: 'static,
{
/// Clones and returns the current value of the memo, and subscribes
/// the running effect to the memo.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// let (count, set_count) = create_signal(cx, 0);
/// let double_count = create_memo(cx, move |_| count() * 2);
///
/// assert_eq!(double_count.get(), 0);
/// set_count(1);
///
/// // double_count() is shorthand for double_count.get()
/// assert_eq!(double_count(), 2);
/// # }).dispose();
/// #
/// ```
pub fn get(&self) -> T
where
T: Clone,
@@ -88,6 +157,26 @@ where
self.with(T::clone)
}
/// Applies a function to the current value of the memo, and subscribes
/// the running effect to this memo.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper = create_memo(cx, move |_| name().to_uppercase());
///
/// // ❌ unnecessarily clones the string
/// let first_char = move || name_upper().chars().next().unwrap();
/// assert_eq!(first_char(), 'A');
///
/// // ✅ gets the first char without cloning the `String`
/// let first_char = move || name_upper.with(|n| n.chars().next().unwrap());
/// assert_eq!(first_char(), 'A');
/// set_name("Bob".to_string());
/// assert_eq!(first_char(), 'B');
/// # }).dispose();
/// #
/// ```
pub fn with<U>(&self, f: impl Fn(&T) -> U) -> U {
// okay to unwrap here, because the value will *always* have initially
// been set by the effect, synchronously

View File

@@ -347,19 +347,21 @@ where
.resource(self.id, |resource: &ResourceState<S, T>| resource.with(f))
}
pub fn loading(&self) -> bool {
/// Returns a signal that indicates whether the resource is currently loading.
pub fn loading(&self) -> ReadSignal<bool> {
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| {
resource.loading.try_with(|n| *n).unwrap_or(false)
})
.resource(self.id, |resource: &ResourceState<S, T>| resource.loading)
}
/// Re-runs the async function with the current source data.
pub fn refetch(&self) {
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| resource.refetch())
}
#[cfg(feature = "ssr")]
/// Returns a [std::future::Future] that will resolve when the resource has loaded,
/// yield its [ResourceId] and a JSON string.
#[cfg(any(feature = "ssr", doc))]
pub async fn to_serialization_resolver(&self) -> (ResourceId, String)
where
T: Serializable,
@@ -372,6 +374,49 @@ where
}
}
/// A signal that reflects the
/// current state of an asynchronous task, allowing you to integrate `async`
/// [Future]s into the synchronous reactive system.
///
/// Takes a `fetcher` function that generates a [Future] when called and a
/// `source` signal that provides the argument for the `fetcher`. Whenever the
/// value of the `source` changes, a new [Future] will be created and run.
///
/// When server-side rendering is used, the server will handle running the
/// [Future] and will stream the result to the client. This process requires the
/// output type of the Future to be [Serializable]. If your output cannot be
/// serialized, or you just want to make sure the [Future] runs locally, use
/// [create_local_resource()].
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// // any old async function; maybe this is calling a REST API or something
/// async fn fetch_cat_picture_urls(how_many: i32) -> Vec<String> {
/// // pretend we're fetching cat pics
/// vec![how_many.to_string()]
/// }
///
/// // a signal that controls how many cat pics we want
/// let (how_many_cats, set_how_many_cats) = create_signal(cx, 1);
///
/// // create a resource that will refetch whenever `how_many_cats` changes
/// # // `csr`, `hydrate`, and `ssr` all have issues here
/// # // because we're not running in a browser or in Tokio. Let's just ignore it.
/// # if false {
/// let cats = create_resource(cx, how_many_cats, fetch_cat_picture_urls);
///
/// // when we read the signal, it contains either
/// // 1) None (if the Future isn't ready yet) or
/// // 2) Some(T) (if the future's already resolved)
/// assert_eq!(cats(), Some(vec!["1".to_string()]));
///
/// // when the signal's value changes, the `Resource` will generate and run a new `Future`
/// set_how_many_cats(2);
/// assert_eq!(cats(), Some(vec!["2".to_string()]));
/// # }
/// # }).dispose();
/// ```
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Resource<S, T>
where
@@ -385,7 +430,10 @@ where
}
// Resources
slotmap::new_key_type! { pub struct ResourceId; }
slotmap::new_key_type! {
/// Unique ID assigned to a [Resource](crate::Resource).
pub struct ResourceId;
}
impl<S, T> Clone for Resource<S, T>
where

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