Compare commits

..

82 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
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
103 changed files with 1943 additions and 1082 deletions

View File

@@ -14,27 +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",
]
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

@@ -5,7 +5,6 @@ pub fn simple_counter(cx: Scope) -> web_sys::Element {
view! { cx,
<div>
<MyComponent><p>"Here's the child"</p></MyComponent>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
@@ -13,14 +12,3 @@ pub fn simple_counter(cx: Scope) -> web_sys::Element {
</div>
}
}
#[component]
fn MyComponent(cx: Scope, children: Option<Box<dyn Fn() -> Vec<Element>>>) -> Element {
view! {
cx,
<my-component>
<p>"Here's the child you passed in: "</p>
<slot></slot>
</my-component>
}
}

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,91 +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 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

@@ -59,9 +59,11 @@ impl Todos {
// if all are complete, mark them all active
if self.remaining() == 0 {
for todo in &self.0 {
if todo.completed.get() {
todo.completed.set(false);
}
todo.completed.update(|completed| {
if *completed {
*completed = false
}
});
}
}
// otherwise, mark them all complete
@@ -209,7 +211,7 @@ pub fn TodoMVC(cx: Scope) -> Element {
>
<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">
@@ -272,18 +274,15 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
set_editing(false);
};
// the `input` variable above is filled by a ref, when the template is created
// so we create the template and store it in a variable here, so we can
// set up an effect using the `input` below
let tpl = view! { cx,
view! { cx,
<li
class="todo"
class:editing={editing}
class:completed={move || todo.completed.get()}
_ref=input
>
<div class="view">
<input
_ref=input
class="toggle"
type="checkbox"
prop:checked={move || (todo.completed)()}
@@ -292,7 +291,15 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
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))/>
@@ -315,19 +322,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
})
}
</li>
};
// toggling to edit mode should focus the input
#[cfg(any(feature = "csr", feature = "hydrate"))]
create_effect(cx, move |_| {
if editing() {
if let Some(input) = input.dyn_ref::<HtmlInputElement>() {
input.focus();
}
}
});
tpl
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

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.15"
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.13" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.12" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.13" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.12" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.15" }
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
@@ -60,6 +62,7 @@
//! - 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
@@ -70,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

