Compare commits

..

23 Commits

Author SHA1 Message Date
Greg Johnston
8278cbbf4e fix: don't re-mount identical child in DynChild 2023-06-19 09:14:45 -04:00
Joseph Cruz
3c6748b30d build(examples): generate workspace members variable (#1201)
* build(examples): add gen members task

* build(examples): include members variable
2023-06-17 18:19:01 -04:00
Cherry
24945f67bf docs: add note to docs on how to fix failing builds which rebuild-std (#1200)
Fixes #1199
2023-06-17 16:51:57 -04:00
Joseph Cruz
edddab1e51 ci(examples): automatically keep the list of example projects current (#1198) 2023-06-17 16:51:31 -04:00
Greg Johnston
acfc86d2a4 fix: SVG <use> in SSR (#1203) 2023-06-17 16:47:39 -04:00
Greg Johnston
651868dec9 fix: animations on multiple back navigations (closes #1088) (#1204) 2023-06-17 16:47:19 -04:00
Joseph Cruz
18bc03e660 ci(examples): split check example and improve workflows (#1191) 2023-06-15 21:44:37 -04:00
jquesada2016
5f0013e482 fix: reorder <For /> apply_diff steps (#1196) 2023-06-15 20:37:17 -04:00
martin frances
10c0a2de65 chore: cleared clippy warnings (#1190)
The change in indentation makes the PR hard to review

so I will discuss the change in conversational language

Two "if"'s checks were merged into one "if"

this

-        if let Some(expr) = node.value() {
-            if let syn::Expr::Tuple(tuple) = expr {

becomes

+        if let Some(Tuple(tuple)) = node.value() {
2023-06-15 20:11:50 -04:00
Karim Lalani
b24be2566d docs: renamed function names in 13-actions chapter of book to reduce confusion (#1175) 2023-06-15 20:09:46 -04:00
Greg Johnston
77439b5db5 fix: setting set_pending now that <Transition/> body doesn't re-render (#1193) 2023-06-15 20:09:22 -04:00
Greg Johnston
23594a43ea fix: allow FnOnce extractors (#1192) 2023-06-15 20:09:13 -04:00
hchockarprasad
601db7aa86 fix: handle nested data in serde_qs deserialization correctly (#1183) 2023-06-15 10:15:10 -04:00
Joseph Cruz
d15ba11104 fix(examples/js-framework-benchmark): error: cannot find macro template in this scope (#1182) (#1189) 2023-06-15 08:19:38 -04:00
Joseph Cruz
d45d92433f ci(examples): include all example projects (#1188) 2023-06-14 15:16:14 -04:00
jquesada2016
97127a90c6 fix: new <For/> bug when clearing which ignores further additions (#1181) 2023-06-14 13:56:56 -04:00
martin frances
55bb63edea chore: updated cached 0.43.0 to 0.44.0 (#1187) 2023-06-14 11:07:24 -04:00
martin frances
15a4e54435 chore: criterion was outdated version 0.4.0 becomes 0.5.1. (#1184) 2023-06-14 11:06:50 -04:00
Joseph Cruz
3a522aef5d ci(examples): split jobs and verify changed examples (#1155) 2023-06-13 21:29:54 -04:00
Greg Johnston
a98885a123 fix: <ErrorBoundary/> IDs with new hydration key system (#1180) 2023-06-13 18:38:23 -04:00
Greg Johnston
2b7923261b docs: fix failing doctests from server fn docs (#1179) 2023-06-13 17:49:16 -04:00
Greg Johnston
b043f829a6 docs: clarify available server fn encodings (#1178) 2023-06-13 16:01:45 -04:00
jquesada2016
f415f7b146 fix: removes in new <For/> causing panics in some circumstances (#1173) 2023-06-13 15:43:02 -04:00
40 changed files with 532 additions and 320 deletions

View File

@@ -1,46 +1,46 @@
name: Check examples
name: Check Examples
on:
push:
branches: [main]
branches:
- main
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
branches:
- main
jobs:
test:
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
setup:
name: Get Examples
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Install JQ Tool
uses: mbround18/install-jq@v1
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v examples/README.md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all examples
run: cargo make check-examples
matrix-job:
name: Check
needs: [setup]
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-example-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"

View File

@@ -1,17 +1,21 @@
name: Verify Examples
name: Run Example Task
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_call:
inputs:
directory:
required: true
type: string
cargo_make_task:
required: true
type: string
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:
name: Verify examples ${{ matrix.os }} (using rustc ${{ matrix.rust }}
name: Run ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
@@ -21,6 +25,7 @@ jobs:
- ubuntu-latest
steps:
# Setup environment
- uses: actions/checkout@v3
- name: Setup Rust
@@ -63,7 +68,6 @@ jobs:
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
@@ -75,5 +79,12 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Run verify-flow on all web examples
run: cargo make --profile=github-actions verify-examples
# Verify project
- name: ${{ inputs.cargo_make_task }}
run: |
if [ "${{ inputs.directory }}" = "INTERNAL" ]; then
echo No verification required
else
cd ${{ inputs.directory }}
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}
fi

View File

@@ -0,0 +1,47 @@
name: Verify All Examples
on:
workflow_dispatch:
push:
tags:
- v*
schedule:
# Run once a day at 3:00 AM EST
- cron: "0 8 * * *"
jobs:
setup:
name: Get Examples
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install JQ Tool
uses: mbround18/install-jq@v1
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v examples/README.md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
matrix-job:
name: Verify
needs: [setup]
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-example-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "verify-flow"

View File

@@ -0,0 +1,71 @@
name: Verify Changed Examples
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
setup:
name: Get Changes
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get all example files that changed
id: changed-files
uses: tj-actions/changed-files@v36
with:
files: |
examples
- name: List all example files that changed
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
- name: Get example project directories that changed
id: changed-dirs
uses: tj-actions/changed-files@v36
with:
dir_names: true
dir_names_max_depth: "2"
files: |
examples
!examples/cargo-make
!examples/gtk
!examples/Makefile.toml
!examples/README.md
json: true
quotepath: false
- name: List example project directories that changed
run: echo '${{ steps.changed-dirs.outputs.all_changed_files }}'
- name: Set Matrix
id: set-matrix
run: |
if [ ${{ steps.changed-files.outputs.any_changed }} == 'true' ]; then
# Create matrix with changed directories
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
else
# Create matrix with one item to prevent an empty vector error
echo "matrix={\"directory\":[\"INTERNAL\"]}" >> "$GITHUB_OUTPUT"
fi
matrix-job:
name: Verify
needs: [setup]
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-example-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "verify-flow"

View File

@@ -45,6 +45,8 @@ dependencies = [
{ name = "check", path = "examples/fetch" },
{ name = "check", path = "examples/hackernews" },
{ name = "check", path = "examples/hackernews_axum" },
{ name = "check", path = "examples/js-framework-benchmark" },
{ name = "check", path = "examples/leptos-tailwind-axum" },
{ name = "check", path = "examples/login_with_token_csr_only" },
{ name = "check", path = "examples/parent_child" },
{ name = "check", path = "examples/router" },
@@ -54,6 +56,7 @@ dependencies = [
{ name = "check", path = "examples/ssr_modes_axum" },
{ name = "check", path = "examples/tailwind" },
{ name = "check", path = "examples/tailwind_csr_trunk" },
{ name = "check", path = "examples/timer" },
{ name = "check", path = "examples/todo_app_sqlite" },
{ name = "check", path = "examples/todo_app_sqlite_axum" },
{ name = "check", path = "examples/todo_app_sqlite_viz" },
@@ -102,6 +105,7 @@ args = ["make", "test-unit-and-web"]
[tasks.verify-examples]
description = "Run all quality checks and tests for examples"
env = { CLEAN_AFTER_VERIFY = "true" }
cwd = "examples"
command = "cargo"
args = ["make", "verify-flow"]

View File

@@ -47,6 +47,12 @@ Note that if you're using this with SSR too, the same Cargo profile will be appl
target = "x86_64-unknown-linux-gnu" # or whatever
```
Also note that in some cases, the cfg feature `has_std` will not be set, which may cause build errors with some dependencies which check for `has_std`. You may fix any build errors due to this by adding:
```toml
[build]
rustflags = ["--cfg=has_std"]
```
And you'll need to add `panic = "abort"` to `[profile.release]` in `Cargo.toml`. Note that this applies the same `build-std` and panic settings to your server binary, which may not be desirable. Some further exploration is probably needed here.
5. One of the sources of binary size in WASM binaries can be `serde` serialization/deserialization code. Leptos uses `serde` by default to serialize and deserialize resources created with `create_resource`. You might try experimenting with the `miniserde` and `serde-lite` features, which allow you to use those crates for serialization and deserialization instead; each only implements a subset of `serde`s functionality, but typically optimizes for size over speed.

View File

@@ -11,7 +11,7 @@ Actions and resources seem similar, but they represent fundamentally different t
Say we have some `async` function we want to run.
```rust
async fn add_todo(new_title: &str) -> Uuid {
async fn add_todo_request(new_title: &str) -> Uuid {
/* do some stuff on the server to add a new todo */
}
```
@@ -41,16 +41,16 @@ async fn add_todo(new_title: &str) -> Uuid {
So in this case, all we need to do to create an action is
```rust
let add_todo = create_action(cx, |input: &String| {
let add_todo_action = create_action(cx, |input: &String| {
let input = input.to_owned();
async move { add_todo(&input).await }
async move { add_todo_request(&input).await }
});
```
Rather than calling `add_todo` directly, well call it with `.dispatch()`, as in
Rather than calling `add_todo_action` directly, well call it with `.dispatch()`, as in
```rust
add_todo.dispatch("Some value".to_string());
add_todo_action.dispatch("Some value".to_string());
```
You can do this from an event listener, a timeout, or anywhere; because `.dispatch()` isnt an `async` function, it can be called from a synchronous context.
@@ -58,9 +58,9 @@ You can do this from an event listener, a timeout, or anywhere; because `.dispat
Actions provide access to a few signals that synchronize between the asynchronous action youre calling and the synchronous reactive system:
```rust
let submitted = add_todo.input(); // RwSignal<Option<String>>
let pending = add_todo.pending(); // ReadSignal<bool>
let todo_id = add_todo.value(); // RwSignal<Option<Uuid>>
let submitted = add_todo_action.input(); // RwSignal<Option<String>>
let pending = add_todo_action.pending(); // ReadSignal<bool>
let todo_id = add_todo_action.value(); // RwSignal<Option<Uuid>>
```
This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed.
@@ -73,7 +73,7 @@ view! { cx,
on:submit=move |ev| {
ev.prevent_default(); // don't reload the page...
let input = input_ref.get().expect("input to exist");
add_todo.dispatch(input.value());
add_todo_action.dispatch(input.value());
}
>
<label>

View File

@@ -15,16 +15,33 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"fetch",
"hackernews",
"hackernews_axum",
"js-framework-benchmark",
"leptos-tailwind-axum",
"login_with_token_csr_only",
"parent_child",
"router",
"session_auth_axum",
"slots",
"ssr_modes",
"ssr_modes_axum",
"tailwind",
"tailwind_csr_trunk",
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
"todomvc",
]
[tasks.gen-members]
workspace = false
description = "Generate the list of workspace members"
script = '''
examples=$(ls |
grep -v README.md |
grep -v Makefile.toml |
grep -v cargo-make |
grep -v gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
'''

View File

@@ -6,7 +6,8 @@ description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.check-format]
args = ["fmt", "--", "--check", "--config-path", "../../"]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
[tasks.clean-cargo]
description = "Runs the cargo clean command."

View File

@@ -15,7 +15,11 @@ dependencies = ["test-flow", "test-e2e-flow"]
[tasks.pre-verify]
[tasks.post-verify]
dependencies = ["clean-all"]
dependencies = ["maybe-clean-all"]
[tasks.maybe-clean-all]
description = "Used to clean up locally after call to verify-examples"
condition = { env_true = ["CLEAN_AFTER_VERIFY"] }
[tasks.test-e2e-flow]
description = "Provides pre and post hooks for test-e2e"

View File

@@ -1,3 +1,7 @@
[tasks.test]
env = { RUN_CARGO_TEST = false }
condition = { env_true = ["RUN_CARGO_TEST"] }
[tasks.post-test]
dependencies = ["test-wasm"]

View File

@@ -1,6 +1,7 @@
use counter_without_macros::counter;
use leptos::*;
/// Show the counter
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -5,10 +5,13 @@ extend = [
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
args = ["+nightly", "build-all-features", "--target", "wasm32-unknown-unknown"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
args = ["+nightly", "check-all-features", "--target", "wasm32-unknown-unknown"]
install_crate = "cargo-all-features"
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features --target wasm32-unknown-unknown -- -D warnings" }

View File

@@ -0,0 +1,11 @@
extend = [{ path = "../cargo-make/main.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -20,26 +20,25 @@ pub fn App(cx: Scope) -> impl IntoView {
#[component]
fn Home(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, 0);
// thanks to https://tailwindcomponents.com/component/blue-buttons-example for the showcase layout
view! { cx,
<Title text="Leptos + Tailwindcss"/>
<main>
<div class="bg-gradient-to-tl from-blue-800 to-blue-500 text-white font-mono flex flex-col min-h-screen">
<div class="flex flex-row-reverse flex-wrap m-auto">
<button on:click=move |_| set_value.update(|value| *value += 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
"+"
</button>
<button class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-800 border-blue-900 text-white">
{value}
</button>
<button on:click=move |_| set_value.update(|value| *value -= 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
"-"
</button>
</div>
<main>
<div class="bg-gradient-to-tl from-blue-800 to-blue-500 text-white font-mono flex flex-col min-h-screen">
<div class="flex flex-row-reverse flex-wrap m-auto">
<button on:click=move |_| set_value.update(|value| *value += 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
"+"
</button>
<button class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-800 border-blue-900 text-white">
{value}
</button>
<button on:click=move |_| set_value.update(|value| *value -= 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
"-"
</button>
</div>
</main>
</div>
</main>
}
}

View File

@@ -1,10 +1,9 @@
use cfg_if::cfg_if;
use http::status::StatusCode;
use leptos::*;
use thiserror::Error;
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
use thiserror::Error;
#[derive(Clone, Debug, Error)]
pub enum AppError {

View File

@@ -4,13 +4,13 @@ async fn main() {
use axum::{extract::Extension, routing::post, Router};
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use leptos_tailwind::{app::*, fileserv::file_and_error_handler};
use log::info;
use leptos_tailwind::app::*;
use leptos_tailwind::fileserv::file_and_error_handler;
use std::sync::Arc;
simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging");
simple_logger::init_with_level(log::Level::Info)
.expect("couldn't initialize logging");
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
// For deployment these variables are:
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
@@ -24,7 +24,11 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
.leptos_routes(
leptos_options.clone(),
routes,
|cx| view! { cx, <App/> },
)
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));

View File

@@ -1 +1,4 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../../" }

View File

@@ -1 +1,4 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../../" }

View File

@@ -1 +1,4 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../../" }

View File

@@ -108,4 +108,4 @@ Many thanks to GreatGreg for putting together this guide. You can find the origi
## Playwright Testing
- Run `cargo make setup` to install dependencies
- Run `cargo leptos test` or `cargo leptos end-to-end` to run the tests
- Run `cargo leptos test` or `cargo leptos end-to-end` to run the test

View File

@@ -1105,19 +1105,19 @@ where
pub trait Extractor<T> {
type Future;
fn call(&self, args: T) -> Self::Future;
fn call(self, args: T) -> Self::Future;
}
macro_rules! factory_tuple ({ $($param:ident)* } => {
impl<Func, Fut, $($param,)*> Extractor<($($param,)*)> for Func
where
Func: Fn($($param),*) -> Fut + Clone + 'static,
Func: FnOnce($($param),*) -> Fut + Clone + 'static,
Fut: Future,
{
type Future = Fut;
#[inline]
#[allow(non_snake_case)]
fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future {
fn call(self, ($($param,)*): ($($param,)*)) -> Self::Future {
(self)($($param,)*)
}
}

View File

@@ -1337,19 +1337,19 @@ pub trait Extractor<T, U>
where
T: FromRequestParts<()>,
{
fn call(&self, args: T) -> Pin<Box<dyn Future<Output = U>>>;
fn call(self, args: T) -> Pin<Box<dyn Future<Output = U>>>;
}
macro_rules! factory_tuple ({ $($param:ident)* } => {
impl<Func, Fut, U, $($param,)*> Extractor<($($param,)*), U> for Func
where
$($param: FromRequestParts<()> + Send,)*
Func: Fn($($param),*) -> Fut + 'static,
Func: FnOnce($($param),*) -> Fut + 'static,
Fut: Future<Output = U> + 'static,
{
#[inline]
#[allow(non_snake_case)]
fn call(&self, ($($param,)*): ($($param,)*)) -> Pin<Box<dyn Future<Output = U>>> {
fn call(self, ($($param,)*): ($($param,)*)) -> Pin<Box<dyn Future<Output = U>>> {
Box::pin((self)($($param,)*))
}
}

View File

@@ -1,5 +1,5 @@
use crate::Children;
use leptos_dom::{Errors, IntoView};
use leptos_dom::{Errors, HydrationCtx, IntoView};
use leptos_macro::{component, view};
use leptos_reactive::{
create_rw_signal, provide_context, signal_prelude::*, RwSignal, Scope,
@@ -28,7 +28,7 @@ use leptos_reactive::{
/// }
/// # });
/// ```
#[component(transparent)]
#[component]
pub fn ErrorBoundary<F, IV>(
cx: Scope,
/// The components inside the tag which will get rendered
@@ -40,6 +40,7 @@ where
F: Fn(Scope, RwSignal<Errors>) -> IV + 'static,
IV: IntoView,
{
_ = HydrationCtx::next_component();
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
provide_context(cx, errors);

View File

@@ -1,6 +1,9 @@
use leptos_dom::{Fragment, IntoView, View};
use leptos_macro::component;
use leptos_reactive::{use_context, Scope, SignalSetter, SuspenseContext};
use leptos_reactive::{
create_isomorphic_effect, use_context, Scope, SignalGet, SignalSetter,
SuspenseContext,
};
use std::{
cell::{Cell, RefCell},
rc::Rc,
@@ -97,9 +100,6 @@ where
is_first_run(&first_run, &suspense_context);
first_run.set(is_first_run);
if let Some(set_pending) = &set_pending {
set_pending.set(true);
}
if let Some(prev_children) = &*prev_child.borrow() {
if is_first_run {
fallback().into_view(cx)
@@ -132,9 +132,12 @@ where
}
child_runs.set(child_runs.get() + 1);
if let Some(set_pending) = &set_pending {
set_pending.set(false);
}
let pending = suspense_context.pending_resources;
create_isomorphic_effect(cx, move |_| {
if let Some(set_pending) = set_pending {
set_pending.set(pending.get() > 0)
}
});
frag
}))
.build(),

View File

@@ -26,117 +26,118 @@ fn main() {
mount_to_body(view_fn);
}
// fn view_fn(cx: Scope) -> impl IntoView {
// view! { cx,
// <h2>"Passing Tests"</h2>
// <ul>
// /* These work! */
// <Test from=[1] to=[] />
// <Test from=[1, 2] to=[] />
// <Test from=[1, 2, 3] to=[] />
// <hr/>
// <Test from=[] to=[1] />
// <Test from=[1, 2] to=[1] />
// <Test from=[2, 1] to=[1] />
// <hr/>
// <Test from=[1, 2, 3] to=[1, 2] />
// <Test from=[2] to=[1, 2] />
// <Test from=[1] to=[1, 2] />
// <Test from=[] to=[1, 2, 3] />
// <Test from=[2] to=[1, 2, 3] />
// <Test from=[1] to=[1, 2, 3] />
// <Test from=[1, 3, 2] to=[1, 2, 3] />
// <Test from=[2, 1, 3] to=[1, 2, 3] />
// </ul>
// <h2>"Broken Tests"</h2>
// <ul>
// <Test from=[3] to=[1, 2, 3] />
// <Test from=[3, 1] to=[1, 2, 3] />
// <Test from=[3, 2, 1] to=[1, 2, 3] />
// <hr/>
// <Test from=[1, 4, 2, 3] to=[1, 2, 3, 4] />
// <hr/>
// <Test from=[1, 4, 3, 2, 5] to=[1, 2, 3, 4, 5] />
// <Test from=[4, 5, 3, 1, 2] to=[1, 2, 3, 4, 5] />
// </ul>
// }
// }
// #[component]
// fn Test<From, To>(cx: Scope, from: From, to: To) -> impl IntoView
// where
// From: IntoIterator<Item = usize>,
// To: IntoIterator<Item = usize>,
// {
// let from = from.into_iter().collect::<Vec<_>>();
// let to = to.into_iter().collect::<Vec<_>>();
// let (list, set_list) = create_signal(cx, from.clone());
// request_animation_frame({
// let to = to.clone();
// move || {
// set_list(to);
// }
// });
// view! { cx,
// <li>
// "from: [" {move ||
// from
// .iter()
// .map(ToString::to_string)
// .intersperse(", ".to_string())
// .collect::<String>()
// } "]"
// <br />
// "to: [" {move ||
// to
// .iter()
// .map(ToString::to_string)
// .intersperse(", ".to_string())
// .collect::<String>()
// } "]"
// <br />
// "result: ["
// <For
// each=list
// key=|i| *i
// view=|cx, i| {
// view! { cx, <span>{i} ", "</span> }
// }
// /> "]"
// /* <p>
// "Pre | "
// <For
// each=list
// key=|i| *i
// view=|cx, i| {
// view! { cx, <span>{i}</span> }
// }
// />
// " | Post"
// </p> */
// </li>
// }
// }
fn view_fn(cx: Scope) -> impl IntoView {
let (should_show_a, sett_should_show_a) = create_signal(cx, true);
let a = vec![1, 2, 3, 4];
let b = vec![1, 2, 3];
view! { cx,
<button on:click=move |_| sett_should_show_a.update(|show| *show = !*show)>"Toggle"</button>
<For
each={move || if should_show_a.get() {
a.clone()
} else {
b.clone()
}}
key=|i| *i
view=|cx, i| view! { cx, <h1>{i}</h1> }
/>
<h2>"Passing Tests"</h2>
<ul>
<Test from=[1] to=[]/>
<Test from=[1, 2] to=[3, 2] then=vec![2]/>
<Test from=[1, 2] to=[]/>
<Test from=[1, 2, 3] to=[]/>
<hr/>
<Test from=[] to=[1]/>
<Test from=[1, 2] to=[1]/>
<Test from=[2, 1] to=[1]/>
<hr/>
<Test from=[1, 2, 3] to=[1, 2]/>
<Test from=[2] to=[1, 2]/>
<Test from=[1] to=[1, 2]/>
<Test from=[] to=[1, 2, 3]/>
<Test from=[2] to=[1, 2, 3]/>
<Test from=[1] to=[1, 2, 3]/>
<Test from=[1, 3, 2] to=[1, 2, 3]/>
<Test from=[2, 1, 3] to=[1, 2, 3]/>
<Test from=[3] to=[1, 2, 3]/>
<Test from=[3, 1] to=[1, 2, 3]/>
<Test from=[3, 2, 1] to=[1, 2, 3]/>
<hr/>
<Test from=[1, 4, 2, 3] to=[1, 2, 3, 4]/>
<hr/>
<Test from=[1, 4, 3, 2, 5] to=[1, 2, 3, 4, 5]/>
<Test from=[4, 5, 3, 1, 2] to=[1, 2, 3, 4, 5]/>
</ul>
}
}
#[component]
fn Test<From, To>(
cx: Scope,
from: From,
to: To,
#[prop(optional)] then: Option<Vec<usize>>,
) -> impl IntoView
where
From: IntoIterator<Item = usize>,
To: IntoIterator<Item = usize>,
{
let from = from.into_iter().collect::<Vec<_>>();
let to = to.into_iter().collect::<Vec<_>>();
let (list, set_list) = create_signal(cx, from.clone());
request_animation_frame({
let to = to.clone();
let then = then.clone();
move || {
set_list(to);
if let Some(then) = then {
request_animation_frame({
move || {
set_list(then);
}
});
}
}
});
view! { cx,
<li>
"from: [" {move || {
from
.iter()
.map(ToString::to_string)
.intersperse(", ".to_string())
.collect::<String>()
}} "]" <br/> "to: [" {
let then = then.clone();
move || {
then
.clone()
.unwrap_or(to.iter().copied().collect())
.iter()
.map(ToString::to_string)
.intersperse(", ".to_string())
.collect::<String>()
}
} "]" <br/> "result: ["
<For
each=list
key=|i| *i
view=|cx, i| {
view! { cx, <span>{i} ", "</span> }
}
/> "]"
</li>
}
}
// fn view_fn(cx: Scope) -> impl IntoView {
// let (should_show_a, sett_should_show_a) = create_signal(cx, true);
// let a = vec![2];
// let b = vec![1, 2, 3];
// view! { cx,
// <button on:click=move |_| sett_should_show_a.update(|show| *show = !*show)>"Toggle"</button>
// <For
// each={move || if should_show_a.get() {
// a.clone()
// } else {
// b.clone()
// }}
// key=|i| *i
// view=|cx, i| view! { cx, <h1>{i}</h1> }
// />
// }
// }

View File

@@ -273,7 +273,8 @@ where
// I can imagine some edge case that the child changes while
// hydration is ongoing
if !HydrationCtx::is_hydrating() {
if !was_child_moved && child != new_child {
let same_child = child == new_child;
if !was_child_moved && !same_child {
// Remove the child
let start = child.get_opening_node();
let end = &closing;
@@ -308,10 +309,13 @@ where
}
// Mount the new child
mount_child(
MountKind::Before(&closing),
&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

View File

@@ -800,8 +800,11 @@ fn apply_diff<T, EF, V>(
// The order of cmds needs to be:
// 1. Clear
// 2. Removals
// 3. Remove holes left from removals
// 4. Moves + Add
// 3. Move out
// 4. Resize
// 5. Move in
// 6. Additions
// 7. Removes holes
if diff.clear {
if opening.previous_sibling().is_none()
&& closing.next_sibling().is_none()
@@ -818,32 +821,25 @@ fn apply_diff<T, EF, V>(
#[cfg(not(debug_assertions))]
parent.append_with_node_1(closing).unwrap();
} else {
range.set_start_before(opening).unwrap();
range.set_start_after(opening).unwrap();
range.set_end_before(closing).unwrap();
range.delete_contents().unwrap();
}
return;
children.clear();
if diff.added.is_empty() {
return;
}
}
for DiffOpRemove { at } in &diff.removed {
let item_to_remove = std::mem::take(&mut children[*at]).unwrap();
let item_to_remove = children[*at].take().unwrap();
item_to_remove.prepare_for_move();
}
// Now, remove the holes that might have been left from removing
// items
#[allow(unstable_name_collisions)]
children.drain_filter(|c| c.is_none());
// Resize children if needed
if let Some(added) = diff.added.len().checked_sub(diff.removed.len()) {
let target_size = children.len() + added;
children.resize_with(target_size, || None);
}
let (move_cmds, add_cmds) = unpack_moves(&diff);
let mut moved_children = move_cmds
@@ -859,6 +855,8 @@ fn apply_diff<T, EF, V>(
})
.collect::<Vec<_>>();
children.resize_with(children.len() + diff.added.len(), || None);
for (i, DiffOpMove { to, .. }) in move_cmds
.iter()
.enumerate()
@@ -906,23 +904,36 @@ fn apply_diff<T, EF, V>(
children[at] = Some(each_item);
}
#[allow(unstable_name_collisions)]
children.drain_filter(|c| c.is_none());
}
/// Unpacks adds and moves into a sequence of interleaved
/// add and move commands. Move commands will always return
/// with a `len == 1` and `is_dense = true`.
/// with a `len == 1`.
#[cfg(all(target_arch = "wasm32", feature = "web"))]
fn unpack_moves(diff: &Diff) -> (Vec<DiffOpMove>, Vec<DiffOpAdd>) {
let mut moves = Vec::with_capacity(diff.items_to_move);
let mut adds = Vec::with_capacity(diff.added.len());
let mut removes_iter = diff.removed.iter();
let mut adds_iter = diff.added.iter();
let mut moves_iter = diff.moved.iter();
let mut removes_next = removes_iter.next();
let mut adds_next = adds_iter.next();
let mut moves_next = moves_iter.next().copied();
for i in 0..diff.items_to_move + diff.added.len() {
for i in 0..diff.items_to_move + diff.added.len() + diff.removed.len() {
if let Some(DiffOpRemove { at, .. }) = removes_next {
if i == *at {
removes_next = removes_iter.next();
continue;
}
}
match (adds_next, &mut moves_next) {
(Some(add), Some(move_)) => {
if add.at == i {

View File

@@ -80,7 +80,7 @@ where
E: Error + Send + Sync + 'static,
{
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
let id = ErrorKey(HydrationCtx::peek().id.to_string().into());
let id = ErrorKey(HydrationCtx::peek().fragment.to_string().into());
let errors = use_context::<RwSignal<Errors>>(cx);
match self {
Ok(stuff) => {

View File

@@ -20,10 +20,9 @@ pub fn block_to_primitive_expression(block: &syn::Block) -> Option<&syn::Expr> {
return None;
}
match &block.stmts[0] {
syn::Stmt::Expr(e, None) => return Some(e),
_ => {}
syn::Stmt::Expr(e, None) => Some(e),
_ => None,
}
None
}
/// Converts simple literals to its string representation.

View File

@@ -847,7 +847,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]`
/// macro to specify an alternate encoding:
///
/// ```rust
/// ```rust,ignore
/// #[server(AddTodo, "/api", "Url")]
/// #[server(AddTodo, "/api", "GetJson")]
/// #[server(AddTodo, "/api", "Cbor")]

View File

@@ -10,7 +10,7 @@ use rstml::node::{
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement, NodeName,
};
use std::collections::HashMap;
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
use syn::{spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprPath, Lit};
#[derive(Clone, Copy)]
enum TagType {
@@ -427,14 +427,14 @@ fn element_to_tokens_ssr(
{#component}.into_view(#cx)
}));
} else {
let tag_name = node
.name()
.to_string()
.replace("svg::", "")
.replace("math::", "");
let tag_name = node.name().to_string();
let tag_name = tag_name
.trim_start_matches("svg::")
.trim_start_matches("math::")
.trim_end_matches('_');
let is_script_or_style = tag_name == "script" || tag_name == "style";
template.push('<');
template.push_str(&tag_name);
template.push_str(tag_name);
#[cfg(debug_assertions)]
stmts_for_ide.save_element_completion(node);
@@ -1964,41 +1964,39 @@ fn fancy_class_name<'a>(
// special case for complex class names:
// e.g., Tailwind `class=("mt-[calc(100vh_-_3rem)]", true)`
if name == "class" {
if let Some(expr) = node.value() {
if let syn::Expr::Tuple(tuple) = expr {
if tuple.elems.len() == 2 {
let span = node.key.span();
let class = quote_spanned! {
span => .class
};
let class_name = &tuple.elems[0];
let class_name = if let Expr::Lit(ExprLit {
lit: Lit::Str(s),
..
}) = class_name
{
s.value()
} else {
proc_macro_error::emit_error!(
class_name.span(),
"class name must be a string literal"
);
Default::default()
};
let value = &tuple.elems[1];
return Some((
quote! {
#class(#class_name, (#cx, #value))
},
class_name,
value,
));
if let Some(Tuple(tuple)) = node.value() {
if tuple.elems.len() == 2 {
let span = node.key.span();
let class = quote_spanned! {
span => .class
};
let class_name = &tuple.elems[0];
let class_name = if let Expr::Lit(ExprLit {
lit: Lit::Str(s),
..
}) = class_name
{
s.value()
} else {
proc_macro_error::emit_error!(
tuple.span(),
"class tuples must have two elements."
)
}
class_name.span(),
"class name must be a string literal"
);
Default::default()
};
let value = &tuple.elems[1];
return Some((
quote! {
#class(#class_name, (#cx, #value))
},
class_name,
value,
));
} else {
proc_macro_error::emit_error!(
tuple.span(),
"class tuples must have two elements."
)
}
}
}
@@ -2012,41 +2010,39 @@ fn fancy_style_name<'a>(
) -> Option<(TokenStream, String, &'a Expr)> {
// special case for complex dynamic style names:
if name == "style" {
if let Some(expr) = node.value() {
if let syn::Expr::Tuple(tuple) = expr {
if tuple.elems.len() == 2 {
let span = node.key.span();
let style = quote_spanned! {
span => .style
};
let style_name = &tuple.elems[0];
let style_name = if let Expr::Lit(ExprLit {
lit: Lit::Str(s),
..
}) = style_name
{
s.value()
} else {
proc_macro_error::emit_error!(
style_name.span(),
"style name must be a string literal"
);
Default::default()
};
let value = &tuple.elems[1];
return Some((
quote! {
#style(#style_name, (#cx, #value))
},
style_name,
value,
));
if let Some(Tuple(tuple)) = node.value() {
if tuple.elems.len() == 2 {
let span = node.key.span();
let style = quote_spanned! {
span => .style
};
let style_name = &tuple.elems[0];
let style_name = if let Expr::Lit(ExprLit {
lit: Lit::Str(s),
..
}) = style_name
{
s.value()
} else {
proc_macro_error::emit_error!(
tuple.span(),
"style tuples must have two elements."
)
}
style_name.span(),
"style name must be a string literal"
);
Default::default()
};
let value = &tuple.elems[1];
return Some((
quote! {
#style(#style_name, (#cx, #value))
},
style_name,
value,
));
} else {
proc_macro_error::emit_error!(
tuple.span(),
"style tuples must have two elements."
)
}
}
}

View File

@@ -45,7 +45,7 @@ indexmap = "1"
self_cell = "1.0.0"
[dev-dependencies]
criterion = { version = "0.4.0", features = ["html_reports"] }
criterion = { version = "0.5.1", features = ["html_reports"] }
reactive-signals = { version = "0.1.0-alpha.4", features = ["profile"] }
l021 = { package = "leptos", version = "0.2.1" }
sycamore = { version = "0.8", features = ["ssr"] }

View File

@@ -77,7 +77,7 @@
//! of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]`
//! macro to specify an alternate encoding:
//!
//! ```rust
//! ```rust,ignore
//! #[server(AddTodo, "/api", "Url")]
//! #[server(AddTodo, "/api", "GetJson")]
//! #[server(AddTodo, "/api", "Cbor")]
@@ -161,7 +161,7 @@ impl ServerFnTraitObj {
}
}
#[cfg(any(feature = "ssr"))]
#[cfg(feature = "ssr")]
inventory::collect!(ServerFnTraitObj);
#[allow(unused)]

View File

@@ -10,7 +10,7 @@ description = "Router for the Leptos web framework."
[dependencies]
leptos = { workspace = true }
cached = { version = "0.43.0", optional = true }
cached = { version = "0.44.0", optional = true }
cfg-if = "1"
common_macros = "0.1"
gloo-net = { version = "0.2", features = ["http"] }

View File

@@ -692,6 +692,6 @@ where
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
.unwrap_throw();
let data = data.to_string().as_string().unwrap_or_default();
serde_qs::from_str::<Self>(&data)
serde_qs::Config::new(5, false).deserialize_str::<Self>(&data)
}
}

View File

@@ -278,9 +278,12 @@ impl RouterContextInner {
let global_suspense =
expect_context::<GlobalSuspenseContext>(cx);
let path_stack = self.path_stack;
path_stack.update_value(|stack| {
stack.push(resolved_to.clone())
});
let is_navigating_back = self.is_back.get_untracked();
if !is_navigating_back {
path_stack.update_value(|stack| {
stack.push(resolved_to.clone())
});
}
let set_is_routing = use_context::<SetIsRouting>(cx);
if let Some(set_is_routing) = set_is_routing {

View File

@@ -64,7 +64,9 @@ impl History for BrowserIntegration {
let is_navigating_back = path_stack.with_value(|stack| {
stack.len() == 1
|| stack.get(stack.len() - 2) == Some(&change.value)
|| (stack.len() >= 2
&& stack.get(stack.len() - 2)
== Some(&change.value))
});
if is_navigating_back {
path_stack.update_value(|stack| {

View File

@@ -18,7 +18,7 @@ lazy_static::lazy_static! {
};
}
#[cfg(any(feature = "ssr"))]
#[cfg(feature = "ssr")]
inventory::collect!(DefaultServerFnTraitObj);
/// Attempts to find a server function registered at the given path.

View File

@@ -80,7 +80,7 @@
#[doc(hidden)]
pub use const_format;
// used by the macro
#[cfg(any(feature = "ssr"))]
#[cfg(feature = "ssr")]
#[doc(hidden)]
pub use inventory;
#[cfg(any(feature = "ssr", doc))]
@@ -375,7 +375,8 @@ where
// decode the args
let value = match Self::encoding() {
Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => {
serde_qs::from_bytes(data)
serde_qs::Config::new(5, false)
.deserialize_bytes(data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
Encoding::Cbor => ciborium::de::from_reader(data)