mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 09:02:37 -05:00
Compare commits
49 Commits
1457
...
effect-sch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
642ab9baf9 | ||
|
|
ed58ac2c99 | ||
|
|
4f1fafaea3 | ||
|
|
7a35d33402 | ||
|
|
e6f053d39f | ||
|
|
b3a4c95dad | ||
|
|
de44b1f91f | ||
|
|
689022661d | ||
|
|
905d46a09d | ||
|
|
5585f20940 | ||
|
|
5c3ed3f018 | ||
|
|
03cabf6ea3 | ||
|
|
2798dc455f | ||
|
|
db20be5576 | ||
|
|
495862e9f9 | ||
|
|
2ca1c51fdc | ||
|
|
70e1ad41e2 | ||
|
|
53ec7ed272 | ||
|
|
d98a577740 | ||
|
|
fd834f48c2 | ||
|
|
7be65a37c6 | ||
|
|
3b5e2d86fb | ||
|
|
716b9fb50b | ||
|
|
006ca13797 | ||
|
|
6e008343c8 | ||
|
|
2ca24883ac | ||
|
|
4a43983f4e | ||
|
|
d9e83121c1 | ||
|
|
f5b4b97c9b | ||
|
|
bcfa430a40 | ||
|
|
7c51815cf5 | ||
|
|
fee2fb953b | ||
|
|
8ecb7f59c4 | ||
|
|
b85cb9fb3b | ||
|
|
a631c5ca1c | ||
|
|
bee9bd8f67 | ||
|
|
8d3874f8a9 | ||
|
|
bade16d227 | ||
|
|
e0a132bde3 | ||
|
|
4d7e1f4d26 | ||
|
|
700eee6604 | ||
|
|
694ed61e4c | ||
|
|
d7330097ba | ||
|
|
c65a3a6ca3 | ||
|
|
793c191619 | ||
|
|
6c3e2fe53e | ||
|
|
08c419e3ee | ||
|
|
736f4185b5 | ||
|
|
9cc0fc8c49 |
21
.github/workflows/run-cargo-make-task.yml
vendored
21
.github/workflows/run-cargo-make-task.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Setup environment
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -95,14 +95,17 @@ jobs:
|
||||
|
||||
- name: Maybe install playwright browser dependencies
|
||||
run: |
|
||||
playwright_count=$(find ${{inputs.directory}} -name playwright.config.ts | wc -l)
|
||||
if [ $playwright_count -eq 1 ]; then
|
||||
echo playwright required
|
||||
sudo apt-get update
|
||||
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
|
||||
else
|
||||
echo playwright is not required
|
||||
fi
|
||||
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
if [ ! -v $pw_dir ]; then
|
||||
echo "Playwright required in $pw_dir"
|
||||
cd $pw_dir
|
||||
pnpm dlx playwright install --with-deps
|
||||
else
|
||||
echo Playwright is not required
|
||||
fi
|
||||
done
|
||||
|
||||
# Run Cargo Make Task
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ Cargo.lock
|
||||
.idea
|
||||
.direnv
|
||||
.envrc
|
||||
|
||||
.vscode
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.0"
|
||||
version = "0.5.0-beta2"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.5.0" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.0" }
|
||||
leptos_router = { path = "./router", version = "0.5.0" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0" }
|
||||
leptos = { path = "./leptos", version = "0.5.0-beta2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.0-beta2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0-beta2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.0-beta2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0-beta2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.0-beta2" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.0-beta2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0-beta2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0-beta2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.0-beta2" }
|
||||
leptos_router = { path = "./router", version = "0.5.0-beta2" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.0-beta2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0-beta2" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a suspected security issue, please contact security@leptos.dev rather than opening
|
||||
a public issue.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
The most-recently-released version of the library is supported with security updates.
|
||||
For example, if a security issue is discovered that affects 0.3.2 and all later releases,
|
||||
a 0.4.x patch will be released but a new 0.3.x patch release will not be made. You should
|
||||
plan to update to the latest version to receive any new features or bugfixes of any kind.
|
||||
@@ -65,7 +65,7 @@ And add a simple “Hello, world!” to your `main.rs`
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|| view! { <p>"Hello, world!"</p> })
|
||||
mount_to_body(|| view! { <p>"Hello, world!"</p> })
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -367,7 +367,6 @@ fn GlobalStateInput() -> impl IntoView {
|
||||
// that we created in the other component
|
||||
// neither of them will cause the other to rerun
|
||||
let (name, set_name) = create_slice(
|
||||
|
||||
// we take a slice *from* `state`
|
||||
state,
|
||||
// our getter returns a "slice" of the data
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
- [Error Handling](./view/07_errors.md)
|
||||
- [Parent-Child Communication](./view/08_parent_child.md)
|
||||
- [Passing Children to Components](./view/09_component_children.md)
|
||||
- [No Macros: The View Builder Syntax](./view/builder.md)
|
||||
- [Reactivity](./reactivity/README.md)
|
||||
- [Working with Signals](./reactivity/working_with_signals.md)
|
||||
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)
|
||||
|
||||
@@ -235,9 +235,9 @@ At the very most, you might consider memoizing the final node before running som
|
||||
|
||||
```rust
|
||||
let text = create_memo(move |_| {
|
||||
d()
|
||||
d()
|
||||
});
|
||||
create_effect(move |_| {
|
||||
engrave_text_into_bar_of_gold(&text());
|
||||
engrave_text_into_bar_of_gold(&text());
|
||||
});
|
||||
```
|
||||
|
||||
@@ -18,7 +18,7 @@ let async_data = create_resource(
|
||||
count,
|
||||
// every time `count` changes, this will run
|
||||
|value| async move {
|
||||
log!("loading data from API");
|
||||
logging::log!("loading data from API");
|
||||
load_data(value).await
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ In the previous chapter, we showed how you can create a simple loading screen to
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(0);
|
||||
let a = create_resource(count, |count| async move { load_a(count).await });
|
||||
let once = create_resource(count, |count| async move { load_a(count).await });
|
||||
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
|
||||
@@ -54,7 +54,7 @@ RUN cargo leptos build --release -vv
|
||||
|
||||
FROM rustlang/rust:nightly-bullseye as runner
|
||||
# Copy the server binary to the /app directory
|
||||
COPY --from=builder /app/target/server/release/leptos_website /app/
|
||||
COPY --from=builder /app/target/server/release/leptos_start /app/
|
||||
# /target/site contains our JS/WASM/CSS, etc.
|
||||
COPY --from=builder /app/target/site /app/site
|
||||
# Copy Cargo.toml if it’s needed at runtime
|
||||
@@ -68,7 +68,7 @@ ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
|
||||
ENV LEPTOS_SITE_ROOT="site"
|
||||
EXPOSE 8080
|
||||
# Run the server
|
||||
CMD ["/app/leptos_website"]
|
||||
CMD ["/app/leptos_start"]
|
||||
```
|
||||
|
||||
> Read more: [`gnu` and `musl` build files for Leptos apps](https://github.com/leptos-rs/leptos/issues/1152#issuecomment-1634916088).
|
||||
|
||||
@@ -50,7 +50,6 @@ If you want to really understand the issue here, it may help to look at the expa
|
||||
|
||||
```rust
|
||||
Suspense(
|
||||
|
||||
::leptos::component_props_builder(&Suspense)
|
||||
.fallback(|| ())
|
||||
.children({
|
||||
@@ -61,7 +60,6 @@ Suspense(
|
||||
leptos::Fragment::lazy(|| {
|
||||
vec![
|
||||
(Show(
|
||||
|
||||
::leptos::component_props_builder(&Show)
|
||||
.when(|| true)
|
||||
// but fallback is moved into Show here
|
||||
|
||||
@@ -50,7 +50,6 @@ let (use_last, set_use_last) = create_signal(true);
|
||||
// any time one of the source signals changes
|
||||
create_effect(move |_| {
|
||||
log(
|
||||
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
@@ -122,7 +121,6 @@ Like `create_resource`, `watch` takes a first argument, which is reactively trac
|
||||
let (num, set_num) = create_signal(0);
|
||||
|
||||
let stop = watch(
|
||||
|
||||
move || num.get(),
|
||||
move |num, prev_num, _| {
|
||||
log::debug!("Number: {}; Prev: {:?}", num, prev_num);
|
||||
|
||||
@@ -20,7 +20,7 @@ let text = move || if count_is_odd() {
|
||||
// an effect automatically tracks the signals it depends on
|
||||
// and reruns when they change
|
||||
create_effect(move |_| {
|
||||
log!("text = {}", text());
|
||||
logging::log!("text = {}", text());
|
||||
});
|
||||
|
||||
view! {
|
||||
|
||||
@@ -16,7 +16,7 @@ Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `Wr
|
||||
```rust
|
||||
let (count, set_count) = create_signal(0);
|
||||
set_count(1);
|
||||
log!(count());
|
||||
logging::log!(count());
|
||||
```
|
||||
|
||||
is the same as
|
||||
@@ -24,7 +24,7 @@ is the same as
|
||||
```rust
|
||||
let (count, set_count) = create_signal(0);
|
||||
set_count.set(1);
|
||||
log!(count.get());
|
||||
logging::log!(count.get());
|
||||
```
|
||||
|
||||
You might notice that `.get()` and `.set()` can be implemented in terms of `.with()` and `.update()`. In other words, `count.get()` is identical with `count.with(|n| n.clone())`, and `count.set(1)` is implemented by doing `count.update(|n| *n = 1)`.
|
||||
@@ -63,7 +63,7 @@ if names.with(Vec::is_empty) {
|
||||
}
|
||||
```
|
||||
|
||||
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unncessary closure.
|
||||
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unnecessary closure.
|
||||
|
||||
## Making signals depend on each other
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
#[component]
|
||||
pub fn BusyButton() -> impl IntoView {
|
||||
view! {
|
||||
|
||||
<button on:click=move |_| {
|
||||
spawn_local(async {
|
||||
add_todo("So much to do!".to_string()).await;
|
||||
@@ -70,6 +69,18 @@ There are a few things to note about the way you define a server function, too.
|
||||
- We provide the macro a path. This is a prefix for the path at which we’ll mount a server function handler on our server. (See examples for [Actix](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/main.rs#L44) and [Axum](https://github.com/leptos-rs/leptos/blob/598523cd9d0d775b017cb721e41ebae9349f01e2/examples/todo_app_sqlite_axum/src/main.rs#L51).)
|
||||
- You’ll need to have `serde` as a dependency with the `derive` featured enabled for the macro to work properly. You can easily add it to `Cargo.toml` with `cargo add serde --features=derive`.
|
||||
|
||||
## Server Function URL Prefixes
|
||||
|
||||
You can optionally define a specific URL prefix to be used in the definition of the server function.
|
||||
This is done by providing an optional 2nd argument to the `#[server]` macro.
|
||||
By default the URL prefix will be `/api`, if not specified.
|
||||
Here are some examples:
|
||||
|
||||
```rust
|
||||
#[server(AddTodo)] // will use the default URL prefix of `/api`
|
||||
#[server(AddTodo, "/foo")] // will use the URL prefix of `/foo`
|
||||
```
|
||||
|
||||
## Server Function Encodings
|
||||
|
||||
By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body of the request. (This means that server functions can be called from HTML forms, which we’ll see in a future chapter.) But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` macro to specify an alternate encoding:
|
||||
@@ -105,6 +116,21 @@ In other words, you have two choices:
|
||||
>
|
||||
> The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of your app is not available.
|
||||
|
||||
|
||||
## Server Functions Endpoint Paths
|
||||
|
||||
By default, a unique path will be generated. You can optionally define a specific endpoint path to be used in the URL. This is done by providing an optional 4th argument to the `#[server]` macro. Leptos will generate the complete path by concatenating the URL prefix (2nd argument) and the endpoint path (4th argument).
|
||||
For example,
|
||||
|
||||
```rust
|
||||
#[server(MyServerFnType, "/api", "Url", "hello")]
|
||||
```
|
||||
will generate a server function endpoint at `/api/hello` that accepts a POST request.
|
||||
|
||||
> **Can I use the same server function endpoint path with multiple encodings?**
|
||||
>
|
||||
> No. Different server functions must have unique paths. The `#[server]` macro automatically generates unique paths, but you need to be careful if you choose to specify the complete path manually, as the server looks up server functions by their path.
|
||||
|
||||
## An Important Note on Security
|
||||
|
||||
Server functions are a cool technology, but it’s very important to remember. **Server functions are not magic; they’re syntax sugar for defining a public API.** The _body_ of a server function is never made public; it’s just part of your server binary. But the server function is a publicly accessible API endpoint, and it’s return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function.
|
||||
|
||||
@@ -35,7 +35,6 @@ Here’s a simplified example from our [`session_auth_axum` example](https://git
|
||||
```rust
|
||||
#[server(Login, "/api")]
|
||||
pub async fn login(
|
||||
|
||||
username: String,
|
||||
password: String,
|
||||
remember: Option<String>,
|
||||
|
||||
@@ -9,7 +9,7 @@ Put a log somewhere in your root component. (I usually call mine `<App/>`, but a
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
leptos::log!("where do I run?");
|
||||
logging::log!("where do I run?");
|
||||
// ... whatever
|
||||
}
|
||||
```
|
||||
@@ -87,43 +87,6 @@ The WASM version of your app, running in the browser, expects to find three item
|
||||
|
||||
It’s pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If you’re seeing warnings like this and you don’t think it’s your fault, it’s much more likely that it’s a bug with `<Suspense/>` or something. Feel free to go ahead and open an [issue](https://github.com/leptos-rs/leptos/issues) or [discussion](https://github.com/leptos-rs/leptos/discussions) on GitHub for help.
|
||||
|
||||
### Mutating the DOM during rendering
|
||||
|
||||
This is a slightly more common way to create a client/server mismatch: updating a signal _during rendering_ in a way that mutates the view.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let (loaded, set_loaded) = create_signal(false);
|
||||
|
||||
// create_effect only runs on the client
|
||||
create_effect(move |_| {
|
||||
// do something like reading from localStorage
|
||||
set_loaded(true);
|
||||
});
|
||||
|
||||
move || {
|
||||
if loaded() {
|
||||
view! { <p>"Hello, world!"</p> }.into_any()
|
||||
} else {
|
||||
view! { <div class="loading">"Loading..."</div> }.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This one gives us the scary panic
|
||||
|
||||
```
|
||||
panicked at 'assertion failed: `(left == right)`
|
||||
left: `"DIV"`,
|
||||
right: `"P"`: SSR and CSR elements have the same hydration key but different node kinds.
|
||||
```
|
||||
|
||||
And a handy link to this page!
|
||||
|
||||
The problem here is that `create_effect` runs **immediately** and **synchronously**, but only in the browser. As a result, on the server, `loaded` is false, and a `<div>` is rendered. But on the browser, by the time the view is being rendered, `loaded` has already been set to `true`, and the browser is expecting to find a `<p>`.
|
||||
|
||||
#### Solution
|
||||
|
||||
You can simply tell the effect to wait a tick before updating the signal, by using something like `request_animation_frame`, which will set a short timeout and then update the signal before the next frame.
|
||||
@@ -166,7 +129,7 @@ For example, say that I want to store something in the browser’s `localStorage
|
||||
pub fn App() -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
logging::log!("{storage:?}");
|
||||
}
|
||||
```
|
||||
|
||||
@@ -180,7 +143,7 @@ pub fn App() -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
create_effect(move |_| {
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
logging::log!("{storage:?}");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -37,7 +37,7 @@ impl Todos {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_remaining {
|
||||
fn test_remaining() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ Derived signals let you create reactive computed values that can be used in mult
|
||||
places in your application with minimal overhead.
|
||||
|
||||
Note: Using a derived signal like this means that the calculation runs once per
|
||||
signal change per place we access `double_count`; in other words, twice. This is a
|
||||
signal change and once per place we access `double_count`; in other words, twice. This is a
|
||||
very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||
are designed to solve this problem for expensive calculations.
|
||||
|
||||
|
||||
@@ -35,9 +35,7 @@ Instead, let’s create a `<ProgressBar/>` component.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
) -> impl IntoView {
|
||||
fn ProgressBar() -> impl IntoView {
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
@@ -64,7 +62,6 @@ In Leptos, you define props by giving additional arguments to the component func
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
@@ -118,7 +115,6 @@ argument to the component function with `#[prop(optional)]`.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
// mark this prop optional
|
||||
// you can specify it or not when you use <ProgressBar/>
|
||||
#[prop(optional)]
|
||||
@@ -149,7 +145,6 @@ with `#[prop(default = ...)`.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: ReadSignal<i32>
|
||||
@@ -199,7 +194,6 @@ implement the trait `Fn() -> i32`. So you could use a generic component:
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar<F>(
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: F
|
||||
@@ -254,7 +248,6 @@ reactive value.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
#[prop(into)]
|
||||
@@ -373,7 +366,6 @@ component function, and each one of the props:
|
||||
/// Shows progress toward a goal.
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
/// The maximum value of the progress bar.
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
|
||||
@@ -148,10 +148,10 @@ This _works_, for sure. But if you added a log, you might be surprised
|
||||
|
||||
```rust
|
||||
let message = move || if value() > 5 {
|
||||
log!("{}: rendering Big", value());
|
||||
logging::log!("{}: rendering Big", value());
|
||||
"Big"
|
||||
} else {
|
||||
log!("{}: rendering Small", value());
|
||||
logging::log!("{}: rendering Small", value());
|
||||
"Small"
|
||||
};
|
||||
```
|
||||
|
||||
@@ -72,10 +72,7 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn ButtonB<F>(
|
||||
|
||||
on_click: F,
|
||||
) -> impl IntoView
|
||||
pub fn ButtonB<F>(on_click: F) -> impl IntoView
|
||||
where
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
{
|
||||
|
||||
@@ -47,7 +47,6 @@ Let’s define a component that takes some children and a render prop.
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TakesChildren<F, IV>(
|
||||
|
||||
/// Takes a function (type F) that returns anything that can be
|
||||
/// converted into a View (type IV)
|
||||
render_prop: F,
|
||||
|
||||
98
docs/book/src/view/builder.md
Normal file
98
docs/book/src/view/builder.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# No Macros: The View Builder Syntax
|
||||
|
||||
> If you’re perfectly happy with the `view!` macro syntax described so far, you’re welcome to skip this chapter. The builder syntax described in this section is always available, but never required.
|
||||
|
||||
For one reason or another, many developers would prefer to avoid macros. Perhaps you don’t like the limited `rustfmt` support. (Although, you should check out [`leptosfmt`](https://github.com/bram209/leptosfmt), which is an excellent tool!) Perhaps you worry about the effect of macros on compile time. Perhaps you prefer the aesthetics of pure Rust syntax, or you have trouble context-switching between an HTML-like syntax and your Rust code. Or perhaps you want more flexibility in how you create and manipulate HTML elements than the `view` macro provides.
|
||||
|
||||
If you fall into any of those camps, the builder syntax may be for you.
|
||||
|
||||
The `view` macro expands an HTML-like syntax to a series of Rust functions and method calls. If you’d rather not use the `view` macro, you can simply use that expanded syntax yourself. And it’s actually pretty nice!
|
||||
|
||||
First off, if you want you can even drop the `#[component]` macro: a component is just a setup function that creates your view, so you can define a component as a simple function call:
|
||||
|
||||
```rust
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView { }
|
||||
```
|
||||
|
||||
Elements are created by calling a function with the same name as the HTML element:
|
||||
|
||||
```rust
|
||||
p()
|
||||
```
|
||||
|
||||
You can add children to the element with [`.child()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child), which takes a single child or a tuple or array of types that implement [`IntoView`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html).
|
||||
|
||||
```rust
|
||||
p().child((em().child("Big, "), strong().child("bold "), "text"))
|
||||
```
|
||||
|
||||
Attributes are added with [`.attr()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.attr). This can take any of the same types that you could pass as an attribute into the view macro (types that implement [`IntoAttribute`](https://docs.rs/leptos/latest/leptos/trait.IntoAttribute.html)).
|
||||
|
||||
```rust
|
||||
p().attr("id", "foo").attr("data-count", move || count().to_string())
|
||||
```
|
||||
|
||||
Similarly, the `class:`, `prop:`, and `style:` syntaxes map directly onto [`.class()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.class), [`.prop()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.prop), and [`.style()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.style) methods.
|
||||
|
||||
Event listeners can be added with [`.on()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.on). Typed events found in [`leptos::ev`](https://docs.rs/leptos/latest/leptos/ev/index.html) prevent typos in event names and allow for correct type inference in the callback function.
|
||||
|
||||
```rust
|
||||
button()
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.child("Clear")
|
||||
```
|
||||
|
||||
> Many additional methods can be found in the [`HtmlElement`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child) docs, including some methods that are not directly available in the `view` macro.
|
||||
|
||||
All of this adds up to a very Rusty syntax to build full-featured views, if you prefer this style.
|
||||
|
||||
```rust
|
||||
/// A simple counter view.
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
div()
|
||||
.child((
|
||||
button()
|
||||
// typed events found in leptos::ev
|
||||
// 1) prevent typos in event names
|
||||
// 2) allow for correct type inference in callbacks
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.child("Clear"),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.child("-1"),
|
||||
span().child(("Value: ", move || count.get().value(), "!")),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
.child("+1"),
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
This also has the benefit of being more flexible: because these are all plain Rust functions and methods, it’s easier to use them in things like iterator adapters without any additional “magic”:
|
||||
|
||||
```rust
|
||||
// take some set of attribute names and values
|
||||
let attrs: Vec<(&str, AttributeValue)> = todo!();
|
||||
// you can use the builder syntax to “spread” these onto the
|
||||
// element in a way that’s not possible with the view macro
|
||||
let p = attrs
|
||||
.into_iter()
|
||||
.fold(p(), |el, (name, value)| el.attr(name, value));
|
||||
|
||||
```
|
||||
|
||||
> ## Performance Note
|
||||
>
|
||||
> One caveat: the `view` macro applies significant optimizations in server-side-rendering (SSR) mode to improve HTML rendering performance significantly (think 2-4x faster, depending on the characteristics of any given app). It does this by analyzing your `view` at compile time and converting the static parts into simple HTML strings, rather than expanding them into the builder syntax.
|
||||
>
|
||||
> This means two things:
|
||||
>
|
||||
> 1. The builder syntax and `view` macro should not be mixed, or should only be mixed very carefully: at least in SSR mode, the output of the `view` should be treated as a “black box” that can’t have additional builder methods applied to it without causing inconsistencies.
|
||||
> 2. Using the builder syntax will result in less-than-optimal SSR performance. It won’t be slow, by any means (and it’s worth running your own benchmarks in any case), just slower than the `view`-optimized version.
|
||||
@@ -49,10 +49,12 @@ jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
|
||||
'''
|
||||
|
||||
[tasks.test-runner-report]
|
||||
[tasks.test-report]
|
||||
workspace = false
|
||||
description = "report ci test runners for each example - OPTION: [all]"
|
||||
description = "report web testing technology used by examples - OPTION: [all]"
|
||||
script = '''
|
||||
set -emu
|
||||
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
ITALIC="\e[3m"
|
||||
@@ -60,11 +62,10 @@ YELLOW="\e[0;33m"
|
||||
RESET="\e[0m"
|
||||
|
||||
echo
|
||||
echo "${YELLOW}Test Runner Report${RESET}"
|
||||
echo "${ITALIC}Pass the option \"all\" to show all the examples${RESET}"
|
||||
echo "${YELLOW}Web Test Technology${RESET}"
|
||||
echo
|
||||
|
||||
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' |
|
||||
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' -not -path '*/node_modules/*' |
|
||||
sed 's%./%%' |
|
||||
sed 's%/Makefile.toml%%' |
|
||||
grep -v Makefile.toml |
|
||||
@@ -75,38 +76,78 @@ start_path=$(pwd)
|
||||
for path in $makefile_paths; do
|
||||
cd $path
|
||||
|
||||
test_runner=
|
||||
crate_symbols=
|
||||
|
||||
test_count=$(grep -rl -E "#\[(test|rstest)\]" | wc -l)
|
||||
if [ $test_count -gt 0 ]; then
|
||||
test_runner="-C"
|
||||
fi
|
||||
pw_count=$(find . -name playwright.config.ts | wc -l)
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"cucumber"*)
|
||||
crate_symbols=$crate_symbols"C"
|
||||
;;
|
||||
*"fantoccini"*)
|
||||
crate_symbols=$crate_symbols"D"
|
||||
;;
|
||||
esac
|
||||
done <"./Cargo.toml"
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"wasm-test.toml"*)
|
||||
test_runner=$test_runner"-W"
|
||||
*"cargo-make/wasm-test.toml"*)
|
||||
crate_symbols=$crate_symbols"W"
|
||||
;;
|
||||
*"playwright-test.toml"*)
|
||||
test_runner=$test_runner"-P"
|
||||
*"cargo-make/playwright-test.toml"*)
|
||||
crate_symbols=$crate_symbols"P"
|
||||
crate_symbols=$crate_symbols"N"
|
||||
;;
|
||||
*"cargo-leptos-test.toml"*)
|
||||
test_runner=$test_runner"-L"
|
||||
*"cargo-make/playwright-trunk-test.toml"*)
|
||||
crate_symbols=$crate_symbols"P"
|
||||
crate_symbols=$crate_symbols"T"
|
||||
;;
|
||||
*"cargo-make/trunk_server.toml"*)
|
||||
crate_symbols=$crate_symbols"T"
|
||||
;;
|
||||
*"cargo-make/cargo-leptos-webdriver-test.toml"*)
|
||||
crate_symbols=$crate_symbols"L"
|
||||
;;
|
||||
*"cargo-make/cargo-leptos-test.toml"*)
|
||||
crate_symbols=$crate_symbols"L"
|
||||
if [ $pw_count -gt 0 ]; then
|
||||
crate_symbols=$crate_symbols"P"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done <"./Makefile.toml"
|
||||
|
||||
if [ ! -z "$1" ]; then
|
||||
# Sort list of tools
|
||||
sorted_crate_symbols=$(echo ${crate_symbols} | grep -o . | sort | tr -d "\n")
|
||||
|
||||
formatted_crate_symbols=" ➤ ${BOLD}${YELLOW}${sorted_crate_symbols}${RESET}"
|
||||
crate_line=$path
|
||||
if [ ! -z ${1+x} ]; then
|
||||
# Show all examples
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
elif [ ! -z $test_runner ]; then
|
||||
if [ ! -z $crate_symbols ]; then
|
||||
crate_line=$crate_line$formatted_crate_symbols
|
||||
fi
|
||||
echo $crate_line
|
||||
elif [ ! -z $crate_symbols ]; then
|
||||
# Filter out examples that do not run tests in `ci`
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
crate_line=$crate_line$formatted_crate_symbols
|
||||
echo $crate_line
|
||||
fi
|
||||
|
||||
cd ${start_path}
|
||||
done
|
||||
|
||||
c="${BOLD}${YELLOW}C${RESET} = Cucumber"
|
||||
d="${BOLD}${YELLOW}D${RESET} = WebDriver"
|
||||
l="${BOLD}${YELLOW}L${RESET} = Cargo Leptos"
|
||||
n="${BOLD}${YELLOW}N${RESET} = Node"
|
||||
p="${BOLD}${YELLOW}P${RESET} = Playwright"
|
||||
t="${BOLD}${YELLOW}T${RESET} = Trunk"
|
||||
w="${BOLD}${YELLOW}W${RESET} = WASM"
|
||||
|
||||
echo
|
||||
echo "${ITALIC}Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, W = WASM Test${RESET}"
|
||||
echo "${ITALIC}Keys:${RESET} $c, $d, $l, $n, $p, $t, $w"
|
||||
echo
|
||||
'''
|
||||
|
||||
@@ -4,4 +4,4 @@ The examples in this directory are all built and tested against the current `mai
|
||||
|
||||
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch and not the current release.
|
||||
|
||||
To see the examples as they were at the time of the `0.3.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.3.0/examples).
|
||||
To see the examples as they were at the time of the `0.3.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.3.0/examples).
|
||||
|
||||
@@ -2,7 +2,3 @@ extend = { path = "./cargo-leptos.toml" }
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = ["install-cargo-leptos", "cargo-leptos-e2e"]
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
|
||||
7
examples/cargo-make/cargo-leptos-webdriver-test.toml
Normal file
7
examples/cargo-make/cargo-leptos-webdriver-test.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
extend = [
|
||||
{ path = "./cargo-leptos.toml" },
|
||||
{ path = "../cargo-make/webdriver.toml" },
|
||||
]
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = ["install-cargo-leptos", "start-webdriver", "cargo-leptos-e2e"]
|
||||
@@ -1,6 +1,12 @@
|
||||
extend = { path = "../cargo-make/client-process.toml" }
|
||||
|
||||
[tasks.install-cargo-leptos]
|
||||
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
|
||||
[tasks.build]
|
||||
clear = true
|
||||
command = "cargo"
|
||||
@@ -25,31 +31,3 @@ install_crate = "cargo-all-features"
|
||||
[tasks.start-client]
|
||||
command = "cargo"
|
||||
args = ["leptos", "watch"]
|
||||
|
||||
[tasks.stop-client]
|
||||
condition = { env_set = ["APP_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
|
||||
pkill -f todo_app_sqlite
|
||||
fi
|
||||
|
||||
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
|
||||
pkill -f cargo-leptos
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.client-status]
|
||||
condition = { env_set = ["APP_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${APP_PROCESS_NAME}) ]; then
|
||||
echo " ${APP_PROCESS_NAME} is not running"
|
||||
else
|
||||
echo " ${APP_PROCESS_NAME} is up"
|
||||
fi
|
||||
|
||||
if [ -z $(pidof cargo-leptos) ]; then
|
||||
echo " cargo-leptos is not running"
|
||||
else
|
||||
echo " cargo-leptos is up"
|
||||
fi
|
||||
'''
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
[tasks.clean]
|
||||
dependencies = [
|
||||
"clean-cargo",
|
||||
"clean-trunk",
|
||||
"clean-node_modules",
|
||||
"clean-playwright",
|
||||
"clean-cargo",
|
||||
"clean-trunk",
|
||||
"clean-node_modules",
|
||||
"clean-playwright",
|
||||
]
|
||||
|
||||
[tasks.clean-cargo]
|
||||
command = "cargo"
|
||||
args = ["clean"]
|
||||
command = "rm"
|
||||
args = ["-rf", "target"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
script = '''
|
||||
find . -type d -name dist | xargs rm -rf
|
||||
'''
|
||||
|
||||
[tasks.clean-node_modules]
|
||||
script = '''
|
||||
|
||||
35
examples/cargo-make/client-process.toml
Normal file
35
examples/cargo-make/client-process.toml
Normal file
@@ -0,0 +1,35 @@
|
||||
[tasks.start-client]
|
||||
|
||||
[tasks.stop-client]
|
||||
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ ! -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
|
||||
pkill -ef ${CLIENT_PROCESS_NAME}
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.client-status]
|
||||
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
|
||||
echo " ${CLIENT_PROCESS_NAME} is not running"
|
||||
else
|
||||
echo " ${CLIENT_PROCESS_NAME} is up"
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.maybe-start-client]
|
||||
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
|
||||
echo " Starting ${CLIENT_PROCESS_NAME}"
|
||||
cargo make start-client ${@} &
|
||||
else
|
||||
echo " ${CLIENT_PROCESS_NAME} is already started"
|
||||
fi
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.dev]
|
||||
alias = "maybe-start-client"
|
||||
17
examples/cargo-make/playwright-trunk-test.toml
Normal file
17
examples/cargo-make/playwright-trunk-test.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/playwright.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = [
|
||||
"maybe-start-client",
|
||||
"wait-one",
|
||||
"test-playwright",
|
||||
"stop-client",
|
||||
]
|
||||
|
||||
[tasks.wait-one]
|
||||
script = '''
|
||||
sleep 1
|
||||
'''
|
||||
@@ -1,18 +1,12 @@
|
||||
extend = { path = "../cargo-make/client-process.toml" }
|
||||
|
||||
[env]
|
||||
CLIENT_PROCESS_NAME = "trunk"
|
||||
|
||||
[tasks.build]
|
||||
command = "trunk"
|
||||
args = ["build"]
|
||||
|
||||
[tasks.start-trunk]
|
||||
[tasks.start-client]
|
||||
command = "trunk"
|
||||
args = ["serve", "${@}"]
|
||||
|
||||
[tasks.stop-trunk]
|
||||
script = '''
|
||||
pkill -f "cargo-make"
|
||||
pkill -f "trunk"
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.dev]
|
||||
dependencies = ["start-trunk"]
|
||||
|
||||
@@ -3,9 +3,8 @@ use leptos::{ev, html::*, *};
|
||||
/// A simple counter view.
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(Count::new(initial_value, step));
|
||||
let count = RwSignal::new(Count::new(initial_value, step));
|
||||
|
||||
// elements are created by calling a function with a Scope argument
|
||||
// the function name is the same as the HTML tag name
|
||||
div()
|
||||
// children can be added with .child()
|
||||
@@ -18,27 +17,16 @@ pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
// typed events found in leptos::ev
|
||||
// 1) prevent typos in event names
|
||||
// 2) allow for correct type inference in callbacks
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.on(ev::click, move |_| count.update(Count::clear))
|
||||
.child("Clear"),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.on(ev::click, move |_| count.update(Count::decrease))
|
||||
.child("-1"),
|
||||
span()
|
||||
.child("Value: ")
|
||||
// reactive values are passed to .child() as a tuple
|
||||
// (Scope, [child function]) so an effect can be created
|
||||
.child(move || count.get().value())
|
||||
.child("!"),
|
||||
))
|
||||
.child(
|
||||
span().child(("Value: ", move || count.get().value(), "!")),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
.on(ev::click, move |_| count.update(Count::increase))
|
||||
.child("+1"),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
20
examples/error_boundary/.gitignore
vendored
Normal file
20
examples/error_boundary/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Support playwright testing
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Support trunk
|
||||
dist
|
||||
@@ -1 +1,4 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/playwright-trunk-test.toml" },
|
||||
]
|
||||
|
||||
4
examples/error_boundary/e2e/.gitignore
vendored
Normal file
4
examples/error_boundary/e2e/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
83
examples/error_boundary/e2e/package-lock.json
generated
Normal file
83
examples/error_boundary/e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "grip",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "grip",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
},
|
||||
"node_modules/.pnpm/@playwright+test@1.33.0": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
|
||||
"version": "20.2.1",
|
||||
"extraneous": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
|
||||
"version": "1.33.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
|
||||
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.35.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
|
||||
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
|
||||
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
examples/error_boundary/e2e/package.json
Normal file
7
examples/error_boundary/e2e/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"private": "true",
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
}
|
||||
77
examples/error_boundary/e2e/playwright.config.ts
Normal file
77
examples/error_boundary/e2e/playwright.config.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !process.env.DEV,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.DEV ? 0 : 2,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.DEV ? 1 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [["html", { open: "never" }], ["list"]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1:8080",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "firefox",
|
||||
// use: { ...devices["Desktop Firefox"] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: { ...devices["Desktop Safari"] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: "cd ../ && trunk serve",
|
||||
// url: "http://127.0.0.1:8080",
|
||||
// reuseExistingServer: false, //!process.env.CI,
|
||||
// },
|
||||
});
|
||||
23
examples/error_boundary/e2e/tests/clear_number.spec.ts
Normal file
23
examples/error_boundary/e2e/tests/clear_number.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { HomePage } from "./fixtures/home_page";
|
||||
|
||||
test.describe("Clear Number", () => {
|
||||
test("should see the error message", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.clearInput();
|
||||
|
||||
await expect(ui.errorMessage).toHaveText("Not a number! Errors: ");
|
||||
});
|
||||
test("should see the error list", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.clearInput();
|
||||
|
||||
await expect(ui.errorList).toHaveText(
|
||||
"cannot parse integer from empty string"
|
||||
);
|
||||
});
|
||||
});
|
||||
17
examples/error_boundary/e2e/tests/click_down_arrow.spec.ts
Normal file
17
examples/error_boundary/e2e/tests/click_down_arrow.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { HomePage } from "./fixtures/home_page";
|
||||
|
||||
test.describe("Click Down Arrow", () => {
|
||||
test("should see the negative number", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.clickDownArrow();
|
||||
await ui.clickDownArrow();
|
||||
await ui.clickDownArrow();
|
||||
await ui.clickDownArrow();
|
||||
await ui.clickDownArrow();
|
||||
|
||||
await expect(ui.successMessage).toHaveText("You entered -5");
|
||||
});
|
||||
});
|
||||
15
examples/error_boundary/e2e/tests/click_up_arrow.spec.ts
Normal file
15
examples/error_boundary/e2e/tests/click_up_arrow.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { HomePage } from "./fixtures/home_page";
|
||||
|
||||
test.describe("Click Up Arrow", () => {
|
||||
test("should see the positive number", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.clickUpArrow();
|
||||
await ui.clickUpArrow();
|
||||
await ui.clickUpArrow();
|
||||
|
||||
await expect(ui.successMessage).toHaveText("You entered 3");
|
||||
});
|
||||
});
|
||||
56
examples/error_boundary/e2e/tests/fixtures/home_page.ts
vendored
Normal file
56
examples/error_boundary/e2e/tests/fixtures/home_page.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
|
||||
export class HomePage {
|
||||
readonly page: Page;
|
||||
readonly pageTitle: Locator;
|
||||
readonly numberInput: Locator;
|
||||
readonly successMessage: Locator;
|
||||
readonly errorMessage: Locator;
|
||||
|
||||
readonly errorList: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.pageTitle = page.locator("h1");
|
||||
this.numberInput = page.getByLabel(
|
||||
"Type a number (or something that's not a number!)"
|
||||
);
|
||||
this.successMessage = page.locator("label p");
|
||||
this.errorMessage = page.locator("div p");
|
||||
this.errorList = page.getByRole("list");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/");
|
||||
}
|
||||
|
||||
async enterNumber(count: string, index: number = 0) {
|
||||
await Promise.all([
|
||||
this.numberInput.waitFor(),
|
||||
this.numberInput.fill(count),
|
||||
]);
|
||||
}
|
||||
|
||||
async clickUpArrow() {
|
||||
await Promise.all([
|
||||
this.numberInput.waitFor(),
|
||||
this.numberInput.press("ArrowUp"),
|
||||
]);
|
||||
}
|
||||
|
||||
async clickDownArrow() {
|
||||
await Promise.all([
|
||||
this.numberInput.waitFor(),
|
||||
this.numberInput.press("ArrowDown"),
|
||||
]);
|
||||
}
|
||||
|
||||
async clearInput() {
|
||||
await Promise.all([
|
||||
this.numberInput.waitFor(),
|
||||
this.clickUpArrow(),
|
||||
this.numberInput.press("Backspace"),
|
||||
]);
|
||||
}
|
||||
}
|
||||
11
examples/error_boundary/e2e/tests/open_app.spec.ts
Normal file
11
examples/error_boundary/e2e/tests/open_app.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { HomePage } from "./fixtures/home_page";
|
||||
|
||||
test.describe("Open App", () => {
|
||||
test("should see the page title", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await expect(ui.pageTitle).toHaveText("Error Handling");
|
||||
});
|
||||
});
|
||||
13
examples/error_boundary/e2e/tests/type_number.spec.ts
Normal file
13
examples/error_boundary/e2e/tests/type_number.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { HomePage } from "./fixtures/home_page";
|
||||
|
||||
test.describe("Type Number", () => {
|
||||
test("should see the typed number", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.enterNumber("7");
|
||||
|
||||
await expect(ui.successMessage).toHaveText("You entered 7");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::errors::AppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{Errors, *};
|
||||
use leptos::{logging::log, Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
Router,
|
||||
};
|
||||
use errors_axum::*;
|
||||
use leptos::*;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
}}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos::{logging::log, *};
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
|
||||
@@ -51,8 +51,11 @@ site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style/output.css"
|
||||
# The tailwind input file.
|
||||
#
|
||||
# Optional, Activates the tailwind build
|
||||
tailwind-input-file = "style/tailwind.css"
|
||||
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
|
||||
@@ -1,642 +0,0 @@
|
||||
/*
|
||||
! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
6. Use the user's configured `sans` font-variation-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
font-variation-settings: normal;
|
||||
/* 6 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.m-1 {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
.m-auto {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.h-5 {
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-5 {
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.flex-row-reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.border-b-4 {
|
||||
border-bottom-width: 4px;
|
||||
}
|
||||
|
||||
.border-l-2 {
|
||||
border-left-width: 2px;
|
||||
}
|
||||
|
||||
.border-blue-800 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(30 64 175 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-blue-900 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(30 58 138 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-800 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gradient-to-tl {
|
||||
background-image: linear-gradient(to top left, var(--tw-gradient-stops));
|
||||
}
|
||||
|
||||
.from-blue-800 {
|
||||
--tw-gradient-from: #1e40af var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(30 64 175 / 0) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||
}
|
||||
|
||||
.to-blue-500 {
|
||||
--tw-gradient-to: #3b82f6 var(--tw-gradient-to-position);
|
||||
}
|
||||
|
||||
.fill-current {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.px-3 {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ where
|
||||
}
|
||||
api::Error::Api(err) => err.message,
|
||||
};
|
||||
error!(
|
||||
log::error!(
|
||||
"Unable to login with {}: {msg}",
|
||||
credentials.email
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
Page,
|
||||
};
|
||||
use api_boundary::*;
|
||||
use leptos::*;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod api;
|
||||
use crate::api::*;
|
||||
use leptos::*;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_router::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -122,9 +122,9 @@ pub struct ContactParams {
|
||||
|
||||
#[component]
|
||||
pub fn Contact() -> impl IntoView {
|
||||
log::debug!("rendering <Contact/>");
|
||||
log!("rendering <Contact/>");
|
||||
|
||||
log::debug!(
|
||||
log!(
|
||||
"ExampleContext should be Some(42). It is {:?}",
|
||||
use_context::<ExampleContext>()
|
||||
);
|
||||
@@ -178,13 +178,13 @@ pub fn Contact() -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn About() -> impl IntoView {
|
||||
log::debug!("rendering <About/>");
|
||||
log!("rendering <About/>");
|
||||
|
||||
on_cleanup(|| {
|
||||
log!("cleaning up <About/>");
|
||||
});
|
||||
|
||||
log::debug!(
|
||||
log!(
|
||||
"ExampleContext should be Some(0). It is {:?}",
|
||||
use_context::<ExampleContext>()
|
||||
);
|
||||
@@ -209,7 +209,7 @@ pub fn About() -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn Settings() -> impl IntoView {
|
||||
log::debug!("rendering <Settings/>");
|
||||
log!("rendering <Settings/>");
|
||||
|
||||
on_cleanup(|| {
|
||||
log!("cleaning up <Settings/>");
|
||||
|
||||
@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use session_auth_axum::state::AppState;
|
||||
use session_auth_axum::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
|
||||
use leptos::{log, view, provide_context, get_configuration};
|
||||
use leptos::{logging::log, view, provide_context, get_configuration};
|
||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{routing::post, Router};
|
||||
use leptos::*;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
|
||||
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/webdriver.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
APP_PROCESS_NAME = "suspense_tests"
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = [
|
||||
"install-cargo-leptos",
|
||||
"start-webdriver",
|
||||
"test-e2e-with-auto-start",
|
||||
]
|
||||
|
||||
[tasks.test-e2e-with-auto-start]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
CLIENT_PROCESS_NAME = "suspense_tests"
|
||||
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
|
||||
@@ -156,7 +156,7 @@ fn NestedResourceInside() -> impl IntoView {
|
||||
{move || {
|
||||
one_second.get().map(|_| {
|
||||
let two_second = create_resource(|| (), move |_| async move {
|
||||
leptos::log!("creating two_second resource");
|
||||
logging::log!("creating two_second resource");
|
||||
second_wait_fn(WAIT_TWO_SECONDS).await
|
||||
});
|
||||
view! {
|
||||
|
||||
@@ -84,9 +84,10 @@ site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style/output.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
# The tailwind input file.
|
||||
#
|
||||
# Optional, Activates the tailwind build
|
||||
tailwind-input-file = "style/tailwind.css"
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/playwright.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
CLIENT_PROCESS_NAME = "tailwind"
|
||||
|
||||
@@ -8,10 +8,6 @@ If you don't have `cargo-leptos` installed you can install it with
|
||||
|
||||
Then run
|
||||
|
||||
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
|
||||
|
||||
and
|
||||
|
||||
`cargo leptos watch`
|
||||
|
||||
in this directory.
|
||||
|
||||
2
examples/tailwind/rust-toolchain.toml
Normal file
2
examples/tailwind/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
@@ -12,7 +12,7 @@ cfg_if! {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
log!("hydrate mode - hydrating");
|
||||
logging::log!("hydrate mode - hydrating");
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
@@ -29,7 +29,7 @@ cfg_if! {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
log!("csr mode - mounting to body");
|
||||
logging::log!("csr mode - mounting to body");
|
||||
|
||||
mount_to_body(|| {
|
||||
view! { <App /> }
|
||||
|
||||
@@ -1,650 +0,0 @@
|
||||
/*
|
||||
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
.my-0 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.max-w-3xl {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-sm {
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.bg-amber-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(217 119 6 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(2 132 199 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(125 211 252 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(14 165 233 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(245 158 11 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.px-10 {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.px-5 {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.py-3 {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.pb-10 {
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-200 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 202 202 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(14 165 233 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(125 211 252 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(3 105 161 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(7 89 133 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(153 27 27 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-sky-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
|
||||
}
|
||||
@@ -7,7 +7,7 @@ pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
log!("csr mode - mounting to body");
|
||||
logging::log!("csr mode - mounting to body");
|
||||
|
||||
mount_to_body(|| {
|
||||
view! { <App /> }
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/webdriver.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
APP_PROCESS_NAME = "todo_app_sqlite"
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = [
|
||||
"install-cargo-leptos",
|
||||
"start-webdriver",
|
||||
"test-e2e-with-auto-start",
|
||||
]
|
||||
|
||||
[tasks.test-e2e-with-auto-start]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
CLIENT_PROCESS_NAME = "todo_app_sqlite"
|
||||
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/webdriver.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
APP_PROCESS_NAME = "todo_app_sqlite_axum"
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = [
|
||||
"install-cargo-leptos",
|
||||
"start-webdriver",
|
||||
"test-e2e-with-auto-start",
|
||||
]
|
||||
|
||||
[tasks.test-e2e-with-auto-start]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
CLIENT_PROCESS_NAME = "todo_app_sqlite_axum"
|
||||
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
|
||||
@@ -60,7 +60,7 @@ cfg_if! {
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
logging::log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
|
||||
@@ -67,7 +67,7 @@ cfg_if! {
|
||||
|
||||
// run our app with hyper
|
||||
// `viz::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
logging::log!("listening on http://{}", &addr);
|
||||
viz::Server::bind(&addr)
|
||||
.serve(ServiceMaker::from(app))
|
||||
.await
|
||||
|
||||
@@ -210,13 +210,7 @@ pub fn TodoMVC() -> impl IntoView {
|
||||
// focus the main input on load
|
||||
create_effect(move |_| {
|
||||
if let Some(input) = input_ref.get() {
|
||||
// We use request_animation_frame here because the NodeRef
|
||||
// is filled when the element is created, but before it's mounted
|
||||
// to the DOM. Calling .focus() before it's mounted does nothing.
|
||||
// So inside, we wait a tick for the browser to mount it, then .focus()
|
||||
request_animation_frame(move || {
|
||||
let _ = input.focus();
|
||||
});
|
||||
let _ = input.focus();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -11,14 +11,17 @@ fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
|
||||
Some(val) => val,
|
||||
None => options.reload_port,
|
||||
};
|
||||
let protocol = match options.reload_ws_protocol {
|
||||
leptos_config::ReloadWSProtocol::WS => "'ws://'",
|
||||
leptos_config::ReloadWSProtocol::WSS => "'wss://'",
|
||||
};
|
||||
match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin=""{nonce_str}>(function () {{
|
||||
{}
|
||||
let protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
let host = window.location.hostname;
|
||||
let ws = new WebSocket(protocol + host + ':{reload_port}/live_reload');
|
||||
let ws = new WebSocket({protocol} + host + ':{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
let msg = JSON.parse(ev.data);
|
||||
if (msg.all) window.location.reload();
|
||||
|
||||
@@ -16,14 +16,12 @@ leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.14"
|
||||
typed-builder = "0.16"
|
||||
typed-builder-macro = "0.16"
|
||||
server_fn = { workspace = true }
|
||||
web-sys = { version = "0.3.63", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "." }
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
template_macro = ["leptos_dom/web", "web-sys", "wasm-bindgen"]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::TextProp;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A collection of additional HTML attributes to be applied to an element,
|
||||
/// each of which may or may not be reactive.
|
||||
#[derive(Default, Clone)]
|
||||
#[derive(Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct AdditionalAttributes(pub(crate) Vec<(String, TextProp)>);
|
||||
pub struct AdditionalAttributes(pub(crate) Rc<[(String, TextProp)]>);
|
||||
|
||||
impl<I, T, U> From<I> for AdditionalAttributes
|
||||
where
|
||||
@@ -22,6 +23,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AdditionalAttributes {
|
||||
fn default() -> Self {
|
||||
Self([].into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over additional HTML attributes.
|
||||
#[repr(transparent)]
|
||||
pub struct AdditionalAttributesIter<'a>(
|
||||
|
||||
62
leptos/src/children.rs
Normal file
62
leptos/src/children.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use leptos_dom::Fragment;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// The most common type for the `children` property on components,
|
||||
/// which can only be called once.
|
||||
pub type Children = Box<dyn FnOnce() -> Fragment>;
|
||||
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once.
|
||||
pub type ChildrenFn = Rc<dyn Fn() -> Fragment>;
|
||||
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once, but may mutate the children.
|
||||
pub type ChildrenFnMut = Box<dyn FnMut() -> Fragment>;
|
||||
|
||||
// This is to still support components that accept `Box<dyn Fn() -> Fragment>` as a children.
|
||||
type BoxedChildrenFn = Box<dyn Fn() -> Fragment>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait ToChildren<F> {
|
||||
fn to_children(f: F) -> Self;
|
||||
}
|
||||
|
||||
impl<F> ToChildren<F> for Children
|
||||
where
|
||||
F: FnOnce() -> Fragment + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(f: F) -> Self {
|
||||
Box::new(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> ToChildren<F> for ChildrenFn
|
||||
where
|
||||
F: Fn() -> Fragment + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(f: F) -> Self {
|
||||
Rc::new(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> ToChildren<F> for ChildrenFnMut
|
||||
where
|
||||
F: FnMut() -> Fragment + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(f: F) -> Self {
|
||||
Box::new(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> ToChildren<F> for BoxedChildrenFn
|
||||
where
|
||||
F: Fn() -> Fragment + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(f: F) -> Self {
|
||||
Box::new(f)
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ where
|
||||
matches!(child, View::Suspense(_, _))
|
||||
|| matches!(child, View::Component(repr) if repr.name() == "Transition")
|
||||
}) {
|
||||
crate::debug_warn!("You are using a <Suspense/> or \
|
||||
leptos_dom::logging::console_warn("You are using a <Suspense/> or \
|
||||
<Transition/> as the direct child of an <ErrorBoundary/>. To ensure correct \
|
||||
hydration, these should be reorganized so that the <ErrorBoundary/> is a child \
|
||||
of the <Suspense/> or <Transition/> instead: \n\
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::hash::Hash;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn Counters() -> impl IntoView {
|
||||
/// let (counters, set_counters) = create_signal::<Vec<Counter>>( vec![]);
|
||||
/// let (counters, set_counters) = create_signal::<Vec<Counter>>(vec![]);
|
||||
///
|
||||
/// view! {
|
||||
/// <div>
|
||||
@@ -28,7 +28,7 @@ use std::hash::Hash;
|
||||
/// // a unique key for each item
|
||||
/// key=|counter| counter.id
|
||||
/// // renders each item to a view
|
||||
/// view=move | counter: Counter| {
|
||||
/// view=move |counter: Counter| {
|
||||
/// view! {
|
||||
/// <button>"Value: " {move || counter.count.get()}</button>
|
||||
/// }
|
||||
|
||||
@@ -155,11 +155,15 @@ pub mod ssr {
|
||||
pub use leptos_dom::{ssr::*, ssr_in_order::*};
|
||||
}
|
||||
pub use leptos_dom::{
|
||||
self, create_node_ref, debug_warn, document, error, ev, helpers::*, html,
|
||||
log, math, mount_to, mount_to_body, nonce, svg, warn, window, Attribute,
|
||||
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
|
||||
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
|
||||
self, create_node_ref, document, ev, helpers::*, html, math, mount_to,
|
||||
mount_to_body, nonce, svg, window, Attribute, Class, CollectView, Errors,
|
||||
Fragment, HtmlElement, IntoAttribute, IntoClass, IntoProperty, IntoStyle,
|
||||
IntoView, NodeRef, Property, View,
|
||||
};
|
||||
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||
pub mod logging {
|
||||
pub use leptos_dom::{debug_warn, error, log, warn};
|
||||
}
|
||||
|
||||
/// Types to make it easier to handle errors in your application.
|
||||
pub mod error {
|
||||
@@ -175,7 +179,6 @@ pub use leptos_server::{
|
||||
ServerFnErrorErr,
|
||||
};
|
||||
pub use server_fn::{self, ServerFn as _};
|
||||
pub use typed_builder;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "template_macro"))]
|
||||
pub use {leptos_macro::template, wasm_bindgen, web_sys};
|
||||
mod error_boundary;
|
||||
@@ -195,20 +198,16 @@ pub use text_prop::TextProp;
|
||||
#[doc(hidden)]
|
||||
pub use tracing;
|
||||
pub use transition::*;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder::Optional;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder_macro;
|
||||
mod children;
|
||||
pub use children::*;
|
||||
extern crate self as leptos;
|
||||
|
||||
/// The most common type for the `children` property on components,
|
||||
/// which can only be called once.
|
||||
pub type Children = Box<dyn FnOnce() -> Fragment>;
|
||||
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once.
|
||||
pub type ChildrenFn = Box<dyn Fn() -> Fragment>;
|
||||
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once, but may mutate the children.
|
||||
pub type ChildrenFnMut = Box<dyn FnMut() -> Fragment>;
|
||||
|
||||
/// A type for taking anything that implements [`IntoAttribute`].
|
||||
///
|
||||
/// ```rust
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use leptos::component;
|
||||
use leptos_dom::{Fragment, IntoView};
|
||||
use leptos::{component, ChildrenFn};
|
||||
use leptos_dom::IntoView;
|
||||
use leptos_reactive::{create_memo, signal_prelude::*};
|
||||
|
||||
/// A component that will show its children when the `when` condition is `true`,
|
||||
@@ -38,7 +38,7 @@ pub fn Show<F, W, IV>(
|
||||
/// The scope the component is running in
|
||||
|
||||
/// The components Show wraps
|
||||
children: Box<dyn Fn() -> Fragment>,
|
||||
children: ChildrenFn,
|
||||
/// A closure that returns a bool that determines whether this thing runs
|
||||
when: W,
|
||||
/// A closure that returns what gets rendered if the when statement is false
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use leptos_dom::{DynChild, HydrationCtx, IntoView};
|
||||
use leptos_macro::component;
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
use leptos_reactive::SignalGet;
|
||||
use leptos_reactive::{
|
||||
create_memo, provide_context, SignalGetUntracked, SuspenseContext,
|
||||
};
|
||||
@@ -59,14 +61,14 @@ pub fn Suspense<F, E, V>(
|
||||
/// Returns a fallback UI that will be shown while `async` [`Resource`](leptos_reactive::Resource)s are still loading.
|
||||
fallback: F,
|
||||
/// Children will be displayed once all `async` [`Resource`](leptos_reactive::Resource)s have resolved.
|
||||
children: Box<dyn Fn() -> V>,
|
||||
children: Rc<dyn Fn() -> V>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> E + 'static,
|
||||
E: IntoView,
|
||||
V: IntoView + 'static,
|
||||
{
|
||||
let orig_children = Rc::new(children);
|
||||
let orig_children = children;
|
||||
let context = SuspenseContext::new();
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
@@ -93,6 +95,9 @@ where
|
||||
|
||||
let current_id = HydrationCtx::next_component();
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
let ready = context.ready();
|
||||
|
||||
let child = DynChild::new({
|
||||
move || {
|
||||
// pull lazy memo before checking if context is ready
|
||||
@@ -100,7 +105,7 @@ where
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
{
|
||||
if context.ready() {
|
||||
if ready.get() {
|
||||
children_rendered
|
||||
} else {
|
||||
fallback.get_untracked()
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use leptos_reactive::Oco;
|
||||
use std::{fmt::Debug, rc::Rc};
|
||||
|
||||
/// Describes a value that is either a static or a reactive string, i.e.,
|
||||
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
|
||||
#[derive(Clone)]
|
||||
pub struct TextProp(Rc<dyn Fn() -> String>);
|
||||
pub struct TextProp(Rc<dyn Fn() -> Oco<'static, str>>);
|
||||
|
||||
impl TextProp {
|
||||
/// Accesses the current value of the property.
|
||||
#[inline(always)]
|
||||
pub fn get(&self) -> String {
|
||||
pub fn get(&self) -> Oco<'static, str> {
|
||||
(self.0)()
|
||||
}
|
||||
}
|
||||
@@ -21,23 +22,38 @@ impl Debug for TextProp {
|
||||
|
||||
impl From<String> for TextProp {
|
||||
fn from(s: String) -> Self {
|
||||
let s: Oco<'_, str> = Oco::Counted(Rc::from(s));
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TextProp {
|
||||
fn from(s: &str) -> Self {
|
||||
let s = s.to_string();
|
||||
impl From<&'static str> for TextProp {
|
||||
fn from(s: &'static str) -> Self {
|
||||
let s: Oco<'_, str> = s.into();
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> From<F> for TextProp
|
||||
impl From<Rc<str>> for TextProp {
|
||||
fn from(s: Rc<str>) -> Self {
|
||||
let s: Oco<'_, str> = s.into();
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Oco<'static, str>> for TextProp {
|
||||
fn from(s: Oco<'static, str>) -> Self {
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, S> From<F> for TextProp
|
||||
where
|
||||
F: Fn() -> String + 'static,
|
||||
F: Fn() -> S + 'static,
|
||||
S: Into<Oco<'static, str>>,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn from(s: F) -> Self {
|
||||
TextProp(Rc::new(s))
|
||||
TextProp(Rc::new(move || s().into()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,9 @@ where
|
||||
cfg!(feature = "csr") && first_run.get();
|
||||
let is_first_run =
|
||||
is_first_run(first_run, &suspense_context);
|
||||
first_run.set(false);
|
||||
if was_first_run {
|
||||
first_run.set(false)
|
||||
}
|
||||
|
||||
if let Some(prev_children) = &*prev_child.borrow() {
|
||||
if is_first_run || was_first_run {
|
||||
@@ -112,7 +114,7 @@ where
|
||||
}
|
||||
}
|
||||
})
|
||||
.children(Box::new(move || {
|
||||
.children(Rc::new(move || {
|
||||
let frag = children().into_view();
|
||||
|
||||
if let Some(suspense_context) = use_context::<SuspenseContext>()
|
||||
|
||||
@@ -13,7 +13,7 @@ config = "0.13.3"
|
||||
regex = "1.7.0"
|
||||
serde = { version = "1.0.151", features = ["derive"] }
|
||||
thiserror = "1.0.38"
|
||||
typed-builder = "0.14"
|
||||
typed-builder = "0.16"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
|
||||
@@ -61,6 +61,11 @@ pub struct LeptosOptions {
|
||||
#[builder(default)]
|
||||
#[serde(default)]
|
||||
pub reload_external_port: Option<u32>,
|
||||
/// The protocol the Websocket watcher uses on the client: `ws` in most cases, `wss` when behind a reverse https proxy.
|
||||
/// Defaults to `ws`
|
||||
#[builder(default)]
|
||||
#[serde(default)]
|
||||
pub reload_ws_protocol: ReloadWSProtocol,
|
||||
}
|
||||
|
||||
impl LeptosOptions {
|
||||
@@ -84,7 +89,7 @@ impl LeptosOptions {
|
||||
output_name,
|
||||
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
|
||||
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
|
||||
env: Env::default(),
|
||||
env: env_from_str(env_w_default("LEPTOS_ENV", "DEV")?.as_str())?,
|
||||
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
|
||||
.parse()?,
|
||||
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?
|
||||
@@ -95,6 +100,9 @@ impl LeptosOptions {
|
||||
Some(val) => Some(val.parse()?),
|
||||
None => None,
|
||||
},
|
||||
reload_ws_protocol: ws_from_str(
|
||||
env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -151,45 +159,103 @@ impl Default for Env {
|
||||
}
|
||||
}
|
||||
|
||||
fn from_str(input: &str) -> Result<Env, String> {
|
||||
fn env_from_str(input: &str) -> Result<Env, LeptosConfigError> {
|
||||
let sanitized = input.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
"dev" | "development" => Ok(Env::DEV),
|
||||
"prod" | "production" => Ok(Env::PROD),
|
||||
_ => Err(format!(
|
||||
_ => Err(LeptosConfigError::EnvVarError(format!(
|
||||
"{input} is not a supported environment. Use either `dev` or \
|
||||
`production`.",
|
||||
)),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Env {
|
||||
type Err = ();
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
from_str(input).or_else(|_| Ok(Self::default()))
|
||||
env_from_str(input).or_else(|_| Ok(Self::default()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Env {
|
||||
fn from(str: &str) -> Self {
|
||||
from_str(str).unwrap_or_else(|err| panic!("{}", err))
|
||||
env_from_str(str).unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Result<String, VarError>> for Env {
|
||||
fn from(input: &Result<String, VarError>) -> Self {
|
||||
match input {
|
||||
Ok(str) => from_str(str).unwrap_or_else(|err| panic!("{}", err)),
|
||||
Ok(str) => {
|
||||
env_from_str(str).unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
Err(_) => Self::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Env {
|
||||
type Error = String;
|
||||
type Error = LeptosConfigError;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
from_str(s.as_str())
|
||||
env_from_str(s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum that can be used to define the websocket protocol Leptos uses for hotreloading
|
||||
/// Defaults to `ws`.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
pub enum ReloadWSProtocol {
|
||||
WS,
|
||||
WSS,
|
||||
}
|
||||
|
||||
impl Default for ReloadWSProtocol {
|
||||
fn default() -> Self {
|
||||
Self::WS
|
||||
}
|
||||
}
|
||||
|
||||
fn ws_from_str(input: &str) -> Result<ReloadWSProtocol, LeptosConfigError> {
|
||||
let sanitized = input.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
"ws" | "WS" => Ok(ReloadWSProtocol::WS),
|
||||
"wss" | "WSS" => Ok(ReloadWSProtocol::WSS),
|
||||
_ => Err(LeptosConfigError::EnvVarError(format!(
|
||||
"{input} is not a supported websocket protocol. Use only `ws` or \
|
||||
`wss`.",
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ReloadWSProtocol {
|
||||
type Err = ();
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
ws_from_str(input).or_else(|_| Ok(Self::default()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ReloadWSProtocol {
|
||||
fn from(str: &str) -> Self {
|
||||
ws_from_str(str).unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Result<String, VarError>> for ReloadWSProtocol {
|
||||
fn from(input: &Result<String, VarError>) -> Self {
|
||||
match input {
|
||||
Ok(str) => ws_from_str(str).unwrap_or_else(|err| panic!("{}", err)),
|
||||
Err(_) => Self::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for ReloadWSProtocol {
|
||||
type Error = LeptosConfigError;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
ws_from_str(s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
use crate::{env_w_default, env_wo_default, from_str, Env, LeptosOptions};
|
||||
use crate::{
|
||||
env_from_str, env_w_default, env_wo_default, ws_from_str, Env,
|
||||
LeptosOptions, ReloadWSProtocol,
|
||||
};
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
#[test]
|
||||
fn from_str_env() {
|
||||
assert!(matches!(from_str("dev").unwrap(), Env::DEV));
|
||||
assert!(matches!(from_str("development").unwrap(), Env::DEV));
|
||||
assert!(matches!(from_str("DEV").unwrap(), Env::DEV));
|
||||
assert!(matches!(from_str("DEVELOPMENT").unwrap(), Env::DEV));
|
||||
assert!(matches!(from_str("prod").unwrap(), Env::PROD));
|
||||
assert!(matches!(from_str("production").unwrap(), Env::PROD));
|
||||
assert!(matches!(from_str("PROD").unwrap(), Env::PROD));
|
||||
assert!(matches!(from_str("PRODUCTION").unwrap(), Env::PROD));
|
||||
assert!(from_str("TEST").is_err());
|
||||
assert!(from_str("?").is_err());
|
||||
fn env_from_str_test() {
|
||||
assert!(matches!(env_from_str("dev").unwrap(), Env::DEV));
|
||||
assert!(matches!(env_from_str("development").unwrap(), Env::DEV));
|
||||
assert!(matches!(env_from_str("DEV").unwrap(), Env::DEV));
|
||||
assert!(matches!(env_from_str("DEVELOPMENT").unwrap(), Env::DEV));
|
||||
assert!(matches!(env_from_str("prod").unwrap(), Env::PROD));
|
||||
assert!(matches!(env_from_str("production").unwrap(), Env::PROD));
|
||||
assert!(matches!(env_from_str("PROD").unwrap(), Env::PROD));
|
||||
assert!(matches!(env_from_str("PRODUCTION").unwrap(), Env::PROD));
|
||||
assert!(env_from_str("TEST").is_err());
|
||||
assert!(env_from_str("?").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ws_from_str_test() {
|
||||
assert!(matches!(ws_from_str("ws").unwrap(), ReloadWSProtocol::WS));
|
||||
assert!(matches!(ws_from_str("WS").unwrap(), ReloadWSProtocol::WS));
|
||||
assert!(matches!(ws_from_str("wss").unwrap(), ReloadWSProtocol::WSS));
|
||||
assert!(matches!(ws_from_str("WSS").unwrap(), ReloadWSProtocol::WSS));
|
||||
assert!(ws_from_str("TEST").is_err());
|
||||
assert!(ws_from_str("?").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -49,6 +62,8 @@ fn try_from_env_test() {
|
||||
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80");
|
||||
std::env::set_var("LEPTOS_RELOAD_PORT", "8080");
|
||||
std::env::set_var("LEPTOS_RELOAD_EXTERNAL_PORT", "8080");
|
||||
std::env::set_var("LEPTOS_ENV", "PROD");
|
||||
std::env::set_var("LEPTOS_RELOAD_WS_PROTOCOL", "WSS");
|
||||
|
||||
let config = LeptosOptions::try_from_env().unwrap();
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
@@ -61,4 +76,6 @@ fn try_from_env_test() {
|
||||
);
|
||||
assert_eq!(config.reload_port, 8080);
|
||||
assert_eq!(config.reload_external_port, Some(8080));
|
||||
assert_eq!(config.env, Env::PROD);
|
||||
assert_eq!(config.reload_ws_protocol, ReloadWSProtocol::WSS)
|
||||
}
|
||||
|
||||
323
leptos_dom/src/callback.rs
Normal file
323
leptos_dom/src/callback.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
//! Callbacks define a standard way to store functions and closures,
|
||||
//! in particular for component properties.
|
||||
//!
|
||||
//! # How to use them
|
||||
//! You can always create a callback from a closure, but the prefered way is to use `prop(into)`
|
||||
//! when you define your component:
|
||||
//! ```
|
||||
//! # use leptos::*;
|
||||
//! # use leptos::leptos_dom::{Callback, Callable};
|
||||
//! #[component]
|
||||
//! fn MyComponent(
|
||||
//! #[prop(into)] render_number: Callback<i32, String>,
|
||||
//! ) -> impl IntoView {
|
||||
//! view! {
|
||||
//! <div>
|
||||
//! {render_number.call(42)}
|
||||
//! </div>
|
||||
//! }
|
||||
//! }
|
||||
//! // now you can use it from a closure directly:
|
||||
//! fn test() -> impl IntoView {
|
||||
//! view! {
|
||||
//! <MyComponent render_number = |x: i32| x.to_string()/>
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! *Notes*:
|
||||
//! - in this example, you should use a generic type that implements `Fn(i32) -> String`.
|
||||
//! Callbacks are more usefull when you want optional generic props.
|
||||
//! - All callbacks implement the `Callable` trait. You have to write `my_callback.call(input)`
|
||||
//!
|
||||
//!
|
||||
//! # Types
|
||||
//! This modules defines:
|
||||
//! - [Callback], the most basic callback type
|
||||
//! - [SyncCallback] for scenarios when you need `Send` and `Sync`
|
||||
//! - [HtmlCallback] for a function that returns a [HtmlElement]
|
||||
//! - [ViewCallback] for a function that returns some kind of [view][IntoView]
|
||||
//!
|
||||
//! # Copying vs cloning
|
||||
//! All callbacks type defined in this module are [Clone] but not [Copy].
|
||||
//! To solve this issue, use [StoredValue]; see [StoredCallback] for more
|
||||
//! ```
|
||||
//! # use leptos::*;
|
||||
//! # use leptos::leptos_dom::{Callback, Callable};
|
||||
//! fn test() -> impl IntoView {
|
||||
//! let callback: Callback<i32, String> =
|
||||
//! Callback::new(|x: i32| x.to_string());
|
||||
//! let stored_callback = store_value(callback);
|
||||
//!
|
||||
//! view! {
|
||||
//! <div>
|
||||
//! // `stored_callback` can be moved multiple times
|
||||
//! {move || stored_callback.call(1)}
|
||||
//! {move || stored_callback.call(42)}
|
||||
//! </div>
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Note that for each callback type `T`, `StoredValue<T>` implements `Call`, so you can call them
|
||||
//! without even thinking about it.
|
||||
|
||||
use crate::{AnyElement, ElementDescriptor, HtmlElement, IntoView, View};
|
||||
use leptos_reactive::StoredValue;
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
/// A wrapper trait for calling callbacks.
|
||||
pub trait Callable<In, Out = ()> {
|
||||
/// calls the callback with the specified argument.
|
||||
fn call(&self, input: In) -> Out;
|
||||
}
|
||||
|
||||
/// The most basic leptos callback type.
|
||||
/// For how to use callbacks, see [here][crate::callback]
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::leptos_dom::{Callable, Callback};
|
||||
/// #[component]
|
||||
/// fn MyComponent(
|
||||
/// #[prop(into)] render_number: Callback<i32, String>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// {render_number.call(42)}
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn test() -> impl IntoView {
|
||||
/// view! {
|
||||
/// <MyComponent render_number=move |x: i32| x.to_string()/>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Cloning
|
||||
/// See [StoredCallback]
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Callback<In, Out = ()>(Rc<dyn Fn(In) -> Out>);
|
||||
|
||||
impl<In, Out> Callback<In, Out> {
|
||||
/// creates a new callback from the function or closure
|
||||
pub fn new<F>(f: F) -> Callback<In, Out>
|
||||
where
|
||||
F: Fn(In) -> Out + 'static,
|
||||
{
|
||||
Self(Rc::new(f))
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
|
||||
fn call(&self, input: In) -> Out {
|
||||
(self.0)(input)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, In, Out> From<F> for Callback<In, Out>
|
||||
where
|
||||
F: Fn(In) -> Out + 'static,
|
||||
{
|
||||
fn from(f: F) -> Callback<In, Out> {
|
||||
Callback::new(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// A callback type that implements `Copy`.
|
||||
/// `StoredCallback<In,Out>` is an alias for `StoredValue<Callback<In, Out>>`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::leptos_dom::{Callback, StoredCallback, Callable};
|
||||
/// fn test() -> impl IntoView {
|
||||
/// let callback: Callback<i32, String> =
|
||||
/// Callback::new(|x: i32| x.to_string());
|
||||
/// let stored_callback: StoredCallback<i32, String> =
|
||||
/// store_value(callback);
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// {move || stored_callback.call(1)}
|
||||
/// {move || stored_callback.call(42)}
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Note that in this example, you can replace `Callback` by `SyncCallback` or `ViewCallback`, and
|
||||
/// it will work in the same way.
|
||||
///
|
||||
///
|
||||
/// Note that a prop should never be a [StoredCallback]:
|
||||
/// you have to call [store_value][leptos_reactive::store_value] inside your component code.
|
||||
pub type StoredCallback<In, Out> = StoredValue<Callback<In, Out>>;
|
||||
|
||||
impl<F, In, Out> Callable<In, Out> for StoredValue<F>
|
||||
where
|
||||
F: Callable<In, Out>,
|
||||
{
|
||||
fn call(&self, input: In) -> Out {
|
||||
self.with_value(|cb| cb.call(input))
|
||||
}
|
||||
}
|
||||
|
||||
/// a callback type that is `Send` and `Sync` if the input type is
|
||||
#[derive(Clone)]
|
||||
pub struct SyncCallback<In, Out = ()>(Arc<dyn Fn(In) -> Out>);
|
||||
|
||||
impl<In: 'static, Out: 'static> SyncCallback<In, Out> {
|
||||
/// creates a new callback from the function or closure
|
||||
pub fn new<F>(fun: F) -> Self
|
||||
where
|
||||
F: Fn(In) -> Out + 'static,
|
||||
{
|
||||
Self(Arc::new(fun))
|
||||
}
|
||||
}
|
||||
|
||||
/// A special callback type that returns any Html element.
|
||||
/// You can use it exactly the same way as a classic callback.
|
||||
///
|
||||
/// For how to use callbacks, see [here][crate::callback]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::leptos_dom::{Callable, HtmlCallback};
|
||||
/// #[component]
|
||||
/// fn MyComponent(
|
||||
/// #[prop(into)] render_number: HtmlCallback<i32>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// {render_number.call(42)}
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// fn test() -> impl IntoView {
|
||||
/// view! {
|
||||
/// <MyComponent render_number=move |x: i32| view!{<span>{x}</span>}/>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # `HtmlCallback` with empty input type.
|
||||
/// Note that when `my_html_callback` is `HtmlCallback<()>`, you can use it more easily because it
|
||||
/// implements [IntoView]
|
||||
///
|
||||
/// view!{
|
||||
/// <div>
|
||||
/// {render_number}
|
||||
/// </div>
|
||||
/// }
|
||||
#[derive(Clone)]
|
||||
pub struct HtmlCallback<In = ()>(Rc<dyn Fn(In) -> HtmlElement<AnyElement>>);
|
||||
|
||||
impl<In> HtmlCallback<In> {
|
||||
/// creates a new callback from the function or closure
|
||||
pub fn new<F, H>(f: F) -> Self
|
||||
where
|
||||
F: Fn(In) -> HtmlElement<H> + 'static,
|
||||
H: ElementDescriptor + 'static,
|
||||
{
|
||||
Self(Rc::new(move |x| f(x).into_any()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<In> Callable<In, HtmlElement<AnyElement>> for HtmlCallback<In> {
|
||||
fn call(&self, input: In) -> HtmlElement<AnyElement> {
|
||||
(self.0)(input)
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, F, H> From<F> for HtmlCallback<In>
|
||||
where
|
||||
F: Fn(In) -> HtmlElement<H> + 'static,
|
||||
H: ElementDescriptor + 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
HtmlCallback(Rc::new(move |x| f(x).into_any()))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for HtmlCallback<()> {
|
||||
fn into_view(self) -> View {
|
||||
self.call(()).into_view()
|
||||
}
|
||||
}
|
||||
|
||||
/// A special callback type that returns any [`View`].
|
||||
///
|
||||
/// You can use it exactly the same way as a classic callback.
|
||||
/// For how to use callbacks, see [here][crate::callback]
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::leptos_dom::{ViewCallback, Callable};
|
||||
/// #[component]
|
||||
/// fn MyComponent(
|
||||
/// #[prop(into)] render_number: ViewCallback<i32>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// {render_number.call(42)}
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// fn test() -> impl IntoView {
|
||||
/// view! {
|
||||
/// <MyComponent render_number=move |x: i32| view!{<span>{x}</span>}/>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # `ViewCallback` with empty input type.
|
||||
/// Note that when `my_view_callback` is `ViewCallback<()>`, you can use it more easily because it
|
||||
/// implements [IntoView]
|
||||
///
|
||||
/// view!{
|
||||
/// <div>
|
||||
/// {render_number}
|
||||
/// </div>
|
||||
/// }
|
||||
#[derive(Clone)]
|
||||
pub struct ViewCallback<In>(Rc<dyn Fn(In) -> View>);
|
||||
|
||||
impl<In> ViewCallback<In> {
|
||||
/// creates a new callback from the function or closure
|
||||
fn new<F, V>(f: F) -> Self
|
||||
where
|
||||
F: Fn(In) -> V + 'static,
|
||||
V: IntoView + 'static,
|
||||
{
|
||||
ViewCallback(Rc::new(move |x| f(x).into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<In> Callable<In, View> for ViewCallback<In> {
|
||||
fn call(&self, input: In) -> View {
|
||||
(self.0)(input)
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, F, V> From<F> for ViewCallback<In>
|
||||
where
|
||||
F: Fn(In) -> V + 'static,
|
||||
V: IntoView + 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self::new(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for ViewCallback<()> {
|
||||
fn into_view(self) -> View {
|
||||
self.call(()).into_view()
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,12 @@ pub use dyn_child::*;
|
||||
pub use each::*;
|
||||
pub use errors::*;
|
||||
pub use fragment::*;
|
||||
use leptos_reactive::untrack_with_diagnostics;
|
||||
use leptos_reactive::{untrack_with_diagnostics, Oco};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use once_cell::unsync::OnceCell;
|
||||
use std::fmt;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::rc::Rc;
|
||||
use std::{borrow::Cow, fmt};
|
||||
pub use unit::*;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
@@ -55,7 +55,7 @@ pub struct ComponentRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Rc<OnceCell<()>>,
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
pub(crate) name: Cow<'static, str>,
|
||||
pub(crate) name: Oco<'static, str>,
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: Comment,
|
||||
/// The children of the component.
|
||||
@@ -163,24 +163,24 @@ impl IntoView for ComponentRepr {
|
||||
impl ComponentRepr {
|
||||
/// Creates a new [`Component`].
|
||||
#[inline(always)]
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
|
||||
Self::new_with_id_concrete(name.into(), HydrationCtx::id())
|
||||
}
|
||||
|
||||
/// Creates a new [`Component`] with the given hydration ID.
|
||||
#[inline(always)]
|
||||
pub fn new_with_id(
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
id: HydrationKey,
|
||||
) -> Self {
|
||||
Self::new_with_id_concrete(name.into(), id)
|
||||
}
|
||||
|
||||
fn new_with_id_concrete(name: Cow<'static, str>, id: HydrationKey) -> Self {
|
||||
fn new_with_id_concrete(name: Oco<'static, str>, id: HydrationKey) -> Self {
|
||||
let markers = (
|
||||
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
|
||||
Comment::new(format!("</{name}>"), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Owned(format!("<{name}>")), &id, false),
|
||||
Comment::new(format!("<{name}>"), &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -236,7 +236,7 @@ where
|
||||
V: IntoView,
|
||||
{
|
||||
id: HydrationKey,
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
children_fn: F,
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ where
|
||||
V: IntoView,
|
||||
{
|
||||
/// Creates a new component.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>, f: F) -> Self {
|
||||
pub fn new(name: impl Into<Oco<'static, str>>, f: F) -> Self {
|
||||
Self {
|
||||
id: HydrationCtx::id(),
|
||||
name: name.into(),
|
||||
|
||||
@@ -3,11 +3,11 @@ use crate::{
|
||||
Comment, IntoView, View,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use std::{borrow::Cow, cell::RefCell, fmt, ops::Deref, rc::Rc};
|
||||
use std::{cell::RefCell, fmt, ops::Deref, rc::Rc};
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use crate::{mount_child, prepare_to_move, unmount_child, MountKind, Mountable, Text};
|
||||
use leptos_reactive::create_effect;
|
||||
use leptos_reactive::create_render_effect;
|
||||
use wasm_bindgen::JsCast;
|
||||
}
|
||||
}
|
||||
@@ -83,9 +83,9 @@ impl Mountable for DynChildRepr {
|
||||
impl DynChildRepr {
|
||||
fn new_with_id(id: HydrationKey) -> Self {
|
||||
let markers = (
|
||||
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
|
||||
Comment::new("</DynChild>", &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Borrowed("<DynChild>"), &id, false),
|
||||
Comment::new("<DynChild>", &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -181,194 +181,204 @@ where
|
||||
let span = tracing::Span::current();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
create_effect(move |prev_run: Option<Option<web_sys::Node>>| {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.enter();
|
||||
create_render_effect(
|
||||
move |prev_run: Option<Option<web_sys::Node>>| {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.enter();
|
||||
|
||||
let new_child = child_fn().into_view();
|
||||
let new_child = child_fn().into_view();
|
||||
|
||||
let mut child_borrow = child.borrow_mut();
|
||||
let mut child_borrow = child.borrow_mut();
|
||||
|
||||
// Is this at least the second time we are loading a child?
|
||||
if let Some(prev_t) = prev_run {
|
||||
let child = child_borrow.take().unwrap();
|
||||
// Is this at least the second time we are loading a child?
|
||||
if let Some(prev_t) = prev_run {
|
||||
let child = child_borrow.take().unwrap();
|
||||
|
||||
// We need to know if our child wasn't moved elsewhere.
|
||||
// If it was, `DynChild` no longer "owns" that child, and
|
||||
// is therefore no longer sound to unmount it from the DOM
|
||||
// or to reuse it in the case of a text node
|
||||
// We need to know if our child wasn't moved elsewhere.
|
||||
// If it was, `DynChild` no longer "owns" that child, and
|
||||
// is therefore no longer sound to unmount it from the DOM
|
||||
// or to reuse it in the case of a text node
|
||||
|
||||
// TODO check does this still detect moves correctly?
|
||||
let was_child_moved = prev_t.is_none()
|
||||
&& child
|
||||
.get_closing_node()
|
||||
.next_non_view_marker_sibling()
|
||||
.as_ref()
|
||||
!= Some(&closing);
|
||||
// TODO check does this still detect moves correctly?
|
||||
let was_child_moved = prev_t.is_none()
|
||||
&& child
|
||||
.get_closing_node()
|
||||
.next_non_view_marker_sibling()
|
||||
.as_ref()
|
||||
!= Some(&closing);
|
||||
|
||||
// If the previous child was a text node, we would like to
|
||||
// make use of it again if our current child is also a text
|
||||
// node
|
||||
let ret = if let Some(prev_t) = prev_t {
|
||||
// Here, our child is also a text node
|
||||
// If the previous child was a text node, we would like to
|
||||
// make use of it again if our current child is also a text
|
||||
// node
|
||||
let ret = if let Some(prev_t) = prev_t {
|
||||
// Here, our child is also a text node
|
||||
|
||||
// nb: the match/ownership gymnastics here
|
||||
// are so that, if we can reuse the text node,
|
||||
// we can take ownership of new_t so we don't clone
|
||||
// the contents, which in O(n) on the length of the text
|
||||
if matches!(new_child, View::Text(_)) {
|
||||
if !was_child_moved && child != new_child {
|
||||
let mut new_t = match new_child {
|
||||
View::Text(t) => t,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
prev_t
|
||||
.unchecked_ref::<web_sys::Text>()
|
||||
.set_data(&new_t.content);
|
||||
// nb: the match/ownership gymnastics here
|
||||
// are so that, if we can reuse the text node,
|
||||
// we can take ownership of new_t so we don't clone
|
||||
// the contents, which in O(n) on the length of the text
|
||||
if matches!(new_child, View::Text(_)) {
|
||||
if !was_child_moved && child != new_child {
|
||||
let mut new_t = match new_child {
|
||||
View::Text(t) => t,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
prev_t
|
||||
.unchecked_ref::<web_sys::Text>()
|
||||
.set_data(&new_t.content);
|
||||
|
||||
// replace new_t's text node with the prev node
|
||||
// see discussion: https://github.com/leptos-rs/leptos/pull/1472
|
||||
new_t.node = prev_t.clone();
|
||||
// replace new_t's text node with the prev node
|
||||
// see discussion: https://github.com/leptos-rs/leptos/pull/1472
|
||||
new_t.node = prev_t.clone();
|
||||
|
||||
let new_child = View::Text(new_t);
|
||||
**child_borrow = Some(new_child);
|
||||
let new_child = View::Text(new_t);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
Some(prev_t)
|
||||
} else {
|
||||
let new_t = new_child.as_text().unwrap();
|
||||
Some(prev_t)
|
||||
} else {
|
||||
let new_t = new_child.as_text().unwrap();
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
|
||||
**child_borrow = Some(new_child.clone());
|
||||
|
||||
Some(new_t.node.clone())
|
||||
}
|
||||
}
|
||||
// Child is not a text node, so we can remove the previous
|
||||
// text node
|
||||
else {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the text
|
||||
closing
|
||||
.previous_non_view_marker_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
}
|
||||
|
||||
// Mount the new child, and we're done
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
|
||||
**child_borrow = Some(new_child.clone());
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
Some(new_t.node.clone())
|
||||
None
|
||||
}
|
||||
}
|
||||
// Child is not a text node, so we can remove the previous
|
||||
// text node
|
||||
// Otherwise, the new child can still be a text node,
|
||||
// but we know the previous child was not, so no special
|
||||
// treatment here
|
||||
else {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the text
|
||||
closing
|
||||
.previous_non_view_marker_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
// Technically, I think this check shouldn't be necessary, but
|
||||
// I can imagine some edge case that the child changes while
|
||||
// hydration is ongoing
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
let same_child = child == new_child;
|
||||
if !was_child_moved && !same_child {
|
||||
// Remove the child
|
||||
let start = child.get_opening_node();
|
||||
let end = &closing;
|
||||
|
||||
match child {
|
||||
View::CoreComponent(
|
||||
crate::CoreComponent::DynChild(
|
||||
child,
|
||||
),
|
||||
) => {
|
||||
let start =
|
||||
child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
View::Component(child) => {
|
||||
let start =
|
||||
child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
_ => unmount_child(&start, end),
|
||||
}
|
||||
}
|
||||
|
||||
// Mount the new child
|
||||
// If it's the same child, don't re-mount
|
||||
if !same_child {
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mount the new child, and we're done
|
||||
// We want to reuse text nodes, so hold onto it if
|
||||
// our child is one
|
||||
let t =
|
||||
new_child.get_text().map(|t| t.node.clone());
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
t
|
||||
};
|
||||
|
||||
ret
|
||||
}
|
||||
// Otherwise, we know for sure this is our first time
|
||||
else {
|
||||
// If it's a text node, we want to use the old text node
|
||||
// as the text node for the DynChild, rather than the new
|
||||
// text node being created during hydration
|
||||
let new_child = if HydrationCtx::is_hydrating()
|
||||
&& new_child.get_text().is_some()
|
||||
{
|
||||
let t = closing
|
||||
.previous_non_view_marker_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Text>();
|
||||
|
||||
let new_child = match new_child {
|
||||
View::Text(text) => text,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
t.set_data(&new_child.content);
|
||||
View::Text(Text {
|
||||
node: t.unchecked_into(),
|
||||
content: new_child.content,
|
||||
})
|
||||
} else {
|
||||
new_child
|
||||
};
|
||||
|
||||
// If we are not hydrating, we simply mount the child
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
// Otherwise, the new child can still be a text node,
|
||||
// but we know the previous child was not, so no special
|
||||
// treatment here
|
||||
else {
|
||||
// Technically, I think this check shouldn't be necessary, but
|
||||
// I can imagine some edge case that the child changes while
|
||||
// hydration is ongoing
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
let same_child = child == new_child;
|
||||
if !was_child_moved && !same_child {
|
||||
// Remove the child
|
||||
let start = child.get_opening_node();
|
||||
let end = &closing;
|
||||
|
||||
match child {
|
||||
View::CoreComponent(
|
||||
crate::CoreComponent::DynChild(child),
|
||||
) => {
|
||||
let start = child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
View::Component(child) => {
|
||||
let start = child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
_ => unmount_child(&start, end),
|
||||
}
|
||||
}
|
||||
|
||||
// Mount the new child
|
||||
// If it's the same child, don't re-mount
|
||||
if !same_child {
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We want to reuse text nodes, so hold onto it if
|
||||
// our child is one
|
||||
// We want to update text nodes, rather than replace them, so
|
||||
// make sure to hold onto the text node
|
||||
let t = new_child.get_text().map(|t| t.node.clone());
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
t
|
||||
};
|
||||
|
||||
ret
|
||||
}
|
||||
// Otherwise, we know for sure this is our first time
|
||||
else {
|
||||
// If it's a text node, we want to use the old text node
|
||||
// as the text node for the DynChild, rather than the new
|
||||
// text node being created during hydration
|
||||
let new_child = if HydrationCtx::is_hydrating()
|
||||
&& new_child.get_text().is_some()
|
||||
{
|
||||
let t = closing
|
||||
.previous_non_view_marker_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Text>();
|
||||
|
||||
let new_child = match new_child {
|
||||
View::Text(text) => text,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
t.set_data(&new_child.content);
|
||||
View::Text(Text {
|
||||
node: t.unchecked_into(),
|
||||
content: new_child.content,
|
||||
})
|
||||
} else {
|
||||
new_child
|
||||
};
|
||||
|
||||
// If we are not hydrating, we simply mount the child
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
}
|
||||
|
||||
// We want to update text nodes, rather than replace them, so
|
||||
// make sure to hold onto the text node
|
||||
let t = new_child.get_text().map(|t| t.node.clone());
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
t
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use crate::hydration::HydrationKey;
|
||||
use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View};
|
||||
use leptos_reactive::{as_child_of_current_owner, Disposer};
|
||||
use std::{borrow::Cow, cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc};
|
||||
use std::{cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use web::*;
|
||||
|
||||
@@ -12,7 +12,7 @@ mod web {
|
||||
mount_child, prepare_to_move, MountKind, Mountable, RANGE,
|
||||
};
|
||||
pub use drain_filter_polyfill::VecExt as VecDrainFilterExt;
|
||||
pub use leptos_reactive::create_effect;
|
||||
pub use leptos_reactive::create_render_effect;
|
||||
pub use std::cell::OnceCell;
|
||||
pub use wasm_bindgen::JsCast;
|
||||
}
|
||||
@@ -79,9 +79,9 @@ impl Default for EachRepr {
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
let markers = (
|
||||
Comment::new(Cow::Borrowed("</Each>"), &id, true),
|
||||
Comment::new("</Each>", &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Borrowed("<Each>"), &id, false),
|
||||
Comment::new("<Each>", &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -224,13 +224,13 @@ impl EachItem {
|
||||
|
||||
let markers = (
|
||||
if needs_closing {
|
||||
Some(Comment::new(Cow::Borrowed("</EachItem>"), &id, true))
|
||||
Some(Comment::new("</EachItem>", &id, true))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
#[cfg(debug_assertions)]
|
||||
if needs_closing {
|
||||
Some(Comment::new(Cow::Borrowed("<EachItem>"), &id, false))
|
||||
Some(Comment::new("<EachItem>", &id, false))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -406,90 +406,100 @@ where
|
||||
let each_fn = as_child_of_current_owner(each_fn);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
create_effect(move |prev_hash_run: Option<HashRun<FxIndexSet<K>>>| {
|
||||
let mut children_borrow = children.borrow_mut();
|
||||
create_render_effect(
|
||||
move |prev_hash_run: Option<HashRun<FxIndexSet<K>>>| {
|
||||
let mut children_borrow = children.borrow_mut();
|
||||
|
||||
#[cfg(all(
|
||||
not(debug_assertions),
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
let opening = if let Some(Some(child)) = children_borrow.get(0) {
|
||||
// correctly remove opening <!--<EachItem/>-->
|
||||
let child_opening = child.get_opening_node();
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(all(
|
||||
not(debug_assertions),
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
let opening = if let Some(Some(child)) = children_borrow.get(0)
|
||||
{
|
||||
use crate::components::dyn_child::NonViewMarkerSibling;
|
||||
child_opening
|
||||
.previous_non_view_marker_sibling()
|
||||
.unwrap_or(child_opening)
|
||||
// correctly remove opening <!--<EachItem/>-->
|
||||
let child_opening = child.get_opening_node();
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
use crate::components::dyn_child::NonViewMarkerSibling;
|
||||
child_opening
|
||||
.previous_non_view_marker_sibling()
|
||||
.unwrap_or(child_opening)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
child_opening
|
||||
}
|
||||
} else {
|
||||
closing.clone()
|
||||
};
|
||||
|
||||
let items_iter = items_fn().into_iter();
|
||||
|
||||
let (capacity, _) = items_iter.size_hint();
|
||||
let mut hashed_items = FxIndexSet::with_capacity_and_hasher(
|
||||
capacity,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
if let Some(HashRun(prev_hash_run)) = prev_hash_run {
|
||||
if !prev_hash_run.is_empty() {
|
||||
let mut items = Vec::with_capacity(capacity);
|
||||
for item in items_iter {
|
||||
hashed_items.insert(key_fn(&item));
|
||||
items.push(Some(item));
|
||||
}
|
||||
|
||||
let cmds = diff(&prev_hash_run, &hashed_items);
|
||||
|
||||
apply_diff(
|
||||
#[cfg(all(
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
&opening,
|
||||
#[cfg(all(
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
&closing,
|
||||
cmds,
|
||||
&mut children_borrow,
|
||||
items,
|
||||
&each_fn,
|
||||
);
|
||||
return HashRun(hashed_items);
|
||||
}
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
child_opening
|
||||
}
|
||||
} else {
|
||||
closing.clone()
|
||||
};
|
||||
|
||||
let items_iter = items_fn().into_iter();
|
||||
// if previous run is empty
|
||||
*children_borrow = Vec::with_capacity(capacity);
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
|
||||
let (capacity, _) = items_iter.size_hint();
|
||||
let mut hashed_items = FxIndexSet::with_capacity_and_hasher(
|
||||
capacity,
|
||||
Default::default(),
|
||||
);
|
||||
for item in items_iter {
|
||||
hashed_items.insert(key_fn(&item));
|
||||
let (child, disposer) = each_fn(item);
|
||||
let each_item = EachItem::new(disposer, child.into_view());
|
||||
|
||||
if let Some(HashRun(prev_hash_run)) = prev_hash_run {
|
||||
if !prev_hash_run.is_empty() {
|
||||
let mut items = Vec::with_capacity(capacity);
|
||||
for item in items_iter {
|
||||
hashed_items.insert(key_fn(&item));
|
||||
items.push(Some(item));
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
_ = fragment
|
||||
.append_child(&each_item.get_mountable_node());
|
||||
}
|
||||
|
||||
let cmds = diff(&prev_hash_run, &hashed_items);
|
||||
|
||||
apply_diff(
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
&opening,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
&closing,
|
||||
cmds,
|
||||
&mut children_borrow,
|
||||
items,
|
||||
&each_fn,
|
||||
);
|
||||
return HashRun(hashed_items);
|
||||
children_borrow.push(Some(each_item));
|
||||
}
|
||||
}
|
||||
|
||||
// if previous run is empty
|
||||
*children_borrow = Vec::with_capacity(capacity);
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
|
||||
for item in items_iter {
|
||||
hashed_items.insert(key_fn(&item));
|
||||
let (child, disposer) = each_fn(item);
|
||||
let each_item = EachItem::new(disposer, child.into_view());
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
_ = fragment.append_child(&each_item.get_mountable_node());
|
||||
}
|
||||
closing
|
||||
.unchecked_ref::<web_sys::Element>()
|
||||
.before_with_node_1(&fragment)
|
||||
.expect("before to not err");
|
||||
|
||||
children_borrow.push(Some(each_item));
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
closing
|
||||
.unchecked_ref::<web_sys::Element>()
|
||||
.before_with_node_1(&fragment)
|
||||
.expect("before to not err");
|
||||
|
||||
HashRun(hashed_items)
|
||||
});
|
||||
HashRun(hashed_items)
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod typed;
|
||||
|
||||
use std::{borrow::Cow, cell::RefCell, collections::HashSet};
|
||||
use leptos_reactive::Oco;
|
||||
use std::{cell::RefCell, collections::HashSet};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::{
|
||||
convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue,
|
||||
@@ -8,7 +9,7 @@ use wasm_bindgen::{
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
pub(crate) static GLOBAL_EVENTS: RefCell<HashSet<Cow<'static, str>>> = RefCell::new(HashSet::new());
|
||||
pub(crate) static GLOBAL_EVENTS: RefCell<HashSet<Oco<'static, str>>> = RefCell::new(HashSet::new());
|
||||
}
|
||||
|
||||
// Used in template macro
|
||||
@@ -47,8 +48,8 @@ pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn add_event_listener<E>(
|
||||
target: &web_sys::Element,
|
||||
key: Cow<'static, str>,
|
||||
event_name: Cow<'static, str>,
|
||||
key: Oco<'static, str>,
|
||||
event_name: Oco<'static, str>,
|
||||
#[cfg(debug_assertions)] mut cb: Box<dyn FnMut(E)>,
|
||||
#[cfg(not(debug_assertions))] cb: Box<dyn FnMut(E)>,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
@@ -115,7 +116,7 @@ pub(crate) fn add_event_listener_undelegated<E>(
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn add_delegated_event_listener(
|
||||
key: &str,
|
||||
event_name: Cow<'static, str>,
|
||||
event_name: Oco<'static, str>,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
) {
|
||||
GLOBAL_EVENTS.with(|global_events| {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Types for all DOM events.
|
||||
|
||||
use std::{borrow::Cow, marker::PhantomData};
|
||||
use leptos_reactive::Oco;
|
||||
use std::marker::PhantomData;
|
||||
use wasm_bindgen::convert::FromWasmAbi;
|
||||
|
||||
/// A trait for converting types into [web_sys events](web_sys).
|
||||
@@ -16,10 +17,10 @@ pub trait EventDescriptor: Clone {
|
||||
const BUBBLES: bool;
|
||||
|
||||
/// The name of the event, such as `click` or `mouseover`.
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
fn name(&self) -> Oco<'static, str>;
|
||||
|
||||
/// The key used for event delegation.
|
||||
fn event_delegation_key(&self) -> Cow<'static, str>;
|
||||
fn event_delegation_key(&self) -> Oco<'static, str>;
|
||||
|
||||
/// Return the options for this type. This is only used when you create a [`Custom`] event
|
||||
/// handler.
|
||||
@@ -39,12 +40,12 @@ impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
|
||||
type EventType = Ev::EventType;
|
||||
|
||||
#[inline(always)]
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
self.0.name()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn event_delegation_key(&self) -> Cow<'static, str> {
|
||||
fn event_delegation_key(&self) -> Oco<'static, str> {
|
||||
self.0.event_delegation_key()
|
||||
}
|
||||
|
||||
@@ -54,7 +55,7 @@ impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
|
||||
/// A custom event.
|
||||
#[derive(Debug)]
|
||||
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
options: Option<web_sys::AddEventListenerOptions>,
|
||||
_event_type: PhantomData<E>,
|
||||
}
|
||||
@@ -72,11 +73,11 @@ impl<E: FromWasmAbi> Clone for Custom<E> {
|
||||
impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
|
||||
type EventType = E;
|
||||
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn event_delegation_key(&self) -> Cow<'static, str> {
|
||||
fn event_delegation_key(&self) -> Oco<'static, str> {
|
||||
format!("$$${}", self.name).into()
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@ impl<E: FromWasmAbi> Custom<E> {
|
||||
/// Creates a custom event type that can be used within
|
||||
/// [`HtmlElement::on`](crate::HtmlElement::on), for events
|
||||
/// which are not covered in the [`ev`](crate::ev) module.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
options: None,
|
||||
@@ -299,12 +300,12 @@ macro_rules! generate_event_types {
|
||||
type EventType = web_sys::$web_event;
|
||||
|
||||
#[inline(always)]
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
stringify!([< $($event)+ >]).into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn event_delegation_key(&self) -> Cow<'static, str> {
|
||||
fn event_delegation_key(&self) -> Oco<'static, str> {
|
||||
concat!("$$$", stringify!([< $($event)+ >])).into()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ pub fn set_property(
|
||||
}
|
||||
|
||||
/// Gets the value of a property set on a DOM element.
|
||||
#[doc(hidden)]
|
||||
pub fn get_property(
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
@@ -245,7 +246,7 @@ pub fn set_timeout_with_handle(
|
||||
/// listeners to prevent them from firing constantly as you type.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::{leptos_dom::helpers::debounce, *};
|
||||
/// use leptos::{leptos_dom::helpers::debounce, logging::log, *};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn DebouncedButton() -> impl IntoView {
|
||||
@@ -429,7 +430,7 @@ pub fn window_event_listener_untyped(
|
||||
/// Creates a window event listener from a typed event, returning a
|
||||
/// cancelable handle.
|
||||
/// ```
|
||||
/// use leptos::{leptos_dom::helpers::window_event_listener, *};
|
||||
/// use leptos::{leptos_dom::helpers::window_event_listener, logging::log, *};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn App() -> impl IntoView {
|
||||
|
||||
@@ -63,15 +63,18 @@ cfg_if! {
|
||||
use crate::{
|
||||
ev::EventDescriptor,
|
||||
hydration::HydrationCtx,
|
||||
macro_helpers::{IntoAttribute, IntoClass, IntoProperty, IntoStyle},
|
||||
macro_helpers::{
|
||||
Attribute, IntoAttribute, IntoClass, IntoProperty, IntoStyle,
|
||||
},
|
||||
Element, Fragment, IntoView, NodeRef, Text, View,
|
||||
};
|
||||
use std::{borrow::Cow, fmt};
|
||||
use leptos_reactive::Oco;
|
||||
use std::fmt;
|
||||
|
||||
/// Trait which allows creating an element tag.
|
||||
pub trait ElementDescriptor: ElementDescriptorBounds {
|
||||
/// The name of the element, i.e., `div`, `p`, `custom-element`.
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
fn name(&self) -> Oco<'static, str>;
|
||||
|
||||
/// Determines if the tag is void, i.e., `<input>` and `<br>`.
|
||||
#[inline(always)]
|
||||
@@ -126,7 +129,7 @@ where
|
||||
/// Represents potentially any element.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnyElement {
|
||||
pub(crate) name: Cow<'static, str>,
|
||||
pub(crate) name: Oco<'static, str>,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) element: web_sys::HtmlElement,
|
||||
pub(crate) is_void: bool,
|
||||
@@ -159,7 +162,7 @@ impl std::convert::AsRef<web_sys::HtmlElement> for AnyElement {
|
||||
}
|
||||
|
||||
impl ElementDescriptor for AnyElement {
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
@@ -178,7 +181,7 @@ impl ElementDescriptor for AnyElement {
|
||||
/// Represents a custom HTML element, such as `<my-element>`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Custom {
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element: web_sys::HtmlElement,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
@@ -187,7 +190,7 @@ pub struct Custom {
|
||||
|
||||
impl Custom {
|
||||
/// Creates a new custom element with the given tag name.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
|
||||
let name = name.into();
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
@@ -266,7 +269,7 @@ impl std::convert::AsRef<web_sys::HtmlElement> for Custom {
|
||||
}
|
||||
|
||||
impl ElementDescriptor for Custom {
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
@@ -294,12 +297,12 @@ cfg_if! {
|
||||
#[derive(educe::Educe, Clone)]
|
||||
#[educe(Debug)]
|
||||
pub struct HtmlElement<El: ElementDescriptor> {
|
||||
pub(crate) element: El,
|
||||
pub(crate) attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
|
||||
#[educe(Debug(ignore))]
|
||||
pub(crate) children: ElementChildren,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>
|
||||
pub(crate) element: El,
|
||||
pub(crate) attrs: SmallVec<[(Oco<'static, str>, Oco<'static, str>); 4]>,
|
||||
#[educe(Debug(ignore))]
|
||||
pub(crate) children: ElementChildren,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Clone, educe::Educe, PartialEq, Eq)]
|
||||
@@ -308,14 +311,14 @@ cfg_if! {
|
||||
#[educe(Default)]
|
||||
Empty,
|
||||
Children(Vec<View>),
|
||||
InnerHtml(Cow<'static, str>),
|
||||
InnerHtml(Oco<'static, str>),
|
||||
Chunks(Vec<StringOrView>)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone)]
|
||||
pub enum StringOrView {
|
||||
String(Cow<'static, str>),
|
||||
String(Oco<'static, str>),
|
||||
View(std::rc::Rc<dyn Fn() -> View>)
|
||||
}
|
||||
|
||||
@@ -445,7 +448,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
/// Adds an `id` to the element.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn id(self, id: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn id(self, id: impl Into<Oco<'static, str>>) -> Self {
|
||||
let id = id.into();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -575,7 +578,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
#[cfg_attr(all(target_arch = "wasm32", feature = "web"), inline(always))]
|
||||
pub fn attr(
|
||||
self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
attr: impl IntoAttribute,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
@@ -592,8 +595,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
use crate::macro_helpers::Attribute;
|
||||
|
||||
let mut this = self;
|
||||
|
||||
let mut attr = attr.into_attribute();
|
||||
@@ -621,6 +622,18 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds multiple attributes to the element
|
||||
#[track_caller]
|
||||
pub fn attrs(
|
||||
mut self,
|
||||
attrs: impl std::iter::IntoIterator<Item = (&'static str, Attribute)>,
|
||||
) -> Self {
|
||||
for (name, value) in attrs {
|
||||
self = self.attr(name, value);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a class to an element.
|
||||
///
|
||||
/// **Note**: In the builder syntax, this will be overwritten by the `class`
|
||||
@@ -634,7 +647,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
#[track_caller]
|
||||
pub fn class(
|
||||
self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
class: impl IntoClass,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
@@ -686,7 +699,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
/// Adds a list of classes separated by ASCII whitespace to an element.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn classes(self, classes: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn classes(self, classes: impl Into<Oco<'static, str>>) -> Self {
|
||||
self.classes_inner(&classes.into())
|
||||
}
|
||||
|
||||
@@ -698,7 +711,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = C>,
|
||||
C: Into<Cow<'static, str>>,
|
||||
C: Into<Oco<'static, str>>,
|
||||
{
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
@@ -706,14 +719,14 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
|
||||
let class_list = self.element.as_ref().class_list();
|
||||
|
||||
leptos_reactive::create_effect(
|
||||
leptos_reactive::create_render_effect(
|
||||
move |prev_classes: Option<
|
||||
SmallVec<[Cow<'static, str>; 4]>,
|
||||
SmallVec<[Oco<'static, str>; 4]>,
|
||||
>| {
|
||||
let classes = classes_signal()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<SmallVec<[Cow<'static, str>; 4]>>(
|
||||
.collect::<SmallVec<[Oco<'static, str>; 4]>>(
|
||||
);
|
||||
|
||||
let new_classes = classes
|
||||
@@ -797,7 +810,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
#[track_caller]
|
||||
pub fn style(
|
||||
self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
style: impl IntoStyle,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
@@ -856,7 +869,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
#[track_caller]
|
||||
pub fn prop(
|
||||
self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
value: impl IntoProperty,
|
||||
) -> Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -1016,7 +1029,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
/// sanitize the input to avoid a cross-site scripting (XSS)
|
||||
/// vulnerability.
|
||||
#[inline(always)]
|
||||
pub fn inner_html(self, html: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn inner_html(self, html: impl Into<Oco<'static, str>>) -> Self {
|
||||
let html = html.into();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -1103,7 +1116,7 @@ pub fn custom<El: ElementDescriptor>(el: El) -> HtmlElement<Custom> {
|
||||
|
||||
/// Creates a text node.
|
||||
#[inline(always)]
|
||||
pub fn text(text: impl Into<Cow<'static, str>>) -> Text {
|
||||
pub fn text(text: impl Into<Oco<'static, str>>) -> Text {
|
||||
Text::new(text.into())
|
||||
}
|
||||
|
||||
@@ -1190,7 +1203,7 @@ macro_rules! generate_html_tags {
|
||||
|
||||
impl ElementDescriptor for [<$tag:camel $($trailing_)?>] {
|
||||
#[inline(always)]
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
stringify!($tag).into()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
#[cfg_attr(any(debug_assertions, feature = "ssr"), macro_use)]
|
||||
pub extern crate tracing;
|
||||
|
||||
pub mod callback;
|
||||
mod components;
|
||||
mod events;
|
||||
pub mod helpers;
|
||||
pub mod html;
|
||||
mod hydration;
|
||||
mod logging;
|
||||
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||
pub mod logging;
|
||||
mod macro_helpers;
|
||||
pub mod math;
|
||||
mod node_ref;
|
||||
@@ -24,6 +26,7 @@ pub mod ssr;
|
||||
pub mod ssr_in_order;
|
||||
pub mod svg;
|
||||
mod transparent;
|
||||
pub use callback::*;
|
||||
use cfg_if::cfg_if;
|
||||
pub use components::*;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -34,11 +37,11 @@ pub use events::{typed as ev, typed::EventHandler};
|
||||
pub use html::HtmlElement;
|
||||
use html::{AnyElement, ElementDescriptor};
|
||||
pub use hydration::{HydrationCtx, HydrationKey};
|
||||
use leptos_reactive::Oco;
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
pub use logging::*;
|
||||
pub use macro_helpers::*;
|
||||
pub use node_ref::*;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -240,7 +243,7 @@ cfg_if! {
|
||||
pub struct Element {
|
||||
#[doc(hidden)]
|
||||
#[cfg(debug_assertions)]
|
||||
pub name: Cow<'static, str>,
|
||||
pub name: Oco<'static, str>,
|
||||
#[doc(hidden)]
|
||||
pub element: web_sys::HtmlElement,
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -261,9 +264,9 @@ cfg_if! {
|
||||
/// HTML element.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Element {
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
is_void: bool,
|
||||
attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
|
||||
attrs: SmallVec<[(Oco<'static, str>, Oco<'static, str>); 4]>,
|
||||
children: ElementChildren,
|
||||
id: HydrationKey,
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -396,13 +399,13 @@ impl Element {
|
||||
struct Comment {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
node: web_sys::Node,
|
||||
content: Cow<'static, str>,
|
||||
content: Oco<'static, str>,
|
||||
}
|
||||
|
||||
impl Comment {
|
||||
#[inline]
|
||||
fn new(
|
||||
content: impl Into<Cow<'static, str>>,
|
||||
content: impl Into<Oco<'static, str>>,
|
||||
id: &HydrationKey,
|
||||
closing: bool,
|
||||
) -> Self {
|
||||
@@ -410,7 +413,7 @@ impl Comment {
|
||||
}
|
||||
|
||||
fn new_inner(
|
||||
content: Cow<'static, str>,
|
||||
content: Oco<'static, str>,
|
||||
id: &HydrationKey,
|
||||
closing: bool,
|
||||
) -> Self {
|
||||
@@ -466,12 +469,13 @@ pub struct Text {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
node: web_sys::Node,
|
||||
/// The current contents of the text node.
|
||||
pub content: Cow<'static, str>,
|
||||
pub content: Oco<'static, str>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Text {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "\"{}\"", self.content)
|
||||
fmt::Debug::fmt(&self.content, f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +488,7 @@ impl IntoView for Text {
|
||||
|
||||
impl Text {
|
||||
/// Creates a new [`Text`].
|
||||
pub fn new(content: Cow<'static, str>) -> Self {
|
||||
pub fn new(content: Oco<'static, str>) -> Self {
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
node: crate::document()
|
||||
@@ -849,7 +853,7 @@ where
|
||||
N: IntoView,
|
||||
{
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
crate::console_warn(
|
||||
crate::logging::console_warn(
|
||||
"You have both `csr` and `ssr` or `hydrate` and `ssr` enabled as \
|
||||
features, which may cause issues like <Suspense/>` failing to work \
|
||||
silently.",
|
||||
|
||||
@@ -6,21 +6,21 @@ use wasm_bindgen::JsValue;
|
||||
/// or via `println!()` (if not in the browser).
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($($t:tt)*) => ($crate::console_log(&format_args!($($t)*).to_string()))
|
||||
($($t:tt)*) => ($crate::logging::console_log(&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).
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
($($t:tt)*) => ($crate::console_warn(&format_args!($($t)*).to_string()))
|
||||
($($t:tt)*) => ($crate::logging::console_warn(&format_args!($($t)*).to_string()))
|
||||
}
|
||||
|
||||
/// Uses `println!()`-style formatting to log errors to the console (in the browser)
|
||||
/// or via `eprintln!()` (if not in the browser).
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($($t:tt)*) => ($crate::console_error(&format_args!($($t)*).to_string()))
|
||||
($($t:tt)*) => ($crate::logging::console_error(&format_args!($($t)*).to_string()))
|
||||
}
|
||||
|
||||
/// Uses `println!()`-style formatting to log warnings to the console (in the browser)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use leptos_reactive::Oco;
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
@@ -14,11 +15,11 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
#[derive(Clone)]
|
||||
pub enum Attribute {
|
||||
/// A plain string value.
|
||||
String(Cow<'static, str>),
|
||||
String(Oco<'static, str>),
|
||||
/// 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<Cow<'static, str>>),
|
||||
Option(Option<Oco<'static, str>>),
|
||||
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
|
||||
Bool(bool),
|
||||
}
|
||||
@@ -29,7 +30,7 @@ impl Attribute {
|
||||
pub fn as_value_string(
|
||||
&self,
|
||||
attr_name: &'static str,
|
||||
) -> Cow<'static, str> {
|
||||
) -> Oco<'static, str> {
|
||||
match self {
|
||||
Attribute::String(value) => {
|
||||
format!("{attr_name}=\"{value}\"").into()
|
||||
@@ -46,14 +47,14 @@ impl Attribute {
|
||||
.map(|value| format!("{attr_name}=\"{value}\"").into())
|
||||
.unwrap_or_default(),
|
||||
Attribute::Bool(include) => {
|
||||
Cow::Borrowed(if *include { attr_name } else { "" })
|
||||
Oco::Borrowed(if *include { attr_name } else { "" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the attribute to its HTML value at that moment, not including
|
||||
/// the attribute name, so it can be rendered on the server.
|
||||
pub fn as_nameless_value_string(&self) -> Option<Cow<'static, str>> {
|
||||
pub fn as_nameless_value_string(&self) -> Option<Oco<'static, str>> {
|
||||
match self {
|
||||
Attribute::String(value) => Some(value.clone()),
|
||||
Attribute::Fn(f) => {
|
||||
@@ -104,6 +105,7 @@ impl std::fmt::Debug for Attribute {
|
||||
pub trait IntoAttribute {
|
||||
/// Converts the object into an [`Attribute`].
|
||||
fn into_attribute(self) -> Attribute;
|
||||
|
||||
/// Helper function for dealing with `Box<dyn IntoAttribute>`.
|
||||
fn into_attribute_boxed(self: Box<Self>) -> Attribute;
|
||||
}
|
||||
@@ -148,7 +150,7 @@ impl IntoAttribute for Option<Attribute> {
|
||||
impl IntoAttribute for String {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(Cow::Owned(self))
|
||||
Attribute::String(Oco::Owned(self))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
@@ -157,13 +159,22 @@ impl IntoAttribute for String {
|
||||
impl IntoAttribute for &'static str {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(Cow::Borrowed(self))
|
||||
Attribute::String(Oco::Borrowed(self))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Cow<'static, str> {
|
||||
impl IntoAttribute for Rc<str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(Oco::Counted(self))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Oco<'static, str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(self)
|
||||
@@ -184,7 +195,7 @@ impl IntoAttribute for bool {
|
||||
impl IntoAttribute for Option<String> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Cow::Owned))
|
||||
Attribute::Option(self.map(Oco::Owned))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
@@ -193,13 +204,31 @@ impl IntoAttribute for Option<String> {
|
||||
impl IntoAttribute for Option<&'static str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Cow::Borrowed))
|
||||
Attribute::Option(self.map(Oco::Borrowed))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Rc<str>> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Oco::Counted))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Cow<'static, str>> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Oco::from))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Oco<'static, str>> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self)
|
||||
@@ -331,7 +360,7 @@ attr_signal_type_optional!(MaybeProp<T>);
|
||||
#[inline(never)]
|
||||
pub fn attribute_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
value: Attribute,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
@@ -21,6 +21,9 @@ pub enum Class {
|
||||
pub trait IntoClass {
|
||||
/// Converts the object into a [`Class`].
|
||||
fn into_class(self) -> Class;
|
||||
|
||||
/// Helper function for dealing with `Box<dyn IntoClass>`.
|
||||
fn into_class_boxed(self: Box<Self>) -> Class;
|
||||
}
|
||||
|
||||
impl IntoClass for bool {
|
||||
@@ -28,6 +31,10 @@ impl IntoClass for bool {
|
||||
fn into_class(self) -> Class {
|
||||
Class::Value(self)
|
||||
}
|
||||
|
||||
fn into_class_boxed(self: Box<Self>) -> Class {
|
||||
(*self).into_class()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoClass for T
|
||||
@@ -39,6 +46,10 @@ where
|
||||
let modified_fn = Box::new(self);
|
||||
Class::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_class_boxed(self: Box<Self>) -> Class {
|
||||
(*self).into_class()
|
||||
}
|
||||
}
|
||||
|
||||
impl Class {
|
||||
@@ -65,14 +76,14 @@ impl Class {
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::borrow::Cow;
|
||||
use leptos_reactive::Oco;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[doc(hidden)]
|
||||
#[inline(never)]
|
||||
pub fn class_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
value: Class,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
@@ -128,6 +139,10 @@ macro_rules! class_signal_type {
|
||||
let modified_fn = Box::new(move || self.get());
|
||||
Class::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_class_boxed(self: Box<Self>) -> Class {
|
||||
(*self).into_class()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -141,6 +156,10 @@ macro_rules! class_signal_type_optional {
|
||||
let modified_fn = Box::new(move || self.get().unwrap_or(false));
|
||||
Class::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_class_boxed(self: Box<Self>) -> Class {
|
||||
(*self).into_class()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
pub enum Property {
|
||||
/// A static JavaScript value.
|
||||
Value(JsValue),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
|
||||
/// A (presumably reactive) function, which will be run inside an effect to update the property.
|
||||
Fn(Box<dyn Fn() -> JsValue>),
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ pub enum Property {
|
||||
pub trait IntoProperty {
|
||||
/// Converts the object into a [`Property`].
|
||||
fn into_property(self) -> Property;
|
||||
|
||||
/// Helper function for dealing with `Box<dyn IntoProperty>`.
|
||||
fn into_property_boxed(self: Box<Self>) -> Property;
|
||||
}
|
||||
|
||||
impl<T, U> IntoProperty for T
|
||||
@@ -36,6 +39,10 @@ where
|
||||
let modified_fn = Box::new(move || self().into());
|
||||
Property::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_property_boxed(self: Box<Self>) -> Property {
|
||||
(*self).into_property()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! prop_type {
|
||||
@@ -45,6 +52,10 @@ macro_rules! prop_type {
|
||||
fn into_property(self) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
|
||||
fn into_property_boxed(self: Box<Self>) -> Property {
|
||||
(*self).into_property()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoProperty for Option<$prop_type> {
|
||||
@@ -52,6 +63,10 @@ macro_rules! prop_type {
|
||||
fn into_property(self) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
|
||||
fn into_property_boxed(self: Box<Self>) -> Property {
|
||||
(*self).into_property()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -67,6 +82,10 @@ macro_rules! prop_signal_type {
|
||||
let modified_fn = Box::new(move || self.get().into());
|
||||
Property::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_property_boxed(self: Box<Self>) -> Property {
|
||||
(*self).into_property()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -83,6 +102,10 @@ macro_rules! prop_signal_type_optional {
|
||||
let modified_fn = Box::new(move || self.get().into());
|
||||
Property::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_property_boxed(self: Box<Self>) -> Property {
|
||||
(*self).into_property()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -115,13 +138,13 @@ prop_signal_type!(MaybeSignal<T>);
|
||||
prop_signal_type_optional!(MaybeProp<T>);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::borrow::Cow;
|
||||
use leptos_reactive::Oco;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn property_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
value: Property,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user