@@ -81,33 +81,3 @@ fn test_classes() {
);
});
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn test_dash_prefixes() {
use leptos_dom::*;
use leptos_macro::view;
use leptos_reactive::{create_signal, run_scope};
let colons = run_scope(|cx| {
let (value, set_value) = create_signal(cx, 5);
view! {
cx,
<div class="my big" class:a={move || value() > 10} class:red=true class:car={move || value() > 1} attr:id="id"></div>
}
});
let dashes = run_scope(|cx| {
let (value, set_value) = create_signal(cx, 5);
view! {
cx,
<div class="my big" class-a={move || value() > 10} class-red=true class-car={move || value() > 1} attr-id="id"></div>
}
});
assert_eq!(colons, dashes);
assert_eq!(
dashes,
"<div data-hk=\"0-0\" class=\"my big red car\" id=\"id\"></div>"
);
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_core"
version = "0.0.13"
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.12" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.13" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.12" }
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.12"
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.12" }
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,
@@ -88,22 +117,30 @@ pub fn insert_before(
}
}
/// 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 {
@@ -112,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
@@ -147,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,
@@ -158,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()
@@ -166,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()
@@ -173,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(
@@ -202,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,
@@ -223,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,
@@ -234,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)>;
@@ -248,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,
@@ -273,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())
}
@@ -289,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.13"
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"
syn-rsx = "0.9"
uuid = { version = "1", features = ["v4"] }
leptos_dom = { path = "../leptos_dom", version = "0.0.12" }
leptos_reactive = { path = "../leptos_reactive", version = "0.0.12" }
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 {
@@ -109,7 +111,7 @@ mod server;
/// # });
/// ```
///
/// 5. Event handlers can be added with `on:` attributes. If the event name contains a dash, you should use `on-` as the prefix instead.
/// 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| {
@@ -130,8 +132,6 @@ 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). They can also take an `Option`, in which case `Some` sets the property
/// and `None` deletes the property.
///
/// If your property name contains a dash, you should use `prop-` as the prefix instead.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
@@ -154,13 +154,39 @@ mod server;
/// ```
///
/// 7. Classes can be toggled with `class:` attributes, which take a `bool` (or a signal that returns a `bool`).
/// If your class name contains a dash, you should use `class-` as the prefix instead.
/// ```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 (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
/// # ;
/// # }
/// # });
@@ -249,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::form_urlencoded::parse(data).collect::<Vec<_>>();
Ok(Self {
#(#from_form_data_fields),*
})
#url
}
#[cfg(feature = "ssr")]
@@ -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 })
}
}

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! {}
@@ -156,10 +160,17 @@ enum PrevSibChange {
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,38 +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:") || 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 {
name
};
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<_>>();
@@ -254,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,
@@ -270,7 +276,7 @@ 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! {
@@ -285,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;
@@ -351,7 +364,8 @@ fn element_to_tokens(
expressions,
multi,
mode,
idx == 0
idx == 0,
is_template
);
prev_sib = match curr_id {
@@ -382,26 +396,39 @@ 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 {
@@ -409,16 +436,14 @@ fn attr_to_tokens(
};
let name = if name.starts_with("attr:") {
name.replacen("attr:", "", 1)
} else 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)
}
@@ -428,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" {
@@ -470,21 +495,24 @@ fn attr_to_tokens(
}
}
// Event Handlers
else if name.starts_with("on:") || name.starts_with("on-") {
let handler = node
.value
.as_ref()
.expect("event listener attributes need a value");
else if name.starts_with("on:") {
let handler = node
.value
.as_ref()
.expect("event listener attributes need a value")
.as_ref();
if mode != Mode::Ssr {
let name = if name.starts_with("on:") {
name.replacen("on:", "", 1)
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 {
name.replacen("on-", "", 1)
};
expressions.push(quote_spanned! {
span => add_event_listener(#el_id.unchecked_ref(), #name, #handler);
});
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.
@@ -494,31 +522,23 @@ fn attr_to_tokens(
}
}
// Properties
else if name.starts_with("prop:") || name.starts_with("prop-") {
let name = if name.starts_with("prop:") {
name.replacen("prop:", "", 1)
} else {
name.replacen("prop-", "", 1)
};
else if name.starts_with("prop:") {
let name = name.replacen("prop:", "", 1);
// can't set properties in SSR
if mode != Mode::Ssr {
let value = node.value.as_ref().expect("prop: blocks need values");
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:") || name.starts_with("class-") {
let name = if name.starts_with("class:") {
name.replacen("class:", "", 1)
} else {
name.replacen("class-", "", 1)
};
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 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))
});
@@ -597,10 +617,11 @@ fn child_to_tokens(
expressions: &mut Vec<TokenStream>,
multi: bool,
mode: Mode,
is_first_child: bool
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,
@@ -630,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>,
@@ -775,7 +830,7 @@ fn component_to_tokens(
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;
@@ -802,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 {
@@ -868,9 +923,9 @@ fn component_to_tokens(
}
}
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() {
@@ -905,25 +960,24 @@ 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("on-")
|| attr_name.starts_with("prop:")
|| attr_name.starts_with("prop-")
|| attr_name.starts_with("class:")
|| attr_name.starts_with("class-")
|| attr_name.starts_with("attr:")
|| attr_name.starts_with("attr-")
{
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)
@@ -931,71 +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("on: event listener attributes need a value");
Some(quote_spanned! {
span => add_event_listener(#component_name.unchecked_ref(), #event_name, #handler)
})
}
else if let Some(event_name) = attr_name.strip_prefix("on-") {
let span = attr.name_span().unwrap();
let handler = attr
.value
.as_ref()
.expect("on- 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))
})
}
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))
})
}
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");
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 if let Some(name) = attr_name.strip_prefix("attr-") {
let value = attr.value.as_ref().expect("attr- attributes need values");
Some(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() {
@@ -1029,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 {
@@ -1070,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.12"
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

View File

@@ -60,10 +60,15 @@ pub struct Scope {
}
impl Scope {
/// The unique identifier for this scope.
pub fn id(&self) -> ScopeId {
self.id
}
/// Creates a child scope and runs the given function within it.
///
/// The child scope has its own lifetime and disposer, but will be disposed when the parent is
/// disposed, if it has not been already.
pub fn child_scope(self, f: impl FnOnce(Scope)) -> ScopeDisposer {
let (_, child_id, disposer) = self.runtime.run_scope_undisposed(f, Some(self));
let mut children = self.runtime.scope_children.borrow_mut();
@@ -75,6 +80,31 @@ impl Scope {
disposer
}
/// Suspends reactive tracking while running the given function.
///
/// This can be used to isolate parts of the reactive graph from one another.
///
/// ```
/// # use leptos_reactive::*;
/// # run_scope(|cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
/// let c = create_memo(cx, move |_| {
/// // this memo will *only* update when `a` changes
/// a() + cx.untrack(move || b())
/// });
///
/// assert_eq!(c(), 0);
/// set_a(1);
/// assert_eq!(c(), 1);
/// set_b(1);
/// // hasn't updated, because we untracked before reading b
/// assert_eq!(c(), 1);
/// set_a(2);
/// assert_eq!(c(), 3);
///
/// # });
/// ```
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
let prev_observer = self.runtime.observer.take();
let untracked_result = f();
@@ -86,7 +116,7 @@ impl Scope {
// Internals
impl Scope {
pub fn dispose(self) {
pub(crate) fn dispose(self) {
// dispose of all child scopes
let children = {
let mut children = self.runtime.scope_children.borrow_mut();
@@ -168,7 +198,10 @@ pub fn on_cleanup(cx: Scope, cleanup_fn: impl FnOnce() + 'static) {
cleanups.push(Box::new(cleanup_fn));
}
slotmap::new_key_type! { pub struct ScopeId; }
slotmap::new_key_type! {
/// Unique ID assigned to a [Scope](crate::Scope).
pub struct ScopeId;
}
#[derive(Debug)]
pub(crate) enum ScopeProperty {
@@ -177,9 +210,22 @@ pub(crate) enum ScopeProperty {
Resource(ResourceId),
}
/// Creating a [Scope](crate::Scope) gives you a disposer, which can be called
/// to dispose of that reactive scope.
///
/// This will
/// 1. dispose of all child `Scope`s
/// 2. run all cleanup functions defined for this scope by [on_cleanup](crate::on_cleanup).
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
pub struct ScopeDisposer(pub(crate) Box<dyn FnOnce()>);
impl ScopeDisposer {
/// Disposes of a reactive [Scope](crate::Scope).
///
/// This will
/// 1. dispose of all child `Scope`s
/// 2. run all cleanup functions defined for this scope by [on_cleanup](crate::on_cleanup).
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
pub fn dispose(self) {
(self.0)()
}
@@ -188,19 +234,24 @@ impl ScopeDisposer {
impl Scope {
// hydration-specific code
cfg_if! {
if #[cfg(feature = "hydrate")] {
if #[cfg(any(feature = "hydrate", doc))] {
/// `hydrate` only: Whether we're currently hydrating the page.
pub fn is_hydrating(&self) -> bool {
self.runtime.shared_context.borrow().is_some()
}
/// `hydrate` only: Begins the hydration process.
pub fn start_hydration(&self, element: &web_sys::Element) {
self.runtime.start_hydration(element);
}
/// `hydrate` only: Ends the hydration process.
pub fn end_hydration(&self) {
self.runtime.end_hydration();
}
/// `hydrate` only: Gets the next element in the hydration queue, either from the
/// server-rendered DOM or from the template.
pub fn get_next_element(&self, template: &web_sys::Element) -> web_sys::Element {
use wasm_bindgen::{JsCast, UnwrapThrowExt};
@@ -241,7 +292,9 @@ impl Scope {
}
}
#[cfg(any(feature = "csr", feature = "hydrate"))]
/// `hydrate` only: Given the current node, gets the span of the next component that has
/// been marked for hydration, returning its starting node and the set of all its nodes.
#[cfg(any(feature = "csr", feature = "hydrate", doc))]
pub fn get_next_marker(&self, start: &web_sys::Node) -> (web_sys::Node, Vec<web_sys::Node>) {
let mut end = Some(start.clone());
let mut count = 0;
@@ -279,6 +332,7 @@ impl Scope {
(start, current)
}
/// On either the server side or the browser side, generates the next key in the hydration process.
pub fn next_hydration_key(&self) -> String {
let mut sc = self.runtime.shared_context.borrow_mut();
if let Some(ref mut sc) = *sc {
@@ -291,6 +345,7 @@ impl Scope {
}
}
/// Runs the given function with the next hydration context.
pub fn with_next_context<T>(&self, f: impl FnOnce() -> T) -> T {
if self
.runtime
@@ -329,6 +384,7 @@ impl Scope {
self.runtime.all_resources()
}
/// The current key for an HTML fragment created by server-rendering a `<Suspense/>` component.
pub fn current_fragment_key(&self) -> String {
self.runtime
.shared_context
@@ -343,6 +399,8 @@ impl Scope {
self.runtime.serialization_resolvers()
}
/// Registers the given [SuspenseContext](crate::SuspenseContext) with the current scope,
/// calling the `resolver` when its resources are all resolved.
pub fn register_suspense(
&self,
context: SuspenseContext,
@@ -372,6 +430,7 @@ impl Scope {
}
}
/// The set of all HTML fragments current pending, by their keys (see [Self::current_fragment_key]).
pub fn pending_fragments(&self) -> HashMap<String, Pin<Box<dyn Future<Output = String>>>> {
if let Some(ref mut shared_context) = *self.runtime.shared_context.borrow_mut() {
std::mem::take(&mut shared_context.pending_fragments)

View File

@@ -2,20 +2,36 @@ use cfg_if::cfg_if;
use std::rc::Rc;
use thiserror::Error;
/// Describes errors that can occur while serializing and deserializing data,
/// typically during the process of streaming [Resource](crate::Resource)s from
/// the server to the client.
#[derive(Debug, Clone, Error)]
pub enum SerializationError {
/// Errors that occur during serialization.
#[error("error serializing Resource: {0}")]
Serialize(Rc<dyn std::error::Error>),
/// Errors that occur during deserialization.
#[error("error deserializing Resource: {0}")]
Deserialize(Rc<dyn std::error::Error>),
}
/// Describes an object that can be serialized to or from JSON.
///
/// This is primarily used for serializing and deserializing [Resource](crate::Resource)s
/// so they can begin on the server and be resolved on the client, but can be used
/// for any data that needs to be serialized/deserialized.
///
/// This trait is intended to abstract over various serialization crates,
/// as selected between by the crate features `serde` (default), `serde-lite`,
/// and `miniserde`.
pub trait Serializable
where
Self: Sized,
{
/// Serializes the object to JSON.
fn to_json(&self) -> Result<String, SerializationError>;
/// Deserializes the object from JSON.
fn from_json(json: &str) -> Result<Self, SerializationError>;
}

View File

@@ -604,7 +604,10 @@ where
}
// Internals
slotmap::new_key_type! { pub struct SignalId; }
slotmap::new_key_type! {
/// Unique ID assigned to a signal.
pub struct SignalId;
}
#[derive(Debug, Error)]
pub(crate) enum SignalError {
@@ -644,7 +647,13 @@ impl SignalId {
}
}
}?;
let value = value.borrow();
let value = value.try_borrow().unwrap_or_else(|e| {
debug_warn!(
"Signal::try_with_no_subscription failed on Signal<{}>. It seems you're trying to read the value of a signal within an effect caused by updating the signal.",
std::any::type_name::<T>()
);
panic!("{e}");
});
let value = value
.downcast_ref::<T>()
.ok_or_else(|| SignalError::Type(std::any::type_name::<T>()))?;

View File

@@ -26,6 +26,9 @@ cfg_if! {
}
}
/// Spawns and runs a thread-local [std::future::Future] in a platform-independent way.
///
/// This can be used to interface with any `async` code.
pub fn spawn_local<F>(fut: F)
where
F: Future<Output = ()> + 'static,

View File

@@ -1,7 +1,10 @@
use crate::{create_signal, spawn::queue_microtask, ReadSignal, Scope, WriteSignal};
/// Tracks [Resource](crate::Resource)s that are read under a suspense context,
/// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component.
#[derive(Copy, Clone, Debug)]
pub struct SuspenseContext {
/// The number of resources that are currently pending.
pub pending_resources: ReadSignal<usize>,
set_pending_resources: WriteSignal<usize>,
}
@@ -21,6 +24,7 @@ impl PartialEq for SuspenseContext {
impl Eq for SuspenseContext {}
impl SuspenseContext {
/// Creates an empty suspense context.
pub fn new(cx: Scope) -> Self {
let (pending_resources, set_pending_resources) = create_signal(cx, 0);
Self {
@@ -29,11 +33,13 @@ impl SuspenseContext {
}
}
/// Notifies the suspense context that a new resource is now pending.
pub fn increment(&self) {
let setter = self.set_pending_resources;
queue_microtask(move || setter.update(|n| *n += 1));
}
/// Notifies the suspense context that a resource has resolved.
pub fn decrement(&self) {
let setter = self.set_pending_resources;
queue_microtask(move || {
@@ -45,6 +51,7 @@ impl SuspenseContext {
});
}
/// Tests whether all of the pending resources have resolved.
pub fn ready(&self) -> bool {
self.pending_resources
.try_with(|n| *n == 0)

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_server"
version = "0.0.15"
version = "0.0.16"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -8,13 +8,15 @@ repository = "https://github.com/gbj/leptos"
description = "RPC for the Leptos web framework."
[dependencies]
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.12" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.12" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.16" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.16" }
form_urlencoded = "1"
gloo-net = "0.2"
lazy_static = "1"
linear-map = "1"
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_urlencoded = "0.7"
thiserror = "1"
[dev-dependencies]

View File

@@ -1,3 +1,5 @@
#![deny(missing_docs)]
//! # Leptos Server Functions
//!
//! This package is based on a simple idea: sometimes its useful to write functions
@@ -59,7 +61,7 @@
pub use form_urlencoded;
use leptos_reactive::*;
use serde::{Deserialize, Serialize};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{future::Future, pin::Pin, rc::Rc};
use thiserror::Error;
@@ -143,19 +145,17 @@ pub fn server_fn_by_path(path: &str) -> Option<Arc<ServerFnTraitObj>> {
/// Technically, the trait is implemented on a type that describes the server function's arguments.
pub trait ServerFn
where
Self: Sized + 'static,
Self: Serialize + DeserializeOwned + Sized + 'static,
{
/// The return type of the function.
type Output: Serializable;
/// URL prefix that should be prepended by the client to the generated URL.
fn prefix() -> &'static str;
/// The path at which the server function can be reached on the server.
fn url() -> &'static str;
/// A set of `(input_name, input_value)` pairs used to serialize the arguments to the server function.
fn as_form_data(&self) -> Vec<(&'static str, String)>;
/// Deserializes the arguments to the server function from form data.
fn from_form_data(data: &[u8]) -> Result<Self, ServerFnError>;
/// Runs the function on the server.
#[cfg(any(feature = "ssr", doc))]
fn call_fn(self) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>> + Send>>;
@@ -171,7 +171,8 @@ where
// takes a String -> returns its async value
let run_server_fn = Arc::new(|data: &[u8]| {
// decode the args
let value = Self::from_form_data(data);
let value = serde_urlencoded::from_bytes::<Self>(data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()));
Box::pin(async move {
let value = match value {
Ok(v) => v,
@@ -210,18 +211,25 @@ where
/// Type for errors that can occur when using server functions.
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub enum ServerFnError {
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
#[error("error while trying to register the server function: {0}")]
Registration(String),
/// Occurs on the client if there is a network error while trying to run function on server.
#[error("error reaching server to call server function: {0}")]
Request(String),
/// Occurs when there is an error while actually running the function on the server.
#[error("error running server function: {0}")]
ServerError(String),
/// Occurs on the client if there is an error deserializing the server's response.
#[error("error deserializing server function results {0}")]
Deserialization(String),
/// Occurs on the client if there is an error serializing the server function arguments.
#[error("error serializing server function results {0}")]
Serialization(String),
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
#[error("error deserializing server function arguments {0}")]
Args(String),
/// Occurs on the server if there's a missing argument.
#[error("missing argument {0}")]
MissingArg(String),
}
@@ -234,20 +242,13 @@ where
{
use leptos_dom::*;
let args_form_data = web_sys::FormData::new().expect_throw("could not create FormData");
for (field_name, value) in args.as_form_data().into_iter() {
args_form_data
.append_with_str(field_name, &value)
.expect_throw("could not append form field");
}
let args_form_data = web_sys::UrlSearchParams::new_with_str_sequence_sequence(&args_form_data)
.expect_throw("could not URL encode FormData");
let args_form_data = args_form_data.to_string().as_string().unwrap_or_default();
let args_form_data = serde_urlencoded::to_string(&args)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
let resp = gloo_net::http::Request::post(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept", "application/json")
.body(args_form_data.to_string())
.body(args_form_data)
.send()
.await
.map_err(|e| ServerFnError::Request(e.to_string()))?;
@@ -351,7 +352,7 @@ where
input: RwSignal<Option<I>>,
value: RwSignal<Option<O>>,
pending: RwSignal<bool>,
url: Option<&'static str>,
url: Option<String>,
#[allow(clippy::complexity)]
action_fn: Rc<dyn Fn(&I) -> Pin<Box<dyn Future<Output = O>>>>,
}
@@ -398,13 +399,18 @@ where
/// The URL associated with the action (typically as part of a server function.)
/// This enables integration with the `ActionForm` component in `leptos_router`.
pub fn url(&self) -> Option<&str> {
self.url
self.url.as_deref()
}
/// Associates the URL of the given server function with this action.
/// This enables integration with the `ActionForm` component in `leptos_router`.
pub fn using_server_fn<T: ServerFn>(mut self) -> Self {
self.url = Some(T::url());
let prefix = T::prefix();
self.url = if prefix.is_empty() {
Some(T::url().to_string())
} else {
Some(prefix.to_string() + "/" + T::url())
};
self
}
}

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