Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
67ccd37336 fix: remove browser-only SendWrapper items during either kind of Suspense (closes #2907) 2024-09-02 07:57:03 -04:00
279 changed files with 9459 additions and 16614 deletions

View File

@@ -1,21 +1,23 @@
name: CI Changed Examples
on:
push:
branches:
- main
- leptos_0.6
pull_request:
branches:
- main
- leptos_0.6
jobs:
get-example-changed:
uses: ./.github/workflows/get-example-changed.yml
get-matrix:
needs: [get-example-changed]
uses: ./.github/workflows/get-changed-examples-matrix.yml
with:
example_changed: ${{ fromJSON(needs.get-example-changed.outputs.example_changed) }}
test:
name: CI
needs: [get-example-changed, get-matrix]

View File

@@ -1,13 +1,13 @@
name: CI Examples
on:
push:
branches:
- main
- leptos_0.6
pull_request:
branches:
- main
- leptos_0.6
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml

View File

@@ -1,24 +1,27 @@
name: CI semver
on:
push:
branches:
- main
- leptos_0.6
pull_request:
branches:
- main
- leptos_0.6
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
test:
needs: [get-leptos-changed]
if: github.event.pull_request.labels[0].name == 'semver' # needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
name: Run semver check (nightly-2024-08-01)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Semver Checks
uses: obi1kenobi/cargo-semver-checks-action@v2
with:

View File

@@ -1,25 +1,50 @@
name: CI
on:
push:
branches:
- main
- leptos_0.6
pull_request:
branches:
- main
- leptos_0.6
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
get-leptos-matrix:
uses: ./.github/workflows/get-leptos-matrix.yml
test:
name: CI
needs: [get-leptos-changed, get-leptos-matrix]
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
strategy:
matrix: ${{ fromJSON(needs.get-leptos-matrix.outputs.matrix) }}
fail-fast: false
matrix:
directory:
[
any_error,
any_spawner,
const_str_slice_concat,
either_of,
hydration_context,
integrations/actix,
integrations/axum,
integrations/utils,
leptos,
leptos_config,
leptos_dom,
leptos_hot_reload,
leptos_macro,
leptos_server,
meta,
next_tuple,
oco,
or_poisoned,
reactive_graph,
router,
router_macro,
server_fn,
server_fn/server_fn_macro_default,
server_fn_macro,
]
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}

View File

@@ -1,10 +1,12 @@
name: Examples Changed Call
on:
workflow_call:
outputs:
example_changed:
description: "Example Changed"
value: ${{ jobs.get-example-changed.outputs.example_changed }}
jobs:
get-example-changed:
name: Get Example Changed
@@ -16,6 +18,7 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get example files that changed
id: changed-files
uses: tj-actions/changed-files@v45
@@ -23,10 +26,13 @@ jobs:
files: |
examples/**
!examples/cargo-make/**
!examples/gtk/**
!examples/Makefile.toml
!examples/*.md
- name: List example files that changed
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
- name: Set example_changed
id: set-example-changed
run: |

View File

@@ -1,34 +1,38 @@
name: Get Examples Matrix Call
on:
workflow_call:
outputs:
matrix:
description: "Matrix"
value: ${{ jobs.create.outputs.matrix }}
jobs:
create:
name: Create Examples Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
env:
# separate examples using "|" (vertical bar) char like "a|b|c".
# cargo-make should be excluded by default.
EXCLUDED_EXAMPLES: cargo-make
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install jq
run: sudo apt-get install jq
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls -1d examples/*/ |
grep -vE "($EXCLUDED_EXAMPLES)" |
sed 's/\/$//' |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v .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: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"

View File

@@ -1,10 +1,12 @@
name: Get Leptos Changed Call
on:
workflow_call:
outputs:
leptos_changed:
description: "Leptos Changed"
value: ${{ jobs.create.outputs.leptos_changed }}
jobs:
create:
name: Detect Source Change
@@ -16,19 +18,40 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v45
with:
files_ignore: |
.*/**/*
cargo-make/**/*
examples/**/*
projects/**/*
benchmarks/**/*
docs/**/*
files: |
any_error/**
any_spawner/**
const_str_slice_concat/**
either_of/**
hydration_context/**
integrations/actix/**
integrations/axum/**
integrations/utils/**
leptos/**
leptos_config/**
leptos_dom/**
leptos_hot_reload/**
leptos_macro/**
leptos_server/**
meta/**
next_tuple/**
oco/**
or_poisoned/**
reactive_graph/**
router/**
router_macro/**
server_fn/**
server_fn/server_fn_macro_default/**
server_fn_macro/**
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set leptos_changed
id: set-source-changed
run: |

View File

@@ -1,32 +0,0 @@
name: Get Leptos Matrix Call
on:
workflow_call:
outputs:
matrix:
description: "Matrix"
value: ${{ jobs.create.outputs.matrix }}
jobs:
create:
name: Create Leptos Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install jq
run: sudo apt-get install jq
- name: Set Matrix
id: set-matrix
run: |
crates=$(cargo metadata --no-deps --quiet --format-version 1 |
jq -r '.packages[] | select(.name != "workspace") | .manifest_path| rtrimstr("/Cargo.toml")' |
sed "s|$(pwd)/||" |
jq -R -s -c 'split("\n")[:-1]')
echo "Leptos Directories: $crates"
echo "matrix={\"directory\":$crates}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"
pwd
ls | sort -u

View File

@@ -1,4 +1,5 @@
name: Run Task
on:
workflow_call:
inputs:
@@ -11,53 +12,70 @@ on:
toolchain:
required: true
type: string
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
runs-on: ubuntu-latest
steps:
# Setup environment
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ inputs.toolchain }}
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Install binstall
uses: cargo-bins/cargo-binstall@main
- name: Install wasm-bindgen
run: cargo binstall wasm-bindgen-cli --no-confirm
- name: Install cargo-leptos
run: cargo binstall cargo-leptos --no-confirm
- name: Install Trunk
uses: jetli/trunk-action@v0.5.0
with:
version: "latest"
- name: Print Trunk Version
run: trunk --version
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Setup pnpm cache
with:
@@ -65,6 +83,7 @@ jobs:
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Maybe install chromedriver
run: |
project_makefile=${{inputs.directory}}/Makefile.toml
@@ -80,6 +99,7 @@ jobs:
else
echo chromedriver is not required
fi
- name: Maybe install playwright browser dependencies
run: |
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
@@ -93,16 +113,12 @@ jobs:
echo Playwright is not required
fi
done
- name: Install Deno
uses: denoland/setup-deno@v2
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Maybe install gtk-rs dependencies
run: |
if [ ! -z $(echo ${{inputs.directory}} | grep gtk) ]; then
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libgio2.0-cil-dev libgraphene-1.0-dev libcairo2-dev libpango1.0-dev libgtk-4-dev
fi
# Run Cargo Make Task
- name: ${{ inputs.cargo_make_task }}
run: |

View File

@@ -40,36 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.7.0-gamma3"
version = "0.7.0-beta4"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0-gamma3" }
throw_error = { path = "./any_error/", version = "0.2.0-beta4" }
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
either_of = { path = "./either_of/", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma3" }
leptos = { path = "./leptos", version = "0.7.0-gamma3" }
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma3" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma3" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma3" }
leptos_router = { path = "./router", version = "0.7.0-gamma3" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma3" }
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma3" }
leptos_meta = { path = "./meta", version = "0.7.0-gamma3" }
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma3" }
hydration_context = { path = "./hydration_context", version = "0.2.0-beta4" }
leptos = { path = "./leptos", version = "0.7.0-beta4" }
leptos_config = { path = "./leptos_config", version = "0.7.0-beta4" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta4" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta4" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta4" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta4" }
leptos_router = { path = "./router", version = "0.7.0-beta4" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta4" }
leptos_server = { path = "./leptos_server", version = "0.7.0-beta4" }
leptos_meta = { path = "./meta", version = "0.7.0-beta4" }
next_tuple = { path = "./next_tuple", version = "0.1.0-beta4" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma3" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma3" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma3" }
server_fn = { path = "./server_fn", version = "0.7.0-gamma3" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma3" }
tachys = { path = "./tachys", version = "0.1.0-gamma3" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta4" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta4" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta4" }
server_fn = { path = "./server_fn", version = "0.7.0-beta4" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta4" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta4" }
tachys = { path = "./tachys", version = "0.1.0-beta4" }
[profile.release]
codegen-units = 1

View File

@@ -1,6 +1,6 @@
[package]
name = "throw_error"
version = "0.2.0-gamma3"
version = "0.2.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -9,7 +9,6 @@ description = "Spawn asynchronous tasks in an executor-independent way."
edition.workspace = true
[dependencies]
async-executor = { version = "1.13.1", optional = true }
futures = "0.3.30"
glib = { version = "0.20.0", optional = true }
thiserror = "1.0"
@@ -20,14 +19,12 @@ tracing = { version = "0.1.40", optional = true }
wasm-bindgen-futures = { version = "0.4.42", optional = true }
[features]
async-executor = ["dep:async-executor"]
tracing = ["dep:tracing"]
tokio = ["dep:tokio"]
glib = ["dep:glib"]
wasm-bindgen = ["dep:wasm-bindgen-futures"]
futures-executor = ["futures/thread-pool", "futures/executor"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

View File

@@ -32,14 +32,11 @@
use std::{future::Future, pin::Pin, sync::OnceLock};
use thiserror::Error;
/// A future that has been pinned.
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
/// A future that has been pinned.
pub type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
pub(crate) type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
static SPAWN: OnceLock<fn(PinnedFuture<()>)> = OnceLock::new();
static SPAWN_LOCAL: OnceLock<fn(PinnedLocalFuture<()>)> = OnceLock::new();
static POLL_LOCAL: OnceLock<fn()> = OnceLock::new();
/// Errors that can occur when using the executor.
#[derive(Error, Debug)]
@@ -118,14 +115,6 @@ impl Executor {
});
_ = rx.await;
}
/// Polls the current async executor.
/// Not all async executors support polling, so this function may not do anything.
pub fn poll_local() {
if let Some(poller) = POLL_LOCAL.get() {
poller()
}
}
}
impl Executor {
@@ -204,15 +193,13 @@ impl Executor {
#[cfg_attr(docsrs, doc(cfg(feature = "futures-executor")))]
pub fn init_futures_executor() -> Result<(), ExecutorError> {
use futures::{
executor::{LocalPool, LocalSpawner, ThreadPool},
executor::{LocalPool, ThreadPool},
task::{LocalSpawnExt, SpawnExt},
};
use std::cell::RefCell;
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
thread_local! {
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
static LOCAL_POOL: LocalPool = LocalPool::new();
}
fn get_thread_pool() -> &'static ThreadPool {
@@ -231,97 +218,28 @@ impl Executor {
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
SPAWNER.with(|spawner| {
LOCAL_POOL.with(|pool| {
let spawner = pool.spawner();
spawner.spawn_local(fut).expect("failed to spawn future");
});
})
.map_err(|_| ExecutorError::AlreadySet)?;
POLL_LOCAL
.set(|| {
LOCAL_POOL.with(|pool| {
if let Ok(mut pool) = pool.try_borrow_mut() {
pool.run_until_stalled();
}
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
});
})
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
/// Globally sets the [`async_executor`] executor as the executor used to spawn tasks,
/// lazily creating a thread pool to spawn tasks into.
///
/// Returns `Err(_)` if an executor has already been set.
///
/// Requires the `async-executor` feature to be activated on this crate.
#[cfg(feature = "async-executor")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-executor")))]
pub fn init_async_executor() -> Result<(), ExecutorError> {
use async_executor::{Executor, LocalExecutor};
static THREAD_POOL: OnceLock<Executor> = OnceLock::new();
thread_local! {
static LOCAL_POOL: LocalExecutor<'static> = const { LocalExecutor::new() };
}
fn get_thread_pool() -> &'static Executor<'static> {
THREAD_POOL.get_or_init(Executor::new)
}
SPAWN
.set(|fut| {
get_thread_pool().spawn(fut).detach();
})
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
LOCAL_POOL.with(|pool| pool.spawn(fut).detach());
})
.map_err(|_| ExecutorError::AlreadySet)?;
POLL_LOCAL
.set(|| {
LOCAL_POOL.with(|pool| pool.try_tick());
})
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
/// Globally sets a custom executor as the executor used to spawn tasks.
///
/// Returns `Err(_)` if an executor has already been set.
pub fn init_custom_executor(
custom_executor: impl CustomExecutor + 'static,
) -> Result<(), ExecutorError> {
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
EXECUTOR
.set(Box::new(custom_executor))
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN
.set(|fut| {
EXECUTOR.get().unwrap().spawn(fut);
})
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| EXECUTOR.get().unwrap().spawn_local(fut))
.map_err(|_| ExecutorError::AlreadySet)?;
POLL_LOCAL
.set(|| EXECUTOR.get().unwrap().poll_local())
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
}
/// A trait for custom executors.
/// Custom executors can be used to integrate with any executor that supports spawning futures.
///
/// All methods can be called recursively.
pub trait CustomExecutor: Send + Sync {
/// Spawns a future, usually on a thread pool.
fn spawn(&self, fut: PinnedFuture<()>);
/// Spawns a local future. May require calling `poll_local` to make progress.
fn spawn_local(&self, fut: PinnedLocalFuture<()>);
/// Polls the executor, if it supports polling.
fn poll_local(&self);
#[cfg(test)]
mod tests {
#[cfg(feature = "futures-executor")]
#[test]
fn can_spawn_local_future() {
use crate::Executor;
use std::rc::Rc;
Executor::init_futures_executor().expect("couldn't set executor");
let rc = Rc::new(());
Executor::spawn_local(async {
_ = rc;
});
Executor::spawn(async {});
}
}

View File

@@ -1,55 +0,0 @@
#[cfg(feature = "futures-executor")]
use any_spawner::{CustomExecutor, Executor, PinnedFuture, PinnedLocalFuture};
#[cfg(feature = "futures-executor")]
#[test]
fn can_create_custom_executor() {
use futures::{
executor::{LocalPool, LocalSpawner},
task::LocalSpawnExt,
};
use std::{
cell::RefCell,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
thread_local! {
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
}
struct CustomFutureExecutor;
impl CustomExecutor for CustomFutureExecutor {
fn spawn(&self, _fut: PinnedFuture<()>) {
panic!("not supported in this test");
}
fn spawn_local(&self, fut: PinnedLocalFuture<()>) {
SPAWNER.with(|spawner| {
spawner.spawn_local(fut).expect("failed to spawn future");
});
}
fn poll_local(&self) {
LOCAL_POOL.with(|pool| {
if let Ok(mut pool) = pool.try_borrow_mut() {
pool.run_until_stalled();
}
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
});
}
}
Executor::init_custom_executor(CustomFutureExecutor)
.expect("couldn't set executor");
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
Executor::spawn_local(async move {
counter_clone.store(1, Ordering::Release);
});
Executor::poll_local();
assert_eq!(counter.load(Ordering::Acquire), 1);
}

View File

@@ -1,38 +0,0 @@
#[cfg(feature = "futures-executor")]
use any_spawner::Executor;
// All tests in this file use the same executor.
#[cfg(feature = "futures-executor")]
#[test]
fn can_spawn_local_future() {
use std::rc::Rc;
let _ = Executor::init_futures_executor();
let rc = Rc::new(());
Executor::spawn_local(async {
_ = rc;
});
Executor::spawn(async {});
}
#[cfg(feature = "futures-executor")]
#[test]
fn can_make_local_progress() {
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
let _ = Executor::init_futures_executor();
let counter = Arc::new(AtomicUsize::new(0));
Executor::spawn_local({
let counter = Arc::clone(&counter);
async move {
assert_eq!(counter.fetch_add(1, Ordering::AcqRel), 0);
Executor::spawn_local(async {
// Should not crash
});
}
});
Executor::poll_local();
assert_eq!(counter.load(Ordering::Acquire), 1);
}

View File

@@ -2,8 +2,7 @@
name = "benchmarks"
version = "0.1.0"
edition = "2021"
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
rust-version.workspace = true
[dependencies]
l0410 = { package = "leptos", version = "0.4.10", features = [

View File

@@ -18,7 +18,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
}
}
let rendered = view! {
let rendered = view! {
<main>
<h1>"Welcome to our benchmark page."</h1>
<p>"Here's some introductory text."</p>
@@ -58,7 +58,7 @@ fn tachys_ssr_bench(b: &mut Bencher) {
}
}
let rendered = view! {
let rendered = view! {
<main>
<h1>"Welcome to our benchmark page."</h1>
<p>"Here's some introductory text."</p>
@@ -92,13 +92,13 @@ fn tera_ssr_bench(b: &mut Bencher) {
{% endfor %}
</main>"#;
static LazyCell<TERA>: Tera = LazyLock::new(|| {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
});
lazy_static::lazy_static! {
static ref TERA: Tera = {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
};
}
#[derive(Serialize, Deserialize)]
struct Counter {

View File

@@ -55,7 +55,7 @@ static TEMPLATE: &str = r#"<main>
{% else %}
<li><a href="/">All</a></li>
{% endif %}
{% if mode_active %}
<li><a href="/active" class="selected">Active</a></li>
{% else %}
@@ -91,13 +91,13 @@ fn tera_todomvc_ssr(b: &mut Bencher) {
use serde::{Deserialize, Serialize};
use tera::*;
static LazyLock<TERA>: Tera = LazyLock( || {
lazy_static::lazy_static! {
static ref TERA: Tera = {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
});
};
}
#[derive(Serialize, Deserialize)]
struct Todo {
@@ -131,13 +131,13 @@ fn tera_todomvc_ssr_1000(b: &mut Bencher) {
use serde::{Deserialize, Serialize};
use tera::*;
static TERA: LazyLock<Tera> = LazyLock::new(|| {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
});
lazy_static::lazy_static! {
static ref TERA: Tera = {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
};
}
#[derive(Serialize, Deserialize)]
struct Todo {

View File

@@ -133,104 +133,3 @@ tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj => A, B, C, D, E, F
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N);
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
/// Matches over the first expression and returns an either ([`Either`], [`EitherOf3`], ... [`EitherOf6`])
/// composed of the values returned by the match arms.
///
/// The pattern syntax is exactly the same as found in a match arm.
///
/// # Examples
///
/// ```
/// # use either_of::*;
/// let either2 = either!(Some("hello"),
/// Some(s) => s.len(),
/// None => 0.0,
/// );
/// assert!(matches!(either2, Either::<usize, f64>::Left(5)));
///
/// let either3 = either!(Some("admin"),
/// Some("admin") => "hello admin",
/// Some(_) => 'x',
/// _ => 0,
/// );
/// assert!(matches!(either3, EitherOf3::<&str, char, i32>::A("hello admin")));
/// ```
#[macro_export]
macro_rules! either {
($match:expr, $left_pattern:pat => $left_expression:expr, $right_pattern:pat => $right_expression:expr,) => {
match $match {
$left_pattern => $crate::Either::Left($left_expression),
$right_pattern => $crate::Either::Right($right_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf3::A($a_expression),
$b_pattern => $crate::EitherOf3::B($b_expression),
$c_pattern => $crate::EitherOf3::C($c_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf4::A($a_expression),
$b_pattern => $crate::EitherOf4::B($b_expression),
$c_pattern => $crate::EitherOf4::C($c_expression),
$d_pattern => $crate::EitherOf4::D($d_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf5::A($a_expression),
$b_pattern => $crate::EitherOf5::B($b_expression),
$c_pattern => $crate::EitherOf5::C($c_expression),
$d_pattern => $crate::EitherOf5::D($d_expression),
$e_pattern => $crate::EitherOf5::E($e_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf6::A($a_expression),
$b_pattern => $crate::EitherOf6::B($b_expression),
$c_pattern => $crate::EitherOf6::C($c_expression),
$d_pattern => $crate::EitherOf6::D($d_expression),
$e_pattern => $crate::EitherOf6::E($e_expression),
$f_pattern => $crate::EitherOf6::F($f_expression),
}
}; // if you need more eithers feel free to open a PR ;-)
}
// compile time test
#[test]
fn either_macro() {
let _: Either<&str, f64> = either!(12,
12 => "12",
_ => 0.0,
);
let _: EitherOf3<&str, f64, i32> = either!(12,
12 => "12",
13 => 0.0,
_ => 12,
);
let _: EitherOf4<&str, f64, char, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
_ => 12,
);
let _: EitherOf5<&str, f64, char, f32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
_ => 12,
);
let _: EitherOf6<&str, f64, char, f32, u8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
_ => 12,
);
}

View File

@@ -1,111 +0,0 @@
[package]
name = "axum_js_ssr"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7.5", optional = true }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
gloo-utils = "0.2.0"
html-escape = "0.2.13"
http-body-util = { version = "0.1.0", optional = true }
js-sys = { version = "0.3.69", optional = true }
leptos = { path = "../../leptos", features = ["tracing"] }
leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
wasm-bindgen = "0.2.92"
web-sys = { version = "0.3.69", features = [ "AddEventListenerOptions", "Document", "Element", "Event", "EventListener", "EventTarget", "Performance", "Window" ], optional = true }
[features]
hydrate = [
"leptos/hydrate",
"dep:js-sys",
"dep:web-sys",
]
ssr = [
"dep:axum",
"dep:http-body-util",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"dep:leptos_axum",
"leptos_router/ssr",
]
[profile.release]
panic = "abort"
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "axum_js_ssr"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
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/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = "assets"
# 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"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
lib-profile-release = "wasm-release"

View File

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

View File

@@ -1,8 +0,0 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[env]
CLIENT_PROCESS_NAME = "axum_js_ssr"

View File

@@ -1,10 +0,0 @@
# Leptos Axum JS SSR Example
This example shows the various ways that JavaScript may be included into
a Leptos application. The intent is to demonstrate how this may be done
and how it may cause the application to fail in an unexpected manner if
done incorrectly.
## Quick Start
Run `cargo leptos watch` to run this example.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,29 +0,0 @@
BSD 3-Clause License
Copyright (c) 2006, Ivan Sagalaev.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,47 +0,0 @@
# Highlight.js CDN Assets
**Note: this contains only a subset of files from the full package from NPM.**
[![install size](https://packagephobia.now.sh/badge?p=highlight.js)](https://packagephobia.now.sh/result?p=highlight.js)
**This package contains only the CDN build assets of highlight.js.**
This may be what you want if you'd like to install the pre-built distributable highlight.js client-side assets via NPM. If you're wanting to use highlight.js mainly on the server-side you likely want the [highlight.js][1] package instead.
To access these files via CDN:<br>
https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/
**If you just want a single .js file with the common languages built-in:
<https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/highlight.min.js>**
---
## Highlight.js
Highlight.js is a syntax highlighter written in JavaScript. It works in
the browser as well as on the server. It works with pretty much any
markup, doesnt depend on any framework, and has automatic language
detection.
If you'd like to read the full README:<br>
<https://github.com/highlightjs/highlight.js/blob/main/README.md>
## License
Highlight.js is released under the BSD License. See [LICENSE][7] file
for details.
## Links
The official site for the library is at <https://highlightjs.org/>.
The Github project may be found at: <https://github.com/highlightjs/highlight.js>
Further in-depth documentation for the API and other topics is at
<http://highlightjs.readthedocs.io/>.
A list of the Core Team and contributors can be found in the [CONTRIBUTORS.md][8] file.
[1]: https://www.npmjs.com/package/highlight.js
[7]: https://github.com/highlightjs/highlight.js/blob/main/LICENSE
[8]: https://github.com/highlightjs/highlight.js/blob/main/CONTRIBUTORS.md

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,93 +0,0 @@
{
"name": "@highlightjs/cdn-assets",
"description": "Syntax highlighting with language autodetection. (pre-compiled CDN assets)",
"keywords": [
"highlight",
"syntax"
],
"homepage": "https://highlightjs.org/",
"version": "11.10.0",
"author": "Josh Goebel <hello@joshgoebel.com>",
"contributors": [
"Josh Goebel <hello@joshgoebel.com>",
"Egor Rogov <e.rogov@postgrespro.ru>",
"Vladimir Jimenez <me@allejo.io>",
"Ivan Sagalaev <maniac@softwaremaniacs.org>",
"Jeremy Hull <sourdrums@gmail.com>",
"Oleg Efimov <efimovov@gmail.com>",
"Gidi Meir Morris <gidi@gidi.io>",
"Jan T. Sott <git@idleberg.com>",
"Li Xuanji <xuanji@gmail.com>",
"Marcos Cáceres <marcos@marcosc.com>",
"Sang Dang <sang.dang@polku.io>"
],
"bugs": {
"url": "https://github.com/highlightjs/highlight.js/issues"
},
"license": "BSD-3-Clause",
"repository": {
"type": "git",
"url": "git://github.com/highlightjs/highlight.js.git"
},
"sideEffects": [
"./es/common.js",
"./lib/common.js",
"*.css",
"*.scss"
],
"scripts": {
"mocha": "mocha",
"lint": "eslint src/*.js src/lib/*.js demo/*.js tools/**/*.js --ignore-pattern vendor",
"lint-languages": "eslint --no-eslintrc -c .eslintrc.lang.js src/languages/**/*.js",
"build_and_test": "npm run build && npm run test",
"build_and_test_browser": "npm run build-browser && npm run test-browser",
"build": "node ./tools/build.js -t node",
"build-cdn": "node ./tools/build.js -t cdn",
"build-browser": "node ./tools/build.js -t browser :common",
"devtool": "npx http-server",
"test": "mocha test",
"test-markup": "mocha test/markup",
"test-detect": "mocha test/detect",
"test-browser": "mocha test/browser",
"test-parser": "mocha test/parser"
},
"engines": {
"node": ">=12.0.0"
},
"devDependencies": {
"@colors/colors": "^1.6.0",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-json": "^6.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/mocha": "^10.0.2",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"clean-css": "^5.3.2",
"cli-table": "^0.3.1",
"commander": "^12.1.0",
"css": "^3.0.0",
"css-color-names": "^1.0.1",
"deep-freeze-es6": "^3.0.2",
"del": "^7.1.0",
"dependency-resolver": "^2.0.1",
"eslint": "^8.57.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"glob": "^8.1.0",
"glob-promise": "^6.0.5",
"handlebars": "^4.7.8",
"http-server": "^14.1.1",
"jsdom": "^24.1.0",
"lodash": "^4.17.20",
"mocha": "^10.2.0",
"refa": "^0.4.1",
"rollup": "^4.0.2",
"should": "^13.2.3",
"terser": "^5.21.0",
"tiny-worker": "^2.3.0",
"typescript": "^5.2.2",
"wcag-contrast": "^3.0.0"
}
}

View File

@@ -1,10 +0,0 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub Dark
Description: Dark theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-dark
Current colors taken from GitHub's CSS
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}

View File

@@ -1,10 +0,0 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub
Description: Light theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}

View File

@@ -1,6 +0,0 @@
{
"name": "axum_js_ssr",
"dependencies": {
"@highlightjs/cdn-assets": "^11.10.0"
}
}

View File

@@ -1,2 +0,0 @@
[toolchain]
channel = "stable" # test change

View File

@@ -1,8 +0,0 @@
use leptos::{prelude::ServerFnError, server};
#[server]
pub async fn fetch_code() -> Result<String, ServerFnError> {
// emulate loading of code from a database/version control/etc
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
Ok(crate::consts::CH05_02A.to_string())
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +0,0 @@
// Example programs from the Rust Programming Language Book
pub const CH03_05A: &str = r#"fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
"#;
// For some reason, swapping the code examples "fixes" example 6. It
// might have something to do with the lower complexity of highlighting
// a shorter example. Anyway, including extra newlines for the shorter
// example to match with the longer in order to avoid reflowing the
// table during the async resource loading for CSR.
pub const CH05_02A: &str = r#"fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
"#;
pub const LEPTOS_HYDRATED: &str = "_leptos_hydrated";

View File

@@ -1,59 +0,0 @@
#[cfg(not(feature = "ssr"))]
mod csr {
use gloo_utils::format::JsValueSerdeExt;
use js_sys::{
Object,
Reflect::{get, set},
};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
#[wasm_bindgen(
module = "/node_modules/@highlightjs/cdn-assets/es/highlight.min.js"
)]
extern "C" {
type HighlightOptions;
#[wasm_bindgen(catch, js_namespace = default, js_name = highlight)]
fn highlight_lang(
code: String,
options: Object,
) -> Result<Object, JsValue>;
#[wasm_bindgen(js_namespace = default, js_name = highlightAll)]
pub fn highlight_all();
}
// Keeping the `ignoreIllegals` argument out of the default case, and since there is no optional arguments
// in Rust, this will have to be provided in a separate function (e.g. `highlight_ignore_illegals`), much
// like how `web_sys` does it for the browser APIs. For simplicity, only the highlighted HTML code is
// returned on success, and None on error.
pub fn highlight(code: String, lang: String) -> Option<String> {
let options = js_sys::Object::new();
set(&options, &"language".into(), &lang.into())
.expect("failed to assign lang to options");
highlight_lang(code, options)
.map(|result| {
let value = get(&result, &"value".into())
.expect("HighlightResult failed to contain the value key");
value.into_serde().expect("Value should have been a string")
})
.ok()
}
}
#[cfg(feature = "ssr")]
mod ssr {
// noop under ssr
pub fn highlight_all() {}
// TODO see if there is a Rust-based solution that will enable isomorphic rendering for this feature.
// the current (disabled) implementation simply calls html_escape.
// pub fn highlight(code: String, _lang: String) -> Option<String> {
// Some(html_escape::encode_text(&code).into_owned())
// }
}
#[cfg(not(feature = "ssr"))]
pub use csr::*;
#[cfg(feature = "ssr")]
pub use ssr::*;

View File

@@ -1,51 +0,0 @@
pub mod api;
pub mod app;
pub mod consts;
pub mod hljs;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
use consts::LEPTOS_HYDRATED;
use std::panic;
panic::set_hook(Box::new(|info| {
// this custom hook will call out to show the usual error log at
// the console while also attempt to update the UI to indicate
// a restart of the application is required to continue.
console_error_panic_hook::hook(info);
let window = leptos::prelude::window();
if !matches!(
js_sys::Reflect::get(&window, &wasm_bindgen::JsValue::from_str(LEPTOS_HYDRATED)),
Ok(t) if t == true
) {
let document = leptos::prelude::document();
let _ = document.query_selector("#reset").map(|el| {
el.map(|el| {
el.set_class_name("panicked");
})
});
let _ = document.query_selector("#notice").map(|el| {
el.map(|el| {
el.set_class_name("panicked");
})
});
}
}));
leptos::mount::hydrate_body(App);
let window = leptos::prelude::window();
js_sys::Reflect::set(
&window,
&wasm_bindgen::JsValue::from_str(LEPTOS_HYDRATED),
&wasm_bindgen::JsValue::TRUE,
)
.expect("error setting hydrated status");
let event = web_sys::Event::new(LEPTOS_HYDRATED)
.expect("error creating hydrated event");
let document = leptos::prelude::document();
document
.dispatch_event(&event)
.expect("error dispatching hydrated event");
leptos::logging::log!("dispatched hydrated event");
}

View File

@@ -1,152 +0,0 @@
#[cfg(feature = "ssr")]
mod latency {
use std::sync::{Mutex, OnceLock};
pub static LATENCY: OnceLock<
Mutex<std::iter::Cycle<std::slice::Iter<'_, u64>>>,
> = OnceLock::new();
pub static ES_LATENCY: OnceLock<
Mutex<std::iter::Cycle<std::slice::Iter<'_, u64>>>,
> = OnceLock::new();
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{
body::Body,
extract::Request,
http::{
header::{self, HeaderValue},
StatusCode,
},
middleware::{self, Next},
response::{IntoResponse, Response},
routing::get,
Router,
};
use axum_js_ssr::app::*;
use http_body_util::BodyExt;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
latency::LATENCY.get_or_init(|| [0, 4, 40, 400].iter().cycle().into());
latency::ES_LATENCY.get_or_init(|| [0].iter().cycle().into());
// Having the ES_LATENCY (a cycle of latency for the loading of the es
// module) in an identical cycle as LATENCY (for the standard version)
// adversely influences the intended demo, as this ultimately delays
// hydration when set too high which can cause panic under every case.
// If you want to test the effects of the delay just modify the list of
// values for the desired cycle of delays.
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
async fn highlight_js() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "text/javascript")],
include_str!(
"../node_modules/@highlightjs/cdn-assets/highlight.min.js"
),
)
}
async fn latency_for_highlight_js(
req: Request,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let uri_parts = &mut req.uri().path().rsplit('/');
let is_highlightjs = uri_parts.next() == Some("highlight.min.js");
let es = uri_parts.next() == Some("es");
let module_type = if es { "es module " } else { "standard " };
let res = next.run(req).await;
if is_highlightjs {
// additional processing if the filename is the test subject
let (mut parts, body) = res.into_parts();
let bytes = body
.collect()
.await
.map_err(|err| {
(
StatusCode::BAD_REQUEST,
format!("error reading body: {err}"),
)
})?
.to_bytes();
let latency = if es {
&latency::ES_LATENCY
} else {
&latency::LATENCY
};
let delay = match latency
.get()
.expect("latency cycle wasn't set up")
.try_lock()
{
Ok(ref mut mutex) => {
*mutex.next().expect("cycle always has next")
}
Err(_) => 0,
};
// inject the logging of the delay used into the target script
log!(
"loading {module_type}highlight.min.js with latency of \
{delay} ms"
);
let js_log = format!(
"\nconsole.log('loaded {module_type}highlight.js with a \
minimum latency of {delay} ms');"
);
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
let bytes = [bytes, js_log.into()].concat();
let length = bytes.len();
let body = Body::from(bytes);
// Provide the bare minimum set of headers to avoid browser cache.
parts.headers = header::HeaderMap::from_iter(
[
(
header::CONTENT_TYPE,
HeaderValue::from_static("text/javascript"),
),
(header::CONTENT_LENGTH, HeaderValue::from(length)),
]
.into_iter(),
);
Ok(Response::from_parts(parts, body))
} else {
Ok(res)
}
}
let app = Router::new()
.route("/highlight.min.js", get(highlight_js))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.layer(middleware::from_fn(latency_for_highlight_js))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View File

@@ -1,171 +0,0 @@
html, body {
margin: 0;
padding: 0;
font-family: sans-serif;
height: 100vh;
overflow: hidden;
}
body {
display: flex;
flex-flow: row nowrap;
}
nav {
min-width: 17em;
height: 100vh;
counter-reset: example-counter 0;
list-style-type: none;
list-style-position: outside;
overflow: auto;
}
nav a {
display: block;
padding: 0.5em 2em;
text-decoration: none;
}
nav a small {
display: block;
}
nav a.example::before {
counter-reset: subexample-counter 0;
counter-increment: example-counter 1;
content: counter(example-counter) ". ";
}
nav a.subexample::before {
counter-increment: subexample-counter 1;
content: counter(example-counter) "." counter(subexample-counter) " ";
}
div#notice {
display: none;
}
main div#notice.panicked {
position: sticky;
top: 0;
padding: 0.5em 2em;
display: block;
}
main {
width: 100%;
overflow: auto;
}
main article {
max-width: 60em;
margin: 0 1em;
padding: 0 1em;
}
main p, main li {
line-height: 1.3em;
}
main li pre code, main div pre code {
display: block;
line-height: normal;
}
main ol, main ul {
padding-left: 2em;
}
h2>code, p>code, li>code {
border-radius: 3px;
padding: 2px;
}
li pre code, div pre code {
margin: 0 !important;
padding: 0 !important;
}
#code-demo {
overflow-x: auto;
}
#code-demo table {
width: 50em;
margin: auto;
}
#code-demo table td {
vertical-align: top;
}
#code-demo table code {
display: block;
padding: 1em;
}
@media (prefers-color-scheme: light) {
nav {
background: #f7f7f7;
}
nav a {
color: #000;
}
nav a[aria-current="page"] {
background-color: #e0e0e0;
}
nav a:hover, h2>code, p>code, li>code {
background-color: #e7e7e7;
}
nav a.panicked, main div#notice.panicked {
background: #fdd;
}
main div#notice.panicked a {
color: #000;
}
nav a.section {
border-bottom: 1px solid #777;
}
}
@media (prefers-color-scheme: dark) {
nav {
background: #080808;
}
nav a {
color: #fff;
}
nav a[aria-current="page"] {
background-color: #3f3f3f;
}
nav a:hover, h2>code, p>code, li>code {
background-color: #383838;
}
nav a.panicked, main div#notice.panicked {
background: #733;
}
main div#notice.panicked a {
color: #fff;
}
nav a.section {
border-bottom: 1px solid #888;
}
}
// Just include the raw style as-is because I can't find a quick and easy way to import them just for the
// appropriate media type...
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}
@media (prefers-color-scheme: light){.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}}
@media (prefers-color-scheme: dark){.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}}

View File

@@ -1,7 +1,7 @@
use counter::*;
use leptos::mount::mount_to;
use leptos::prelude::*;
use leptos::task::tick;
use leptos::spawn::tick;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -2,8 +2,6 @@
name = "counter_isomorphic"
version = "0.1.0"
edition = "2021"
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
[lib]
crate-type = ["cdylib", "rlib"]
@@ -19,6 +17,7 @@ broadcaster = "1.0"
console_log = "1.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
lazy_static = "1.5"
leptos = { path = "../../leptos" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
@@ -47,13 +46,13 @@ denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "counter_isomorphic"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
# When NOT using cargo-leptos this must be updated to "." or the counters will not work. The above warning still applies if you do switch to cargo-leptos later.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
# 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 = "src/styles/tailwind.css"

View File

@@ -1,4 +1,4 @@
use leptos::prelude::*;
use leptos::{prelude::*, reactive_graph::actions::Action};
use leptos_router::{
components::{FlatRoutes, Route, Router, A},
StaticSegment,
@@ -10,12 +10,12 @@ use tracing::instrument;
pub mod ssr_imports {
pub use broadcaster::BroadcastChannel;
pub use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::LazyLock;
pub static COUNT: AtomicI32 = AtomicI32::new(0);
pub static COUNT_CHANNEL: LazyLock<BroadcastChannel<i32>> =
LazyLock::new(BroadcastChannel::<i32>::new);
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
}
#[server]

View File

@@ -1,5 +1,5 @@
use counter_without_macros::counter;
use leptos::{prelude::*, task::tick};
use leptos::{prelude::*, spawn::tick};
use pretty_assertions::assert_eq;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -4,7 +4,7 @@ use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counters::Counters;
use leptos::prelude::*;
use leptos::task::tick;
use leptos::spawn::tick;
use web_sys::HtmlElement;
#[wasm_bindgen_test]

View File

@@ -1,5 +1,5 @@
use directives::App;
use leptos::{prelude::*, task::tick};
use leptos::{prelude::*, spawn::tick};
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
use web_sys::HtmlElement;

18
examples/gtk/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "gtk"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
throw_error = { path = "../../any_error/" }
# these are used to build the integration
gtk = { version = "0.9.0", package = "gtk4" }
next_tuple = { path = "../../next_tuple/" }
paste = "1.0"
# we want to support using glib for the reactive runtime event loop
any_spawner = { path = "../../any_spawner/", features = ["glib"] }
# yes, we want effects to run: this is a "frontend," not a backend
reactive_graph = { path = "../../reactive_graph", features = ["effects"] }

8
examples/gtk/index.html Normal file
View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta name="color-scheme" content="dark">
<link rel="css" href="style.css" data-trunk>
</head>
<body></body>
</html>

View File

@@ -0,0 +1,627 @@
use self::properties::Connect;
use gtk::{
ffi::GtkWidget,
glib::{
object::{IsA, IsClass, ObjectExt},
Object, Value,
},
prelude::{Cast, WidgetExt},
Label, Orientation, Widget,
};
use leptos::{
reactive_graph::effect::RenderEffect,
tachys::{
renderer::{CastFrom, Renderer},
view::{Mountable, Render},
},
};
use next_tuple::NextTuple;
use std::{borrow::Cow, marker::PhantomData};
#[derive(Debug)]
pub struct LeptosGtk;
#[derive(Debug, Clone)]
pub struct Element(pub Widget);
impl Element {
pub fn remove(&self) {
self.0.unparent();
}
}
#[derive(Debug, Clone)]
pub struct Text(pub Element);
impl<T> From<T> for Element
where
T: Into<Widget>,
{
fn from(value: T) -> Self {
Element(value.into())
}
}
impl Mountable<LeptosGtk> for Element {
fn unmount(&mut self) {
self.remove()
}
fn mount(
&mut self,
parent: &<LeptosGtk as Renderer>::Element,
marker: Option<&<LeptosGtk as Renderer>::Node>,
) {
self.0
.insert_before(&parent.0, marker.as_ref().map(|m| &m.0));
}
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
if let Some(parent) = self.0.parent() {
child.mount(&Element(parent), Some(self));
return true;
}
false
}
}
impl Mountable<LeptosGtk> for Text {
fn unmount(&mut self) {
self.0.remove()
}
fn mount(
&mut self,
parent: &<LeptosGtk as Renderer>::Element,
marker: Option<&<LeptosGtk as Renderer>::Node>,
) {
self.0
.0
.insert_before(&parent.0, marker.as_ref().map(|m| &m.0));
}
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
self.0.insert_before_this(child)
}
}
impl CastFrom<Element> for Element {
fn cast_from(source: Element) -> Option<Self> {
Some(source)
}
}
impl CastFrom<Element> for Text {
fn cast_from(source: Element) -> Option<Self> {
source
.0
.downcast::<Label>()
.ok()
.map(|n| Text(Element::from(n)))
}
}
impl AsRef<Element> for Element {
fn as_ref(&self) -> &Element {
self
}
}
impl AsRef<Element> for Text {
fn as_ref(&self) -> &Element {
&self.0
}
}
impl Renderer for LeptosGtk {
type Node = Element;
type Element = Element;
type Text = Text;
type Placeholder = Element;
fn intern(text: &str) -> &str {
text
}
fn create_text_node(text: &str) -> Self::Text {
Text(Element::from(Label::new(Some(text))))
}
fn create_placeholder() -> Self::Placeholder {
let label = Label::new(None);
label.set_visible(false);
Element::from(label)
}
fn set_text(node: &Self::Text, text: &str) {
let node_as_text = node.0 .0.downcast_ref::<Label>().unwrap();
node_as_text.set_label(text);
}
fn set_attribute(node: &Self::Element, name: &str, value: &str) {
node.0.set_property(name, value);
}
fn remove_attribute(node: &Self::Element, name: &str) {
node.0.set_property(name, None::<&str>);
}
fn insert_node(
parent: &Self::Element,
new_child: &Self::Node,
marker: Option<&Self::Node>,
) {
new_child
.0
.insert_before(&parent.0, marker.as_ref().map(|n| &n.0));
}
fn remove_node(
parent: &Self::Element,
child: &Self::Node,
) -> Option<Self::Node> {
todo!()
}
fn remove(node: &Self::Node) {
todo!()
}
fn get_parent(node: &Self::Node) -> Option<Self::Node> {
node.0.parent().map(Element::from)
}
fn first_child(node: &Self::Node) -> Option<Self::Node> {
todo!()
}
fn next_sibling(node: &Self::Node) -> Option<Self::Node> {
todo!()
}
fn log_node(node: &Self::Node) {
todo!()
}
fn clear_children(parent: &Self::Element) {
todo!()
}
}
pub fn root<Chil>(children: Chil) -> (Widget, impl Mountable<LeptosGtk>)
where
Chil: Render<LeptosGtk>,
{
let state = r#box()
.orientation(Orientation::Vertical)
.spacing(12)
.child(children)
.build();
(state.as_widget().clone(), state)
}
pub trait WidgetClass {
type Widget: Into<Widget> + IsA<Object> + IsClass;
}
pub struct LGtkWidget<Widg, Props, Chil> {
widget: PhantomData<Widg>,
properties: Props,
children: Chil,
}
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
where
Widg: WidgetClass,
Chil: NextTuple,
{
pub fn child<T>(
self,
child: T,
) -> LGtkWidget<Widg, Props, Chil::Output<T>> {
let LGtkWidget {
widget,
properties,
children,
} = self;
LGtkWidget {
widget,
properties,
children: children.next_tuple(child),
}
}
}
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
where
Widg: WidgetClass,
Props: NextTuple,
Chil: Render<LeptosGtk>,
{
pub fn connect<F>(
self,
signal_name: &'static str,
callback: F,
) -> LGtkWidget<Widg, Props::Output<Connect<F>>, Chil>
where
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
{
let LGtkWidget {
widget,
properties,
children,
} = self;
LGtkWidget {
widget,
properties: properties.next_tuple(Connect {
signal_name,
callback,
}),
children,
}
}
}
pub struct LGtkWidgetState<Widg, Props, Chil>
where
Chil: Render<LeptosGtk>,
Props: Property,
Widg: WidgetClass,
{
ty: PhantomData<Widg>,
widget: Element,
properties: Props::State,
children: Chil::State,
}
impl<Widg, Props, Chil> LGtkWidgetState<Widg, Props, Chil>
where
Chil: Render<LeptosGtk>,
Props: Property,
Widg: WidgetClass,
{
pub fn as_widget(&self) -> &Widget {
&self.widget.0
}
}
impl<Widg, Props, Chil> Render<LeptosGtk> for LGtkWidget<Widg, Props, Chil>
where
Widg: WidgetClass,
Props: Property,
Chil: Render<LeptosGtk>,
{
type State = LGtkWidgetState<Widg, Props, Chil>;
fn build(self) -> Self::State {
let widget = Object::new::<Widg::Widget>();
let widget = Element::from(widget);
let properties = self.properties.build(&widget);
let mut children = self.children.build();
children.mount(&widget, None);
LGtkWidgetState {
ty: PhantomData,
widget,
properties,
children,
}
}
fn rebuild(self, state: &mut Self::State) {
self.properties
.rebuild(&state.widget, &mut state.properties);
self.children.rebuild(&mut state.children);
}
}
impl<Widg, Props, Chil> Mountable<LeptosGtk>
for LGtkWidgetState<Widg, Props, Chil>
where
Widg: WidgetClass,
Props: Property,
Chil: Render<LeptosGtk>,
{
fn unmount(&mut self) {
self.children.unmount();
self.widget.remove();
}
fn mount(
&mut self,
parent: &<LeptosGtk as Renderer>::Element,
marker: Option<&<LeptosGtk as Renderer>::Node>,
) {
self.children.mount(&self.widget, None);
LeptosGtk::insert_node(parent, &self.widget, marker);
}
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
self.widget.insert_before_this(child)
}
}
pub trait Property {
type State;
fn build(self, element: &Element) -> Self::State;
fn rebuild(self, element: &Element, state: &mut Self::State);
}
impl<T, F> Property for F
where
T: Property,
T::State: 'static,
F: Fn() -> T + 'static,
{
type State = RenderEffect<T::State>;
fn build(self, widget: &Element) -> Self::State {
let widget = widget.clone();
RenderEffect::new(move |prev| {
let value = self();
if let Some(mut prev) = prev {
value.rebuild(&widget, &mut prev);
prev
} else {
value.build(&widget)
}
})
}
fn rebuild(self, widget: &Element, state: &mut Self::State) {}
}
pub fn button() -> LGtkWidget<gtk::Button, (), ()> {
LGtkWidget {
widget: PhantomData,
properties: (),
children: (),
}
}
pub fn r#box() -> LGtkWidget<gtk::Box, (), ()> {
LGtkWidget {
widget: PhantomData,
properties: (),
children: (),
}
}
mod widgets {
use super::WidgetClass;
impl WidgetClass for gtk::Button {
type Widget = Self;
}
impl WidgetClass for gtk::Box {
type Widget = Self;
}
}
pub mod properties {
use super::{
Element, LGtkWidget, LGtkWidgetState, LeptosGtk, Property, WidgetClass,
};
use gtk::glib::{object::ObjectExt, Value};
use leptos::tachys::{renderer::Renderer, view::Render};
use next_tuple::NextTuple;
pub struct Connect<F>
where
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
{
pub signal_name: &'static str,
pub callback: F,
}
impl<F> Property for Connect<F>
where
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
{
type State = ();
fn build(self, element: &Element) -> Self::State {
element.0.connect(self.signal_name, false, self.callback);
}
fn rebuild(self, element: &Element, state: &mut Self::State) {}
}
/* examples for macro */
pub struct Orientation {
value: gtk::Orientation,
}
pub struct OrientationState {
value: gtk::Orientation,
}
impl Property for Orientation {
type State = OrientationState;
fn build(self, element: &Element) -> Self::State {
element.0.set_property("orientation", self.value);
OrientationState { value: self.value }
}
fn rebuild(self, element: &Element, state: &mut Self::State) {
if self.value != state.value {
element.0.set_property("orientation", self.value);
state.value = self.value;
}
}
}
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
where
Widg: WidgetClass,
Props: NextTuple,
Chil: Render<LeptosGtk>,
{
pub fn orientation(
self,
value: impl Into<gtk::Orientation>,
) -> LGtkWidget<Widg, Props::Output<Orientation>, Chil> {
let LGtkWidget {
widget,
properties,
children,
} = self;
LGtkWidget {
widget,
properties: properties.next_tuple(Orientation {
value: value.into(),
}),
children,
}
}
}
pub struct Spacing {
value: i32,
}
pub struct SpacingState {
value: i32,
}
impl Property for Spacing {
type State = SpacingState;
fn build(self, element: &Element) -> Self::State {
element.0.set_property("spacing", self.value);
SpacingState { value: self.value }
}
fn rebuild(self, element: &Element, state: &mut Self::State) {
if self.value != state.value {
element.0.set_property("spacing", self.value);
state.value = self.value;
}
}
}
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
where
Widg: WidgetClass,
Props: NextTuple,
Chil: Render<LeptosGtk>,
{
pub fn spacing(
self,
value: impl Into<i32>,
) -> LGtkWidget<Widg, Props::Output<Spacing>, Chil> {
let LGtkWidget {
widget,
properties,
children,
} = self;
LGtkWidget {
widget,
properties: properties.next_tuple(Spacing {
value: value.into(),
}),
children,
}
}
}
/* end examples for properties macro */
pub struct Label {
value: String,
}
impl Label {
pub fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
}
}
}
pub struct LabelState {
value: String,
}
impl Property for Label {
type State = LabelState;
fn build(self, element: &Element) -> Self::State {
LeptosGtk::set_attribute(element, "label", &self.value);
LabelState { value: self.value }
}
fn rebuild(self, element: &Element, state: &mut Self::State) {
todo!()
}
}
impl Property for () {
type State = ();
fn build(self, _element: &Element) -> Self::State {}
fn rebuild(self, _element: &Element, _state: &mut Self::State) {}
}
macro_rules! tuples {
($($ty:ident),* $(,)?) => {
impl<$($ty,)*> Property for ($($ty,)*)
where $($ty: Property,)*
{
type State = ($($ty::State,)*);
fn build(self, element: &Element) -> Self::State {
#[allow(non_snake_case)]
let ($($ty,)*) = self;
($($ty.build(element),)*)
}
fn rebuild(self, element: &Element, state: &mut Self::State) {
paste::paste! {
#[allow(non_snake_case)]
let ($($ty,)*) = self;
#[allow(non_snake_case)]
let ($([<state_ $ty:lower>],)*) = state;
$($ty.rebuild(element, [<state_ $ty:lower>]));*
}
}
}
}
}
tuples!(A);
tuples!(A, B);
tuples!(A, B, C);
tuples!(A, B, C, D);
tuples!(A, B, C, D, E);
tuples!(A, B, C, D, E, F);
tuples!(A, B, C, D, E, F, G);
tuples!(A, B, C, D, E, F, G, H);
tuples!(A, B, C, D, E, F, G, H, I);
tuples!(A, B, C, D, E, F, G, H, I, J);
tuples!(A, B, C, D, E, F, G, H, I, J, K);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
tuples!(
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W
);
tuples!(
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X
);
tuples!(
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X,
Y
);
}

107
examples/gtk/src/main.rs Normal file
View File

@@ -0,0 +1,107 @@
use any_spawner::Executor;
use gtk::{prelude::*, Application, ApplicationWindow, Orientation};
use leptos::prelude::*;
use leptos_gtk::LeptosGtk;
use std::{mem, thread, time::Duration};
mod leptos_gtk;
const APP_ID: &str = "dev.leptos.Counter";
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
fn main() {
// use the glib event loop to power the reactive system
_ = Executor::init_glib();
let app = Application::builder().application_id(APP_ID).build();
app.connect_startup(|_| load_css());
app.connect_activate(|app| {
// Connect to "activate" signal of `app`
let owner = Owner::new();
let view = owner.with(ui);
let (root, state) = leptos_gtk::root(view);
let window = ApplicationWindow::builder()
.application(app)
.title("TachyGTK")
.child(&root)
.build();
// Present window
window.present();
mem::forget((owner, state));
});
app.run();
}
fn ui() -> impl Render<LeptosGtk> {
let value = RwSignal::new(0);
let rows = RwSignal::new(vec![1, 2, 3, 4, 5]);
Effect::new(move |_| {
println!("value = {}", value.get());
});
// just an example of multithreaded reactivity
thread::spawn(move || loop {
thread::sleep(Duration::from_millis(250));
value.update(|n| *n += 1);
});
vstack((
hstack((
button("-1", move || {
println!("clicked -1");
value.update(|n| *n -= 1);
}),
move || value.get().to_string(),
button("+1", move || value.update(|n| *n += 1)),
)),
button("Swap", move || {
rows.update(|items| {
items.swap(1, 3);
})
}),
hstack(rows),
))
}
fn button(
label: impl Render<LeptosGtk>,
callback: impl Fn() + Send + Sync + 'static,
) -> impl Render<LeptosGtk> {
leptos_gtk::button()
.child(label)
.connect("clicked", move |_| {
callback();
None
})
}
fn vstack(children: impl Render<LeptosGtk>) -> impl Render<LeptosGtk> {
leptos_gtk::r#box()
.orientation(Orientation::Vertical)
.spacing(12)
.child(children)
}
fn hstack(children: impl Render<LeptosGtk>) -> impl Render<LeptosGtk> {
leptos_gtk::r#box()
.orientation(Orientation::Horizontal)
.spacing(12)
.child(children)
}
fn load_css() {
use gtk::{gdk::Display, CssProvider};
let provider = CssProvider::new();
provider.load_from_path("style.css");
// Add the provider to the default screen
gtk::style_context_add_provider_for_display(
&Display::default().expect("Could not connect to a display."),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}

0
examples/gtk/style.css Normal file
View File

View File

@@ -21,16 +21,10 @@ pub fn Nav() -> impl IntoView {
<A href="/job">
<strong>"Jobs"</strong>
</A>
<a
class="github"
href="http://github.com/leptos-rs/leptos"
target="_blank"
rel="noreferrer"
>
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
"Built with Leptos"
</a>
</nav>
</header>
}
.into_any()
}

View File

@@ -50,42 +50,30 @@ pub fn Stories() -> impl IntoView {
<div class="news-view">
<div class="news-list-nav">
<span>
{move || {
if page() > 1 {
Either::Left(
view! {
<a
class="page-link"
href=move || {
format!("/{}?page={}", story_type(), page() - 1)
}
aria-label="Previous Page"
>
"< prev"
</a>
},
)
} else {
Either::Right(
view! {
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
},
)
}
{move || if page() > 1 {
Either::Left(view! {
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
aria-label="Previous Page"
>
"< prev"
</a>
})
} else {
Either::Right(view! {
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
})
}}
</span>
<span>"page " {page}</span>
<Suspense>
<span
class="page-link"
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<a
href=move || format!("/{}?page={}", story_type(), page() + 1)
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
@@ -95,10 +83,14 @@ pub fn Stories() -> impl IntoView {
</div>
<main class="news-list">
<div>
<Transition fallback=move || view! { <p>"Loading..."</p> } set_pending>
<Show when=move || {
stories.read().as_ref().map(Option::is_none).unwrap_or(false)
}>> <p>"Error loading stories."</p></Show>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
set_pending
>
<Show when=move || stories.read().as_ref().map(Option::is_none).unwrap_or(false)>
>
<p>"Error loading stories."</p>
</Show>
<ul>
<For
each=move || stories.get().unwrap_or_default().unwrap_or_default()
@@ -113,78 +105,54 @@ pub fn Stories() -> impl IntoView {
</main>
</div>
}
.into_any()
}
#[component]
fn Story(story: api::Story) -> impl IntoView {
view! {
<li class="news-item">
<li class="news-item">
<span class="score">{story.points}</span>
<span class="title">
{if !story.url.starts_with("item?id=") {
Either::Left(
view! {
<span>
<a href=story.url target="_blank" rel="noreferrer">
{story.title.clone()}
</a>
<span class="host">"(" {story.domain} ")"</span>
</span>
},
)
Either::Left(view! {
<span>
<a href=story.url target="_blank" rel="noreferrer">
{story.title.clone()}
</a>
<span class="host">"("{story.domain}")"</span>
</span>
})
} else {
let title = story.title.clone();
Either::Right(view! { <A href=format!("/stories/{}", story.id)>{title}</A> })
}}
</span>
<br/>
<br />
<span class="meta">
{if story.story_type != "job" {
Either::Left(
view! {
<span>
{"by "}
{story
.user
.map(|user| {
view! {
<A href=format!("/users/{user}")>{user.clone()}</A>
}
})} {format!(" {} | ", story.time_ago)}
<A href=format!(
"/stories/{}",
story.id,
)>
{if story.comments_count.unwrap_or_default() > 0 {
format!(
"{} comments",
story.comments_count.unwrap_or_default(),
)
} else {
"discuss".into()
}}
</A>
</span>
},
)
Either::Left(view! {
<span>
{"by "}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"discuss".into()
}}
</A>
</span>
})
} else {
let title = story.title.clone();
Either::Right(view! { <A href=format!("/item/{}", story.id)>{title}</A> })
}}
</span>
{(story.story_type != "link")
.then(|| {
view! {
" "
<span class="label">{story.story_type}</span>
}
})}
{(story.story_type != "link").then(|| view! {
" "
<span class="label">{story.story_type}</span>
})}
</li>
}
.into_any()
}

View File

@@ -28,21 +28,18 @@ pub fn Story() -> impl IntoView {
<Meta name="description" content=story.title.clone()/>
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">"(" {story.domain} ")"</span>
{story
.user
.map(|user| {
view! {
<p class="meta">
{story.points} " points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>
}
})}
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
@@ -51,7 +48,6 @@ pub fn Story() -> impl IntoView {
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
@@ -59,7 +55,7 @@ pub fn Story() -> impl IntoView {
key=|comment| comment.id
let:comment
>
<Comment comment/>
<Comment comment />
</For>
</ul>
</div>
@@ -68,7 +64,6 @@ pub fn Story() -> impl IntoView {
}
}
}))).build())
.into_any()
}
#[component]
@@ -77,65 +72,43 @@ pub fn Comment(comment: api::Comment) -> impl IntoView {
view! {
<li class="comment">
<div class="by">
<A href=format!(
"/users/{}",
comment.user.clone().unwrap_or_default(),
)>{comment.user.clone()}</A>
{format!(" {}", comment.time_ago)}
</div>
<div class="text" inner_html=comment.content></div>
{(!comment.comments.is_empty())
.then(|| {
view! {
<div>
<div class="toggle" class:open=open>
<a on:click=move |_| {
set_open.update(|n| *n = !*n)
}>
{
let comments_len = comment.comments.len();
move || {
if open.get() {
"[-]".into()
} else {
format!(
"[+] {}{} collapsed",
comments_len,
pluralize(comments_len),
)
}
}
}
</a>
</div>
{move || {
open
.get()
.then({
let comments = comment.comments.clone();
move || {
view! {
<ul class="comment-children">
<For
each=move || comments.clone()
key=|comment| comment.id
let:comment
>
<Comment comment/>
</For>
</ul>
}
}
})
}}
</div>
}
})}
<div class="by">
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
{format!(" {}", comment.time_ago)}
</div>
<div class="text" inner_html=comment.content></div>
{(!comment.comments.is_empty()).then(|| {
view! {
<div>
<div class="toggle" class:open=open>
<a on:click=move |_| set_open.update(|n| *n = !*n)>
{
let comments_len = comment.comments.len();
move || if open.get() {
"[-]".into()
} else {
format!("[+] {}{} collapsed", comments_len, pluralize(comments_len))
}
}
</a>
</div>
{move || open.get().then({
let comments = comment.comments.clone();
move || view! {
<ul class="comment-children">
<For
each=move || comments.clone()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
</ul>
}
})}
</div>
}
})}
</li>
}.into_any()
}

View File

@@ -18,48 +18,30 @@ pub fn User() -> impl IntoView {
);
view! {
<div class="user-view">
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || Suspend::new(async move {
match user.await.clone() {
None => Either::Left(view! { <h1>"User not found."</h1> }),
Some(user) => {
Either::Right(
view! {
<div>
<h1>"User: " {user.id.clone()}</h1>
<ul class="meta">
<li>
<span class="label">"Created: "</span>
{user.created}
</li>
<li>
<span class="label">"Karma: "</span>
{user.karma}
</li>
<li inner_html=user.about class="about"></li>
</ul>
<p class="links">
<a href=format!(
"https://news.ycombinator.com/submitted?id={}",
user.id,
)>"submissions"</a>
" | "
<a href=format!(
"https://news.ycombinator.com/threads?id={}",
user.id,
)>"comments"</a>
</p>
</div>
},
)
}
}
})}
<Suspense fallback=|| view! { "Loading..." }>
{move || Suspend::new(async move { match user.await.clone() {
None => Either::Left(view! { <h1>"User not found."</h1> }),
Some(user) => Either::Right(view! {
<div>
<h1>"User: " {user.id.clone()}</h1>
<ul class="meta">
<li>
<span class="label">"Created: "</span> {user.created}
</li>
<li>
<span class="label">"Karma: "</span> {user.karma}
</li>
<li inner_html={user.about} class="about"></li>
</ul>
<p class="links">
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
" | "
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</p>
</div>
})
}})}
</Suspense>
</div>
}
.into_any()
}

View File

@@ -31,6 +31,7 @@ tokio = { version = "1.39", features = ["full"], optional = true }
http = { version = "1.1", optional = true }
web-sys = { version = "0.3.70", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2.93"
lazy_static = "1.5"
rust-embed = { version = "8.5", features = [
"axum",
"mime_guess",

View File

@@ -17,7 +17,7 @@ leptos = { path = "../../leptos", features = [
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [
"dont-use-islands-router",
"islands-router",
], optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }

View File

@@ -1,14 +1,19 @@
# Work in Progress
# Leptos Todo App Sqlite with Axum
This example is something I wrote on a long layover in the Orlando airport in July. (It was really hot!)
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
It is the culmination of a couple years of thinking and working toward being able to do this, which you can see
described pretty well in the pinned roadmap issue (#1830) and its discussion of different modes of client-side
routing when you use islands.
## Getting Started
This uses *only* server rendering, with no actual islands, but still maintains client-side state across page navigations.
It does this by building on the fact that we now have a statically-typed view tree to do pretty smart updates with
new HTML from the client, with extremely minimal diffing.
See the [Examples README](../README.md) for setup and run instructions.
The demo itself works, but the feature that supports it is incomplete. A couple people have accidentally
used it and broken their applications in ways they don't understand, so I've renamed the feature to `dont-use-islands-router`.
## E2E Testing
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -162,24 +162,22 @@ pub fn App() -> impl IntoView {
<table class="table table-hover table-striped test-data">
<tbody>
<For
each=move || data.get()
key=|row| row.id
each={move || data.get()}
key={|row| row.id}
children=move |row: RowData| {
let row_id = row.id;
let label = row.label;
let is_selected = is_selected.clone();
template! {
< tr class : danger = { move || is_selected.selected(Some(row_id)) }
> < td class = "col-md-1" > { row_id.to_string() } </ td > < td
class = "col-md-4" >< a on : click = move | _ | set_selected
.set(Some(row_id)) > { move || label.get() } </ a ></ td > < td
class = "col-md-1" >< a on : click = move | _ | remove(row_id) ><
span class = "glyphicon glyphicon-remove" aria - hidden = "true" ></
span ></ a ></ td > < td class = "col-md-6" /> </ tr >
}
ViewTemplate::new(view! {
<tr class:danger={move || is_selected.selected(Some(row_id))}>
<td class="col-md-1">{row_id.to_string()}</td>
<td class="col-md-4"><a on:click=move |_| set_selected.set(Some(row_id))>{move || label.get()}</a></td>
<td class="col-md-1"><a on:click=move |_| remove(row_id)><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></td>
<td class="col-md-6"/>
</tr>
})
}
/>
</tbody>
</table>
<span class="preloadicon glyphicon glyphicon-remove" aria-hidden="true"></span>

View File

@@ -1,5 +1,5 @@
use js_framework_benchmark_leptos::*;
use leptos::{prelude::*, task::tick};
use leptos::{prelude::*, spawn::tick};
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -3,7 +3,7 @@ use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use leptos::task::tick;
use leptos::spawn::tick;
use leptos::{leptos_dom::helpers::document, mount::mount_to};
use web_sys::HtmlButtonElement;

View File

@@ -3,7 +3,7 @@
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
<link data-trunk rel="css" href="style.css"/>
<link data-trunk rel="css" href="style.css"/>
</head>
<body></body>
</html>

View File

@@ -5,14 +5,13 @@ use leptos::prelude::*;
use leptos_router::{
components::{
Form, Outlet, ParentRoute, ProtectedRoute, Redirect, Route, Router,
Routes, RoutingProgress, A,
Routes, A,
},
hooks::{use_navigate, use_params, use_query_map},
params::Params,
MatchNestedRoutes,
};
use leptos_router_macro::path;
use std::time::Duration;
use tracing::info;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -27,14 +26,9 @@ pub fn RouterExample() -> impl IntoView {
// this signal will be ued to set whether we are allowed to access a protected route
let (logged_in, set_logged_in) = signal(true);
let (is_routing, set_is_routing) = signal(false);
view! {
<Router set_is_routing>
// shows a progress bar while async data are loading
<div class="routing-progress">
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
</div>
<Router>
<nav>
// ordinary <a> elements can be used for client-side navigation
// using <A> has two effects:
@@ -50,7 +44,7 @@ pub fn RouterExample() -> impl IntoView {
}>{move || if logged_in.get() { "Log Out" } else { "Log In" }}</button>
</nav>
<main>
<Routes transition=true fallback=|| "This page could not be found.">
<Routes fallback=|| "This page could not be found.">
// paths can be created using the path!() macro, or provided as types like
// StaticSegment("about")
<Route path=path!("about") view=About/>
@@ -70,8 +64,8 @@ pub fn RouterExample() -> impl IntoView {
// You can define other routes in their own component.
// Routes implement the MatchNestedRoutes
#[component(transparent)]
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
#[component]
pub fn ContactRoutes() -> impl MatchNestedRoutes<Dom> + Clone {
view! {
<ParentRoute path=path!("") view=ContactList>
<Route path=path!("/") view=|| "Select a contact."/>

View File

@@ -1,8 +1,3 @@
.routing-progress {
width: 100%;
height: 20px;
}
a[aria-current] {
font-weight: bold;
}
@@ -17,8 +12,12 @@ a[aria-current] {
padding: 1rem;
}
.contact {
view-transition-name: contact;
.fadeIn {
animation: 0.5s fadeIn forwards;
}
.fadeOut {
animation: 0.5s fadeOut forwards;
}
@keyframes fadeIn {
@@ -41,44 +40,12 @@ a[aria-current] {
}
}
.router-outlet-0 main {
view-transition-name: main;
.slideIn {
animation: 0.25s slideIn forwards;
}
.router-back main {
view-transition-name: main-back;
}
.router-outlet-1 .contact-list {
view-transition-name: contact;
}
@media (prefers-reduced-motion: no-preference) {
::view-transition-old(contact) {
animation: 0.5s fadeOut;
}
::view-transition-new(contact) {
animation: 0.5s fadeIn;
}
::view-transition-old(main) {
animation: 0.5s slideOut;
}
::view-transition-new(main) {
animation: 0.5s slideIn;
}
::view-transition-old(main-back) {
color: red;
animation: 0.5s slideOutBack;
}
::view-transition-new(main-back) {
color: blue;
animation: 0.5s slideInBack;
}
.slideOut {
animation: 0.25s slideOut forwards;
}
@keyframes slideIn {
@@ -99,6 +66,14 @@ a[aria-current] {
}
}
.slideInBack {
animation: 0.25s slideInBack forwards;
}
.slideOutBack {
animation: 0.25s slideOutBack forwards;
}
@keyframes slideInBack {
from {
transform: translate(-100vw, 0);

View File

@@ -40,8 +40,6 @@ pin-project-lite = "0.2.14"
dashmap = { version = "6.0", optional = true }
once_cell = { version = "1.19", optional = true }
async-broadcast = { version = "0.7.1", optional = true }
bytecheck = "0.8.0"
rkyv = { version = "0.8.8" }
[features]
hydrate = ["leptos/hydrate"]

View File

@@ -1,6 +1,6 @@
use futures::StreamExt;
use http::Method;
use leptos::{html::Input, prelude::*, task::spawn_local};
use leptos::{html::Input, prelude::*, spawn::spawn_local};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use server_fn::{
client::{browser::BrowserClient, Client},
@@ -417,6 +417,7 @@ pub fn FileUploadWithProgress() -> impl IntoView {
/// This requires us to store some global state of all the uploads. In a real app, you probably
/// shouldn't do exactly what I'm doing here in the demo. For example, this map just
/// distinguishes between files by filename, not by user.
#[cfg(feature = "ssr")]
mod progress {
use async_broadcast::{broadcast, Receiver, Sender};

View File

@@ -4,8 +4,6 @@ use leptos::{config::get_configuration, logging};
use leptos_axum::{generate_route_list, LeptosRoutes};
use server_fns_axum::*;
// cargo make cli: error: unneeded `return` statement
#[allow(clippy::needless_return)]
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Error)

View File

@@ -2,8 +2,6 @@
name = "ssr_modes"
version = "0.1.0"
edition = "2021"
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
[lib]
crate-type = ["cdylib", "rlib"]
@@ -13,6 +11,7 @@ actix-files = { version = "0.6.6", optional = true }
actix-web = { version = "4.8", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
lazy_static = "1.5"
leptos = { path = "../../leptos" }
leptos_meta = { path = "../../meta" }
leptos_actix = { path = "../../integrations/actix", optional = true }
@@ -39,12 +38,12 @@ denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "ssr_modes"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
# 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/main.scss"

View File

@@ -1,5 +1,4 @@
use std::sync::LazyLock;
use lazy_static::lazy_static;
use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::{
@@ -147,9 +146,8 @@ fn Post() -> impl IntoView {
}
// Dummy API
static POSTS: LazyLock<[Post; 3]> = LazyLock::new(|| {
[
lazy_static! {
static ref POSTS: Vec<Post> = vec![
Post {
id: 0,
title: "My first post".to_string(),
@@ -165,8 +163,8 @@ static POSTS: LazyLock<[Post; 3]> = LazyLock::new(|| {
title: "My third post".to_string(),
content: "This is my third post".to_string(),
},
]
});
];
}
#[derive(Error, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PostError {

View File

@@ -2,8 +2,6 @@
name = "ssr_modes_axum"
version = "0.1.0"
edition = "2021"
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
[lib]
crate-type = ["cdylib", "rlib"]
@@ -11,6 +9,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
console_error_panic_hook = "0.1.7"
console_log = "1.0"
lazy_static = "1.5"
leptos = { path = "../../leptos", features = [
"hydration",
] } #"nightly", "hydration"] }

View File

@@ -1,5 +1,4 @@
use std::sync::LazyLock;
use lazy_static::lazy_static;
use leptos::prelude::*;
use leptos_meta::MetaTags;
use leptos_meta::*;
@@ -262,9 +261,8 @@ pub fn Admin() -> impl IntoView {
}
// Dummy API
static POSTS: LazyLock<[Post; 3]> = LazyLock::new(|| {
[
lazy_static! {
static ref POSTS: Vec<Post> = vec![
Post {
id: 0,
title: "My first post".to_string(),
@@ -280,8 +278,8 @@ static POSTS: LazyLock<[Post; 3]> = LazyLock::new(|| {
title: "My third post".to_string(),
content: "This is my third post".to_string(),
},
]
});
];
}
#[derive(Error, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PostError {

View File

@@ -13,9 +13,6 @@ leptos = { path = "../../leptos", features = ["csr"] }
reactive_stores = { path = "../../reactive_stores" }
reactive_stores_macro = { path = "../../reactive_stores_macro" }
console_error_panic_hook = "0.1.7"
chrono = { version = "0.4.38", features = ["serde"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
[dev-dependencies]
wasm-bindgen = "0.2.93"

View File

@@ -3,11 +3,6 @@
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
<style>
.hidden {
display: none;
}
</style>
</head>
<body></body>
</html>
</html>

View File

@@ -1,87 +1,43 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use chrono::{Local, NaiveDate};
use leptos::prelude::*;
use reactive_stores::{Field, Patch, Store};
use serde::{Deserialize, Serialize};
use reactive_stores::{Field, Store, StoreFieldIterator};
use reactive_stores_macro::Store;
// ID starts higher than 0 because we have a few starting todos by default
static NEXT_ID: AtomicUsize = AtomicUsize::new(3);
#[derive(Debug, Store, Serialize, Deserialize)]
#[derive(Debug, Store)]
struct Todos {
user: User,
#[store(key: usize = |todo| todo.id)]
user: String,
todos: Vec<Todo>,
}
#[derive(Debug, Store, Patch, Serialize, Deserialize)]
struct User {
name: String,
email: String,
}
#[derive(Debug, Store, Serialize, Deserialize)]
#[derive(Debug, Store)]
struct Todo {
id: usize,
label: String,
status: Status,
}
#[derive(Debug, Default, Clone, Store, Serialize, Deserialize)]
enum Status {
#[default]
Pending,
Scheduled,
ScheduledFor {
date: NaiveDate,
},
Done,
}
impl Status {
pub fn next_step(&mut self) {
*self = match self {
Status::Pending => Status::ScheduledFor {
date: Local::now().naive_local().into(),
},
Status::Scheduled | Status::ScheduledFor { .. } => Status::Done,
Status::Done => Status::Done,
};
}
completed: bool,
}
impl Todo {
pub fn new(label: impl ToString) -> Self {
Self {
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
label: label.to_string(),
status: Status::Pending,
completed: false,
}
}
}
fn data() -> Todos {
Todos {
user: User {
name: "Bob".to_string(),
email: "lawblog@bobloblaw.com".into(),
},
user: "Bob".to_string(),
todos: vec![
Todo {
id: 0,
label: "Create reactive store".to_string(),
status: Status::Pending,
completed: true,
},
Todo {
id: 1,
label: "???".to_string(),
status: Status::Pending,
completed: false,
},
Todo {
id: 2,
label: "Profit".to_string(),
status: Status::Pending,
completed: false,
},
],
}
@@ -93,10 +49,17 @@ pub fn App() -> impl IntoView {
let input_ref = NodeRef::new();
let rows = move || {
store
.todos()
.iter()
.enumerate()
.map(|(idx, todo)| view! { <TodoRow store idx todo/> })
.collect_view()
};
view! {
<p>"Hello, " {move || store.user().name().get()}</p>
<UserForm user=store.user()/>
<hr/>
<p>"Hello, " {move || store.user().get()}</p>
<form on:submit=move |ev| {
ev.prevent_default();
store.todos().write().push(Todo::new(input_ref.get().unwrap().value()));
@@ -104,69 +67,30 @@ pub fn App() -> impl IntoView {
<label>"Add a Todo" <input type="text" node_ref=input_ref/></label>
<input type="submit"/>
</form>
<ol>
// because `todos` is a keyed field, `store.todos()` returns a struct that
// directly implements IntoIterator, so we can use it in <For/> and
// it will manage reactivity for the store fields correctly
<For
each=move || {
leptos::logging::log!("RERUNNING FOR CALCULATION");
store.todos()
}
key=|row| row.id().get()
let:todo
>
<TodoRow store todo/>
</For>
</ol>
<pre>{move || serde_json::to_string_pretty(&*store.read())}</pre>
}
}
#[component]
fn UserForm(#[prop(into)] user: Field<User>) -> impl IntoView {
let error = RwSignal::new(None);
view! {
{move || error.get().map(|n| view! { <p>{n}</p> })}
<form on:submit:target=move |ev| {
ev.prevent_default();
match User::from_event(&ev) {
Ok(new_user) => {
error.set(None);
user.patch(new_user);
}
Err(e) => error.set(Some(e.to_string())),
}
}>
<label>
"Name" <input type="text" name="name" prop:value=move || user.name().get()/>
</label>
<label>
"Email" <input type="email" name="email" prop:value=move || user.email().get()/>
</label>
<input type="submit"/>
</form>
<ol>{rows}</ol>
<div style="display: flex"></div>
}
}
#[component]
fn TodoRow(
store: Store<Todos>,
idx: usize,
#[prop(into)] todo: Field<Todo>,
) -> impl IntoView {
let status = todo.status();
let completed = todo.completed();
let title = todo.label();
let editing = RwSignal::new(true);
let editing = RwSignal::new(false);
view! {
<li style:text-decoration=move || {
status.done().then_some("line-through").unwrap_or_default()
}>
<li
style:text-decoration=move || {
completed.get().then_some("line-through").unwrap_or_default()
}
class:foo=move || completed.get()
>
<p
class:hidden=move || editing.get()
on:click=move |_| {
@@ -182,48 +106,25 @@ fn TodoRow(
prop:value=move || title.get()
on:change=move |ev| {
title.set(event_target_value(&ev));
editing.set(false);
}
on:blur=move |_| editing.set(false)
autofocus
/>
<button on:click=move |_| {
status.write().next_step()
}>
{move || {
if todo.status().done() {
"Done"
} else if status.scheduled() || status.scheduled_for() {
"Scheduled"
} else {
"Pending"
}
}}
</button>
<button on:click=move |_| {
let id = todo.id().get();
store.todos().write().retain(|todo| todo.id != id);
}>"X"</button>
<input
type="date"
prop:value=move || {
todo.status().scheduled_for_date().map(|n| n.get().to_string())
}
class:hidden=move || !todo.status().scheduled_for()
on:change:target=move |ev| {
if let Some(date) = todo.status().scheduled_for_date() {
let value = ev.target().value();
match NaiveDate::parse_from_str(&value, "%Y-%m-%d") {
Ok(new_date) => {
date.set(new_date);
}
Err(e) => warn!("{e}"),
}
}
}
type="checkbox"
prop:checked=move || completed.get()
on:click=move |_| { completed.update(|n| *n = !*n) }
/>
<button on:click=move |_| {
store
.todos()
.update(|todos| {
todos.remove(idx);
});
}>"X"</button>
</li>
}
}

View File

@@ -147,13 +147,14 @@ fn Nested() -> impl IntoView {
"Loading 1..."
}>
{move || {
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
}}
<Suspense fallback=|| {
"Loading 2..."
}>
{move || {
two_second
.get()
.map(|_| {
view! {
<p id="loaded-2">"Two Second: Loaded 2!"</p>
@@ -216,6 +217,7 @@ fn Parallel() -> impl IntoView {
}>
{move || {
one_second
.get()
.map(move |_| {
view! {
<p id="loaded-1">"One Second: Loaded 1!"</p>
@@ -232,6 +234,7 @@ fn Parallel() -> impl IntoView {
}>
{move || {
two_second
.get()
.map(move |_| {
view! {
<p id="loaded-2">"Two Second: Loaded 2!"</p>
@@ -261,7 +264,7 @@ fn Single() -> impl IntoView {
"Loading 1..."
}>
{move || {
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
}}
</Suspense>
@@ -297,7 +300,7 @@ fn InsideComponentChild() -> impl IntoView {
"Loading 1..."
}>
{move || {
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
}}
</Suspense>
@@ -316,7 +319,7 @@ fn LocalResource() -> impl IntoView {
"Loading 1..."
}>
{move || {
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
}}
{move || {
Suspend::new(async move {

View File

@@ -319,7 +319,10 @@ pub fn Todo(todo: Todo) -> impl IntoView {
node_ref=todo_input
class="toggle"
type="checkbox"
bind:checked=todo.completed
prop:checked=move || todo.completed.get()
on:input:target=move |ev| {
todo.completed.set(ev.target().checked());
}
/>
<label on:dblclick=move |_| {

58
flake.lock generated
View File

@@ -5,11 +5,29 @@
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
@@ -20,11 +38,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1727634051,
"narHash": "sha256-S5kVU7U82LfpEukbn/ihcyNt2+EvG7Z5unsKW9H/yFA=",
"lastModified": 1703961334,
"narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "06cf0e1da4208d3766d898b7fdab6513366d45b9",
"rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9",
"type": "github"
},
"original": {
@@ -36,11 +54,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
@@ -59,14 +77,15 @@
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1727749966,
"narHash": "sha256-DUS8ehzqB1DQzfZ4bRXVSollJhu+y7cvh1DJ9mbWebE=",
"lastModified": 1704075545,
"narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "00decf1b4f9886d25030b9ee4aed7bfddccb5f66",
"rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5",
"type": "github"
},
"original": {
@@ -89,6 +108,21 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -1,6 +1,6 @@
[package]
name = "hydration_context"
version = "0.2.0-gamma3"
version = "0.2.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -27,7 +27,6 @@ type SealedErrors = Arc<RwLock<HashSet<SerializedDataId>>>;
/// The shared context that should be used on the server side.
pub struct SsrSharedContext {
id: AtomicUsize,
non_hydration_id: AtomicUsize,
is_hydrating: AtomicBool,
sync_buf: RwLock<Vec<ResolvedData>>,
async_buf: AsyncDataBuf,
@@ -42,7 +41,6 @@ impl SsrSharedContext {
pub fn new() -> Self {
Self {
is_hydrating: AtomicBool::new(true),
non_hydration_id: AtomicUsize::new(usize::MAX),
..Default::default()
}
}
@@ -54,7 +52,6 @@ impl SsrSharedContext {
pub fn new_islands() -> Self {
Self {
is_hydrating: AtomicBool::new(false),
non_hydration_id: AtomicUsize::new(usize::MAX),
..Default::default()
}
}
@@ -76,13 +73,8 @@ impl SharedContext for SsrSharedContext {
false
}
#[track_caller]
fn next_id(&self) -> SerializedDataId {
let id = if self.get_is_hydrating() {
self.id.fetch_add(1, Ordering::Relaxed)
} else {
self.non_hydration_id.fetch_sub(1, Ordering::Relaxed)
};
let id = self.id.fetch_add(1, Ordering::Relaxed);
SerializedDataId(id)
}

View File

@@ -33,7 +33,7 @@ once_cell = "1"
rustdoc-args = ["--generate-link-to-definition"]
[features]
dont-use-islands-router = []
islands-router = []
tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]

View File

@@ -24,7 +24,7 @@ use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
prelude::expect_context,
reactive::{computed::ScopedFuture, owner::Owner},
reactive_graph::{computed::ScopedFuture, owner::Owner},
IntoView,
};
use leptos_integration_utils::{
@@ -749,7 +749,7 @@ where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1381,41 +1381,39 @@ where
),
)
} else {
router
.route(path, web::head().to(HttpResponse::Ok))
.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context_and_method.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
router.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context_and_method.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
};
}
}

View File

@@ -16,6 +16,8 @@ axum = { version = "0.7.5", default-features = false, features = [
] }
dashmap = "6"
futures = "0.3.30"
http = "1.1"
http-body-util = "0.1.2"
leptos = { workspace = true, features = ["nonce", "ssr"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
@@ -24,8 +26,9 @@ leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
once_cell = "1"
parking_lot = "0.12.3"
serde_json = "1.0"
tokio = { version = "1.39", default-features = false }
tower = { version = "0.4.13", features = ["util"] }
tower = "0.4.13"
tower-http = "0.5.2"
tracing = { version = "0.1.40", optional = true }
@@ -35,8 +38,8 @@ tokio = { version = "1.39", features = ["net", "rt-multi-thread"] }
[features]
wasm = []
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
dont-use-islands-router = []
default = ["tokio/fs", "tokio/sync", "tower-http/fs"]
islands-router = []
tracing = ["dep:tracing"]
[package.metadata.docs.rs]

View File

@@ -53,7 +53,7 @@ use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
prelude::*,
reactive::{computed::ScopedFuture, owner::Owner},
reactive_graph::{computed::ScopedFuture, owner::Owner},
IntoView,
};
use leptos_integration_utils::{
@@ -76,7 +76,7 @@ use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
use std::path::Path;
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
#[cfg(feature = "default")]
use tower::util::ServiceExt;
use tower::ServiceExt;
#[cfg(feature = "default")]
use tower_http::services::ServeDir;
// use tracing::Instrument; // TODO check tracing span -- was this used in 0.6 for a missing link?
@@ -606,9 +606,9 @@ where
/// use axum::{
/// body::Body,
/// extract::Path,
/// http::Request,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::{config::LeptosOptions, context::provide_context, prelude::*};
///
/// async fn custom_handler(
@@ -784,7 +784,7 @@ where
_ = replace_blocks; // TODO
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_out_of_order()
@@ -806,9 +806,9 @@ where
/// use axum::{
/// body::Body,
/// extract::Path,
/// http::Request,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::context::provide_context;
///
/// async fn custom_handler(
@@ -849,7 +849,7 @@ where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks| {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1025,9 +1025,9 @@ where
/// use axum::{
/// body::Body,
/// extract::Path,
/// http::Request,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::context::provide_context;
///
/// async fn custom_handler(
@@ -1069,7 +1069,7 @@ where
{
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1093,9 +1093,9 @@ where
/// use axum::{
/// body::Body,
/// extract::Path,
/// http::Request,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::context::provide_context;
///
/// async fn custom_handler(
@@ -1146,7 +1146,7 @@ where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1342,7 +1342,8 @@ where
.with(|| {
// stub out a path for now
provide_context(RequestUrl::new(""));
let (mock_parts, _) = Request::new(Body::from("")).into_parts();
let (mock_parts, _) =
http::Request::new(Body::from("")).into_parts();
let (mock_meta, _) = ServerMetaContext::new();
provide_contexts("", &mock_meta, mock_parts, Default::default());
additional_context();
@@ -1401,8 +1402,8 @@ impl StaticRouteGenerator {
let add_context = additional_context.clone();
move || {
let full_path = format!("http://leptos.dev{path}");
let mock_req = Request::builder()
.method(Method::GET)
let mock_req = http::Request::builder()
.method(http::Method::GET)
.header("Accept", "text/html")
.body(Body::empty())
.unwrap();
@@ -1494,12 +1495,10 @@ impl StaticRouteGenerator {
_ = routes;
_ = app_fn;
_ = additional_context;
Self(Box::new(|_| {
panic!(
"Static routes are not currently supported on WASM32 \
server targets."
);
}))
panic!(
"Static routes are not currently supported on WASM32 server \
targets."
);
}
}
@@ -1934,7 +1933,7 @@ where
///
/// #[server]
/// pub async fn request_method() -> Result<String, ServerFnError> {
/// use axum::http::Method;
/// use http::Method;
/// use leptos_axum::extract;
///
/// // you can extract anything that a regular Axum extractor can extract
@@ -1993,7 +1992,7 @@ where
move |uri: Uri, State(options): State<S>, req: Request<Body>| {
Box::pin(async move {
let options = LeptosOptions::from_ref(&options);
let res = get_static_file(uri, &options.site_root, req.headers());
let res = get_static_file(uri, &options.site_root);
let res = res.await.unwrap();
if res.status() == StatusCode::OK {
@@ -2027,26 +2026,14 @@ where
async fn get_static_file(
uri: Uri,
root: &str,
headers: &HeaderMap<HeaderValue>,
) -> Result<Response<Body>, (StatusCode, String)> {
use axum::http::header::ACCEPT_ENCODING;
let req = Request::builder().uri(uri);
let req = match headers.get(ACCEPT_ENCODING) {
Some(value) => req.header(ACCEPT_ENCODING, value),
None => req,
};
let req = req.body(Body::empty()).unwrap();
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root)
.precompressed_gzip()
.precompressed_br()
.oneshot(req)
.await
{
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,

View File

@@ -2,7 +2,7 @@ use futures::{stream::once, Stream, StreamExt};
use hydration_context::{SharedContext, SsrSharedContext};
use leptos::{
nonce::use_nonce,
reactive::owner::{Owner, Sandboxed},
reactive_graph::owner::{Owner, Sandboxed},
IntoView,
};
use leptos_config::LeptosOptions;
@@ -59,7 +59,7 @@ pub trait ExtendResponse: Sized {
// drop the owner, cleaning up the reactive runtime,
// once the stream is over
.chain(once(async move {
owner.unset();
drop(owner);
Default::default()
})),
));

View File

@@ -28,7 +28,7 @@ paste = "1.0"
rand = { version = "0.8.5", optional = true }
reactive_graph = { workspace = true, features = ["serde"] }
rustc-hash = "2.0"
tachys = { workspace = true, features = ["reactive_graph", "reactive_stores", "oco"] }
tachys = { workspace = true, features = ["reactive_graph", "oco"] }
thiserror = "1.0"
tracing = { version = "0.1.40", optional = true }
typed-builder = "0.19.1"
@@ -45,7 +45,7 @@ web-sys = { version = "0.3.70", features = [
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = "=0.2.93"
wasm-bindgen = "0.2.93"
serde_qs = "0.13.0"
slotmap = "1.0"
futures = "0.3.30"
@@ -56,7 +56,6 @@ hydration = [
"reactive_graph/hydration",
"leptos_server/hydration",
"hydration_context/browser",
"leptos_dom/hydration"
]
csr = ["leptos_macro/csr", "reactive_graph/effects"]
hydrate = [

View File

@@ -0,0 +1,59 @@
#![allow(deprecated)]
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(Clone)]
#[repr(transparent)]
#[deprecated = "Most uses of `AdditionalAttributes` can be replaced with `#[prop(attrs)]` \
and the `attr:` syntax. If you have a use case that still requires `AdditionalAttributes`, please \
open a GitHub issue here and share it: https://github.com/leptos-rs/leptos"]
pub struct AdditionalAttributes(pub(crate) Rc<[(String, TextProp)]>);
impl<I, T, U> From<I> for AdditionalAttributes
where
I: IntoIterator<Item = (T, U)>,
T: Into<String>,
U: Into<TextProp>,
{
fn from(value: I) -> Self {
Self(
value
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
}
}
impl Default for AdditionalAttributes {
fn default() -> Self {
Self([].into_iter().collect())
}
}
/// Iterator over additional HTML attributes.
#[repr(transparent)]
pub struct AdditionalAttributesIter<'a>(
std::slice::Iter<'a, (String, TextProp)>,
);
impl<'a> Iterator for AdditionalAttributesIter<'a> {
type Item = &'a (String, TextProp);
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
impl<'a> IntoIterator for &'a AdditionalAttributes {
type Item = &'a (String, TextProp);
type IntoIter = AdditionalAttributesIter<'a>;
fn into_iter(self) -> Self::IntoIter {
AdditionalAttributesIter(self.0.iter())
}
}

View File

@@ -1,15 +1,12 @@
use crate::{children::ChildrenFn, component, control_flow::Show, IntoView};
use crate::{ChildrenFn, Show};
use core::time::Duration;
use leptos_dom::helpers::TimeoutHandle;
use leptos::component;
use leptos_dom::{helpers::TimeoutHandle, IntoView};
use leptos_macro::view;
use reactive_graph::{
effect::RenderEffect,
owner::{on_cleanup, StoredValue},
signal::RwSignal,
traits::{Get, GetUntracked, GetValue, Set, SetValue},
wrappers::read::Signal,
use leptos_reactive::{
create_render_effect, on_cleanup, signal_prelude::*, store_value,
StoredValue,
};
use tachys::prelude::*;
/// A component that will show its children when the `when` condition is `true`.
/// Additionally, you need to specify a `hide_delay`. If the `when` condition changes to `false`,
@@ -19,10 +16,10 @@ use tachys::prelude::*;
///
/// ```rust
/// # use core::time::Duration;
/// # use leptos::prelude::*;
/// # use leptos::*;
/// # #[component]
/// # pub fn App() -> impl IntoView {
/// let show = RwSignal::new(false);
/// let show = create_rw_signal(false);
///
/// view! {
/// <div
@@ -53,7 +50,7 @@ pub fn AnimatedShow(
children: ChildrenFn,
/// If the component should show or not
#[prop(into)]
when: Signal<bool>,
when: MaybeSignal<bool>,
/// Optional CSS class to apply if `when == true`
#[prop(optional)]
show_class: &'static str,
@@ -63,15 +60,15 @@ pub fn AnimatedShow(
/// The timeout after which the component will be unmounted if `when == false`
hide_delay: Duration,
) -> impl IntoView {
let handle: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
let cls = RwSignal::new(if when.get_untracked() {
let handle: StoredValue<Option<TimeoutHandle>> = store_value(None);
let cls = create_rw_signal(if when.get_untracked() {
show_class
} else {
hide_class
});
let show = RwSignal::new(when.get_untracked());
let show = create_rw_signal(when.get_untracked());
let eff = RenderEffect::new(move |_| {
create_render_effect(move |_| {
if when.get() {
// clear any possibly active timer
if let Some(h) = handle.get_value() {
@@ -96,7 +93,6 @@ pub fn AnimatedShow(
if let Some(Some(h)) = handle.try_get_value() {
h.clear();
}
drop(eff);
});
view! {

View File

@@ -1,8 +1,10 @@
use crate::{prelude::Suspend, suspense_component::Suspense, IntoView};
use crate::Suspense;
use leptos_dom::IntoView;
use leptos_macro::{component, view};
use leptos_server::ArcOnceResource;
use reactive_graph::prelude::ReadUntracked;
use serde::{de::DeserializeOwned, Serialize};
use leptos_reactive::{
create_blocking_resource, create_local_resource, create_resource,
store_value, Serializable,
};
#[component]
/// Allows you to inline the data loading for an `async` block or
@@ -13,8 +15,11 @@ use serde::{de::DeserializeOwned, Serialize};
/// Adding `let:{variable name}` to the props makes the data available in the children
/// that variable name, when resolved.
/// ```
/// # use leptos::prelude::*;
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # if false {
/// # let runtime = create_runtime();
/// async fn fetch_monkeys(monkey: i32) -> i32 {
/// // do some expensive work
/// 3
@@ -22,23 +27,29 @@ use serde::{de::DeserializeOwned, Serialize};
///
/// view! {
/// <Await
/// future=fetch_monkeys(3)
/// future=|| fetch_monkeys(3)
/// let:data
/// >
/// <p>{*data} " little monkeys, jumping on the bed."</p>
/// </Await>
/// }
/// # ;
/// # runtime.dispose();
/// # }
/// ```
pub fn Await<T, Fut, Chil, V>(
/// A [`Future`](std::future::Future) that will the component will `.await`
/// before rendering.
future: Fut,
/// If `true`, the component will create a blocking resource, preventing
pub fn Await<T, Fut, FF, VF, V>(
/// A function that returns the [`Future`](std::future::Future) that
/// will the component will `.await` before rendering.
future: FF,
/// If `true`, the component will use [`create_blocking_resource`], preventing
/// the HTML stream from returning anything before `future` has resolved.
#[prop(optional)]
blocking: bool,
/// If `true`, the component will use [`create_local_resource`], this will
/// always run on the local system and therefore its result type does not
/// need to be `Serializable`.
#[prop(optional)]
local: bool,
/// A function that takes a reference to the resolved data from the `future`
/// renders a view.
///
@@ -47,58 +58,65 @@ pub fn Await<T, Fut, Chil, V>(
/// `let:` syntax to specify the name for the data variable.
///
/// ```rust
/// # use leptos::prelude::*;
/// # use leptos::*;
/// # if false {
/// # let runtime = create_runtime();
/// # async fn fetch_monkeys(monkey: i32) -> i32 {
/// # 3
/// # }
/// view! {
/// <Await
/// future=fetch_monkeys(3)
/// future=|| fetch_monkeys(3)
/// let:data
/// >
/// <p>{*data} " little monkeys, jumping on the bed."</p>
/// </Await>
/// }
/// # ;
/// # runtime.dispose();
/// # }
/// ```
/// is the same as
/// ```rust
/// # use leptos::prelude::*;
/// # use leptos::*;
/// # if false {
/// # let runtime = create_runtime();
/// # async fn fetch_monkeys(monkey: i32) -> i32 {
/// # 3
/// # }
/// view! {
/// <Await
/// future=fetch_monkeys(3)
/// future=|| fetch_monkeys(3)
/// children=|data| view! {
/// <p>{*data} " little monkeys, jumping on the bed."</p>
/// }
/// />
/// }
/// # ;
/// # runtime.dispose();
/// # }
/// ```
children: Chil,
children: VF,
) -> impl IntoView
where
T: Send + Sync + Serialize + DeserializeOwned + 'static,
Fut: std::future::Future<Output = T> + Send + 'static,
Chil: FnOnce(&T) -> V + Send + 'static,
V: IntoView + 'static,
Fut: std::future::Future<Output = T> + 'static,
FF: Fn() -> Fut + 'static,
V: IntoView,
VF: Fn(&T) -> V + 'static,
T: Serializable + 'static,
{
let res = ArcOnceResource::<T>::new_with_options(future, blocking);
let ready = res.ready();
let res = if blocking {
create_blocking_resource(|| (), move |_| future())
} else if local {
create_local_resource(|| (), move |_| future())
} else {
create_resource(|| (), move |_| future())
};
let view = store_value(children);
view! {
<Suspense fallback=|| ()>
{Suspend::new(async move {
ready.await;
children(res.read_untracked().as_ref().unwrap())
})}
{move || res.map(|data| view.with_value(|view| view(data)))}
</Suspense>
}
}

View File

@@ -41,10 +41,7 @@
//!
//! Use `SyncCallback` if the function is not `Sync` and `Send`.
use reactive_graph::{
owner::{LocalStorage, StoredValue},
traits::WithValue,
};
use reactive_graph::owner::{LocalStorage, StoredValue};
use std::{fmt, rc::Rc, sync::Arc};
/// A wrapper trait for calling callbacks.

View File

@@ -3,10 +3,13 @@ use std::{
fmt::{self, Debug},
sync::Arc,
};
use tachys::view::{
any_view::{AnyView, IntoAny},
fragment::{Fragment, IntoFragment},
RenderHtml,
use tachys::{
renderer::dom::Dom,
view::{
any_view::{AnyView, IntoAny},
fragment::{Fragment, IntoFragment},
RenderHtml,
},
};
/// The most common type for the `children` property on components,
@@ -14,31 +17,31 @@ use tachys::view::{
///
/// This does not support iterating over individual nodes within the children.
/// To iterate over children, use [`ChildrenFragment`].
pub type Children = Box<dyn FnOnce() -> AnyView + Send>;
pub type Children = Box<dyn FnOnce() -> AnyView<Dom> + Send>;
/// A type for the `children` property on components that can be called only once,
/// and provides a collection of all the children passed to this component.
pub type ChildrenFragment = Box<dyn FnOnce() -> Fragment + Send>;
pub type ChildrenFragment = Box<dyn FnOnce() -> Fragment<Dom> + Send>;
/// A type for the `children` property on components that can be called
/// more than once.
pub type ChildrenFn = Arc<dyn Fn() -> AnyView + Send + Sync>;
pub type ChildrenFn = Arc<dyn Fn() -> AnyView<Dom> + Send + Sync>;
/// A type for the `children` property on components that can be called more than once,
/// and provides a collection of all the children passed to this component.
pub type ChildrenFragmentFn = Arc<dyn Fn() -> Fragment + Send>;
pub type ChildrenFragmentFn = Arc<dyn Fn() -> Fragment<Dom> + Send>;
/// 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() -> AnyView + Send>;
pub type ChildrenFnMut = Box<dyn FnMut() -> AnyView<Dom> + Send>;
/// A type for the `children` property on components that can be called more than once,
/// but may mutate the children, and provides a collection of all the children
/// passed to this component.
pub type ChildrenFragmentMut = Box<dyn FnMut() -> Fragment + Send>;
pub type ChildrenFragmentMut = Box<dyn FnMut() -> Fragment<Dom> + Send>;
// This is to still support components that accept `Box<dyn Fn() -> AnyView>` as a children.
type BoxedChildrenFn = Box<dyn Fn() -> AnyView + Send>;
type BoxedChildrenFn = Box<dyn Fn() -> AnyView<Dom> + Send>;
/// This trait can be used when constructing a component that takes children without needing
/// to know exactly what children type the component expects. This is used internally by the
@@ -94,7 +97,7 @@ pub trait ToChildren<F> {
impl<F, C> ToChildren<F> for Children
where
F: FnOnce() -> C + Send + 'static,
C: RenderHtml + Send + 'static,
C: RenderHtml<Dom> + Send + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -105,7 +108,7 @@ where
impl<F, C> ToChildren<F> for ChildrenFn
where
F: Fn() -> C + Send + Sync + 'static,
C: RenderHtml + Send + 'static,
C: RenderHtml<Dom> + Send + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -116,7 +119,7 @@ where
impl<F, C> ToChildren<F> for ChildrenFnMut
where
F: Fn() -> C + Send + 'static,
C: RenderHtml + Send + 'static,
C: RenderHtml<Dom> + Send + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -127,7 +130,7 @@ where
impl<F, C> ToChildren<F> for BoxedChildrenFn
where
F: Fn() -> C + Send + 'static,
C: RenderHtml + Send + 'static,
C: RenderHtml<Dom> + Send + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -138,7 +141,7 @@ where
impl<F, C> ToChildren<F> for ChildrenFragment
where
F: FnOnce() -> C + Send + 'static,
C: IntoFragment,
C: IntoFragment<Dom>,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -149,7 +152,7 @@ where
impl<F, C> ToChildren<F> for ChildrenFragmentFn
where
F: Fn() -> C + Send + 'static,
C: IntoFragment,
C: IntoFragment<Dom>,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -160,7 +163,7 @@ where
impl<F, C> ToChildren<F> for ChildrenFragmentMut
where
F: FnMut() -> C + Send + 'static,
C: IntoFragment,
C: IntoFragment<Dom>,
{
#[inline]
fn to_children(mut f: F) -> Self {
@@ -171,7 +174,7 @@ where
/// New-type wrapper for a function that returns a view with `From` and `Default` traits implemented
/// to enable optional props in for example `<Show>` and `<Suspense>`.
#[derive(Clone)]
pub struct ViewFn(Arc<dyn Fn() -> AnyView + Send + Sync + 'static>);
pub struct ViewFn(Arc<dyn Fn() -> AnyView<Dom> + Send + Sync + 'static>);
impl Default for ViewFn {
fn default() -> Self {
@@ -182,7 +185,7 @@ impl Default for ViewFn {
impl<F, C> From<F> for ViewFn
where
F: Fn() -> C + Send + Sync + 'static,
C: RenderHtml + Send + 'static,
C: RenderHtml<Dom> + Send + 'static,
{
fn from(value: F) -> Self {
Self(Arc::new(move || value().into_any()))
@@ -191,14 +194,14 @@ where
impl ViewFn {
/// Execute the wrapped function
pub fn run(&self) -> AnyView {
pub fn run(&self) -> AnyView<Dom> {
(self.0)()
}
}
/// New-type wrapper for a function, which will only be called once and returns a view with `From` and
/// `Default` traits implemented to enable optional props in for example `<Show>` and `<Suspense>`.
pub struct ViewFnOnce(Box<dyn FnOnce() -> AnyView + Send + 'static>);
pub struct ViewFnOnce(Box<dyn FnOnce() -> AnyView<Dom> + Send + 'static>);
impl Default for ViewFnOnce {
fn default() -> Self {
@@ -209,7 +212,7 @@ impl Default for ViewFnOnce {
impl<F, C> From<F> for ViewFnOnce
where
F: FnOnce() -> C + Send + 'static,
C: RenderHtml + Send + 'static,
C: RenderHtml<Dom> + Send + 'static,
{
fn from(value: F) -> Self {
Self(Box::new(move || value().into_any()))
@@ -218,7 +221,7 @@ where
impl ViewFnOnce {
/// Execute the wrapped function
pub fn run(self) -> AnyView {
pub fn run(self) -> AnyView<Dom> {
(self.0)()
}
}

View File

@@ -4,16 +4,16 @@ use leptos_macro::component;
use reactive_graph::{
computed::ArcMemo,
effect::RenderEffect,
owner::{provide_context, Owner},
owner::Owner,
signal::ArcRwSignal,
traits::{Get, Update, With, WithUntracked},
};
use rustc_hash::FxHashMap;
use std::{fmt::Debug, sync::Arc};
use std::{fmt::Debug, marker::PhantomData, sync::Arc};
use tachys::{
html::attribute::Attribute,
hydration::Cursor,
reactive_graph::OwnedView,
renderer::Renderer,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
@@ -96,33 +96,27 @@ where
let hook = hook as Arc<dyn ErrorHook>;
let _guard = throw_error::set_error_hook(Arc::clone(&hook));
let children = children.into_inner()();
let owner = Owner::new();
let children = owner.with(|| {
provide_context(Arc::clone(&hook));
children.into_inner()()
});
OwnedView::new_with_owner(
ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children,
errors,
fallback,
},
owner,
)
ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children,
errors,
fallback,
rndr: PhantomData,
}
}
struct ErrorBoundaryView<Chil, FalFn> {
struct ErrorBoundaryView<Chil, FalFn, Rndr> {
hook: Arc<dyn ErrorHook>,
boundary_id: SerializedDataId,
errors_empty: ArcMemo<bool>,
children: Chil,
fallback: FalFn,
errors: ArcRwSignal<Errors>,
rndr: PhantomData<Rndr>,
}
struct ErrorBoundaryViewState<Chil, Fal> {
@@ -131,10 +125,11 @@ struct ErrorBoundaryViewState<Chil, Fal> {
fallback: Option<Fal>,
}
impl<Chil, Fal> Mountable for ErrorBoundaryViewState<Chil, Fal>
impl<Chil, Fal, Rndr> Mountable<Rndr> for ErrorBoundaryViewState<Chil, Fal>
where
Chil: Mountable,
Fal: Mountable,
Chil: Mountable<Rndr>,
Fal: Mountable<Rndr>,
Rndr: Renderer,
{
fn unmount(&mut self) {
if let Some(fallback) = &mut self.fallback {
@@ -144,11 +139,7 @@ where
}
}
fn mount(
&mut self,
parent: &tachys::renderer::types::Element,
marker: Option<&tachys::renderer::types::Node>,
) {
fn mount(&mut self, parent: &Rndr::Element, marker: Option<&Rndr::Node>) {
if let Some(fallback) = &mut self.fallback {
fallback.mount(parent, marker);
} else {
@@ -156,7 +147,7 @@ where
}
}
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
fn insert_before_this(&self, child: &mut dyn Mountable<Rndr>) -> bool {
if let Some(fallback) = &self.fallback {
fallback.insert_before_this(child)
} else {
@@ -165,11 +156,13 @@ where
}
}
impl<Chil, FalFn, Fal> Render for ErrorBoundaryView<Chil, FalFn>
impl<Chil, FalFn, Fal, Rndr> Render<Rndr>
for ErrorBoundaryView<Chil, FalFn, Rndr>
where
Chil: Render + 'static,
Chil: Render<Rndr> + 'static,
FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
Fal: Render + 'static,
Fal: Render<Rndr> + 'static,
Rndr: Renderer,
{
type State = RenderEffect<ErrorBoundaryViewState<Chil::State, Fal::State>>;
@@ -226,21 +219,26 @@ where
}
}
impl<Chil, FalFn, Fal> AddAnyAttr for ErrorBoundaryView<Chil, FalFn>
impl<Chil, FalFn, Fal, Rndr> AddAnyAttr<Rndr>
for ErrorBoundaryView<Chil, FalFn, Rndr>
where
Chil: RenderHtml + 'static,
Chil: RenderHtml<Rndr> + 'static,
FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
Fal: RenderHtml + Send + 'static,
Fal: RenderHtml<Rndr> + Send + 'static,
Rndr: Renderer,
{
type Output<SomeNewAttr: Attribute> =
ErrorBoundaryView<Chil::Output<SomeNewAttr::CloneableOwned>, FalFn>;
type Output<SomeNewAttr: Attribute<Rndr>> = ErrorBoundaryView<
Chil::Output<SomeNewAttr::CloneableOwned>,
FalFn,
Rndr,
>;
fn add_any_attr<NewAttr: Attribute>(
fn add_any_attr<NewAttr: Attribute<Rndr>>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
Self::Output<NewAttr>: RenderHtml<Rndr>,
{
let ErrorBoundaryView {
hook,
@@ -249,6 +247,7 @@ where
children,
fallback,
errors,
rndr,
} = self;
ErrorBoundaryView {
hook,
@@ -257,17 +256,20 @@ where
children: children.add_any_attr(attr.into_cloneable_owned()),
fallback,
errors,
rndr,
}
}
}
impl<Chil, FalFn, Fal> RenderHtml for ErrorBoundaryView<Chil, FalFn>
impl<Chil, FalFn, Fal, Rndr> RenderHtml<Rndr>
for ErrorBoundaryView<Chil, FalFn, Rndr>
where
Chil: RenderHtml + Send + 'static,
Chil: RenderHtml<Rndr> + Send + 'static,
FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
Fal: RenderHtml + Send + 'static,
Fal: RenderHtml<Rndr> + Send + 'static,
Rndr: Renderer,
{
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn, Rndr>;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
@@ -292,6 +294,7 @@ where
children: children.resolve().await,
fallback,
errors,
rndr: PhantomData,
}
}
@@ -365,7 +368,7 @@ where
fn hydrate<const FROM_SERVER: bool>(
mut self,
cursor: &Cursor,
cursor: &Cursor<Rndr>,
position: &PositionState,
) -> Self::State {
let mut children = Some(self.children);

View File

@@ -157,7 +157,7 @@ where
};
move || keyed(each(), key.clone(), children.clone())
}
/*
#[cfg(test)]
mod tests {
use crate::prelude::*;
@@ -168,7 +168,7 @@ mod tests {
fn creates_list() {
Owner::new().with(|| {
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
let list: View<HtmlElement<_, _, _>> = view! {
let list: View<HtmlElement<_, _, _, Dom>> = view! {
<ol>
<For each=move || values.get() key=|i| *i let:i>
<li>{i}</li>
@@ -187,7 +187,7 @@ mod tests {
fn creates_list_enumerate() {
Owner::new().with(|| {
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
let list: View<HtmlElement<_, _, _>> = view! {
let list: View<HtmlElement<_, _, _, Dom>> = view! {
<ol>
<ForEnumerate each=move || values.get() key=|i| *i let(index, i)>
<li>{move || index.get()}"-"{i}</li>
@@ -200,7 +200,7 @@ mod tests {
<!>-<!>4</li><li>4<!>-<!>5</li><!></ol>"
);
let list: View<HtmlElement<_, _, _>> = view! {
let list: View<HtmlElement<_, _, _, Dom>> = view! {
<ol>
<ForEnumerate each=move || values.get() key=|i| *i let(index, i)>
<li>{move || index.get()}"-"{i}</li>
@@ -216,4 +216,3 @@ mod tests {
});
}
}
*/

View File

@@ -1,8 +1,8 @@
(function (root, pkg_path, output_name, wasm_output_name) {
import(`${root}/${pkg_path}/${output_name}.js`)
(function (pkg_path, output_name, wasm_output_name) {
import(`/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
});
})
})
})

View File

@@ -1,4 +1,4 @@
((root, pkg_path, output_name, wasm_output_name) => {
((pkg_path, output_name, wasm_output_name) => {
function idle(c) {
if ("requestIdleCallback" in window) {
window.requestIdleCallback(c);
@@ -34,11 +34,11 @@
return { el: null, id: null, children: tree };
}
function hydrateIsland(el, id, mod) {
const islandFn = mod[id];
const islandFn = mod[`_island_${id}`];
if (islandFn) {
islandFn(el);
} else {
console.warn(`Could not find WASM function for the island ${id}.`);
console.warn(`Could not find WASM function for the island ${l}.`);
}
}
function hydrateIslands(entry, mod) {
@@ -50,9 +50,9 @@
}
}
idle(() => {
import(`${root}/${pkg_path}/${output_name}.js`)
import(`/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
hydrateIslands(islandTree(document.body, null), mod);
});

View File

@@ -38,9 +38,6 @@ pub fn AutoReload(
pub fn HydrationScripts(
options: LeptosOptions,
#[prop(optional)] islands: bool,
/// A base url, not including a trailing slash
#[prop(optional, into)]
root: Option<String>,
) -> impl IntoView {
let mut js_file_name = options.output_name.to_string();
let mut wasm_file_name = options.output_name.to_string();
@@ -59,10 +56,9 @@ pub fn HydrationScripts(
if !line.is_empty() {
if let Some((file, hash)) = line.split_once(':') {
if file == "js" {
js_file_name.push_str(&format!(".{}", hash.trim()));
js_file_name.push_str(&format!(".{}", hash));
} else if file == "wasm" {
wasm_file_name
.push_str(&format!(".{}", hash.trim()));
wasm_file_name.push_str(&format!(".{}", hash));
}
}
}
@@ -86,18 +82,17 @@ pub fn HydrationScripts(
include_str!("./hydration_script.js")
};
let root = root.unwrap_or_default();
view! {
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link rel="modulepreload" href=format!("/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
href=format!("/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?})")}
{format!("{script}({pkg_path:?}, {js_file_name:?}, {wasm_file_name:?})")}
</script>
}
}

View File

@@ -2,6 +2,7 @@ use std::borrow::Cow;
use tachys::{
html::attribute::Attribute,
hydration::Cursor,
renderer::{dom::Dom, Renderer},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
@@ -49,14 +50,14 @@ impl<T> View<T> {
pub trait IntoView
where
Self: Sized + Render + RenderHtml + Send,
Self: Sized + Render<Dom> + RenderHtml<Dom> + Send,
{
fn into_view(self) -> View<Self>;
}
impl<T> IntoView for T
where
T: Sized + Render + RenderHtml + Send, //+ AddAnyAttr,
T: Sized + Render<Dom> + RenderHtml<Dom> + Send, //+ AddAnyAttr<Dom>,
{
fn into_view(self) -> View<Self> {
View {
@@ -67,7 +68,7 @@ where
}
}
impl<T: Render> Render for View<T> {
impl<T: Render<Rndr>, Rndr: Renderer> Render<Rndr> for View<T> {
type State = T::State;
fn build(self) -> Self::State {
@@ -79,10 +80,10 @@ impl<T: Render> Render for View<T> {
}
}
impl<T: RenderHtml> RenderHtml for View<T> {
impl<T: RenderHtml<Rndr>, Rndr: Renderer> RenderHtml<Rndr> for View<T> {
type AsyncOutput = T::AsyncOutput;
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
const MIN_LENGTH: usize = <T as RenderHtml<Rndr>>::MIN_LENGTH;
async fn resolve(self) -> Self::AsyncOutput {
self.inner.resolve().await
@@ -146,7 +147,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor,
cursor: &Cursor<Rndr>,
position: &PositionState,
) -> Self::State {
self.inner.hydrate::<FROM_SERVER>(cursor, position)
@@ -165,15 +166,18 @@ impl<T: ToTemplate> ToTemplate for View<T> {
}
}
impl<T: AddAnyAttr> AddAnyAttr for View<T> {
type Output<SomeNewAttr: Attribute> = View<T::Output<SomeNewAttr>>;
impl<T: AddAnyAttr<Rndr>, Rndr> AddAnyAttr<Rndr> for View<T>
where
Rndr: Renderer,
{
type Output<SomeNewAttr: Attribute<Rndr>> = View<T::Output<SomeNewAttr>>;
fn add_any_attr<NewAttr: Attribute>(
fn add_any_attr<NewAttr: Attribute<Rndr>>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
Self::Output<NewAttr>: RenderHtml<Rndr>,
{
let View {
inner,

View File

@@ -168,15 +168,14 @@ pub mod prelude {
pub use leptos_server::*;
pub use oco_ref::*;
pub use reactive_graph::{
actions::*, computed::*, effect::*, graph::untrack, owner::*,
signal::*, wrappers::read::*,
actions::*, computed::*, effect::*, owner::*, signal::*,
wrappers::read::*,
};
pub use server_fn::{self, ServerFnError};
pub use tachys::{
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::{
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
},
self,
reactive_graph::{node_ref::*, Suspend},
view::template::ViewTemplate,
};
}
pub use export_types::*;
@@ -202,12 +201,10 @@ pub mod error {
pub use throw_error::*;
}
/// Control-flow components like `<Show>`, `<For>`, and `<Await>`.
/// Control-flow components like `<Show>` and `<For>`.
pub mod control_flow {
pub use crate::{animated_show::*, await_::*, for_loop::*, show::*};
pub use crate::{for_loop::*, show::*};
}
mod animated_show;
mod await_;
mod for_loop;
mod show;
@@ -233,7 +230,6 @@ mod suspense_component;
pub mod text_prop;
mod transition;
pub use leptos_macro::*;
#[doc(inline)]
pub use server_fn;
#[doc(hidden)]
pub use typed_builder;
@@ -241,22 +237,16 @@ pub use typed_builder;
pub use typed_builder_macro;
mod into_view;
pub use into_view::IntoView;
#[doc(inline)]
pub use leptos_dom;
mod provider;
#[doc(inline)]
pub use tachys;
/// Tools to mount an application to the DOM, or to hydrate it from server-rendered HTML.
pub mod mount;
#[doc(inline)]
pub use leptos_config as config;
#[doc(inline)]
pub use oco_ref as oco;
mod from_form_data;
#[doc(inline)]
pub use either_of as either;
#[doc(inline)]
pub use reactive_graph as reactive;
pub use reactive_graph;
/// Provide and access data along the reactive graph, sharing data without directly passing arguments.
pub mod context {
@@ -264,22 +254,17 @@ pub mod context {
pub use reactive_graph::owner::{provide_context, use_context};
}
#[doc(inline)]
pub use leptos_server as server;
/// HTML attribute types.
#[doc(inline)]
pub use tachys::html::attribute as attr;
/// HTML element types.
#[doc(inline)]
pub use tachys::html::element as html;
/// HTML event types.
#[doc(no_inline)]
pub use tachys::html::event as ev;
/// MathML element types.
#[doc(inline)]
pub use tachys::mathml as math;
/// SVG element types.
#[doc(inline)]
pub use tachys::svg;
/// Utilities for simple isomorphic logging to the console or terminal.
@@ -287,7 +272,7 @@ pub mod logging {
pub use leptos_dom::{debug_warn, error, log, warn};
}
pub mod task {
pub mod spawn {
pub use any_spawner::Executor;
use std::future::Future;
@@ -305,14 +290,9 @@ pub mod task {
Executor::spawn_local(fut)
}
/// Waits until the next "tick" of the current async executor.
pub async fn tick() {
Executor::tick().await
}
pub use reactive_graph::{
spawn_local_scoped, spawn_local_scoped_with_cancellation,
};
}
// these reexports are used in islands
@@ -329,3 +309,234 @@ pub use tracing;
pub use wasm_bindgen;
#[doc(hidden)]
pub use web_sys;
/*mod additional_attributes;
pub use additional_attributes::*;
mod await_;
pub use await_::*;
pub use leptos_config::{self, get_configuration, LeptosOptions};
#[cfg(not(all(
target_arch = "wasm32",
any(feature = "csr", feature = "hydrate")
)))]
/// Utilities for server-side rendering HTML.
pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
pub use leptos_dom::{
self, create_node_ref, document, ev,
helpers::{
event_target, event_target_checked, event_target_value,
request_animation_frame, request_animation_frame_with_handle,
request_idle_callback, request_idle_callback_with_handle, set_interval,
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener, window_event_listener_untyped,
},
html,
html::Binding,
math, mount_to, mount_to_body, nonce, svg, window, Attribute, Class,
CollectView, Errors, EventHandlerFn, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
};
/// Types to make it easier to handle errors in your application.
pub mod error {
pub use server_fn::error::{Error, Result};
}
#[cfg(all(target_arch = "wasm32", feature = "template_macro"))]
pub use leptos_macro::template;
#[cfg(not(all(target_arch = "wasm32", feature = "template_macro")))]
pub use leptos_macro::view as template;
pub use leptos_macro::{component, island, slice, slot, view, Params};
cfg_if::cfg_if!(
if #[cfg(feature="spin")] {
pub use leptos_spin_macro::server;
} else {
pub use leptos_macro::server;
}
);
pub use leptos_reactive::*;
pub use leptos_server::{
self, create_action, create_multi_action, create_server_action,
create_server_multi_action, Action, MultiAction, ServerFnError,
ServerFnErrorErr,
};
pub use server_fn::{self, ServerFn as _};
mod error_boundary;
pub use error_boundary::*;
mod animated_show;
mod for_loop;
mod provider;
mod show;
pub use animated_show::*;
pub use for_loop::*;
pub use provider::*;
#[cfg(feature = "experimental-islands")]
pub use serde;
#[cfg(feature = "experimental-islands")]
pub use serde_json;
pub use show::*;
//pub use suspense_component::*;
mod suspense_component;
//mod transition;
#[cfg(feature = "tracing")]
#[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;
#[doc(hidden)]
#[cfg(any(
feature = "csr",
feature = "hydrate",
feature = "template_macro"
))]
pub use wasm_bindgen; // used in islands
#[doc(hidden)]
#[cfg(any(
feature = "csr",
feature = "hydrate",
feature = "template_macro"
))]
pub use web_sys; // used in islands
mod children;
mod portal;
mod view_fn;
pub use children::*;
pub use portal::*;
pub use view_fn::*;
extern crate self as leptos;
/// A type for taking anything that implements [`IntoAttribute`].
///
/// ```rust
/// use leptos::*;
///
/// #[component]
/// pub fn MyHeading(
/// text: String,
/// #[prop(optional, into)] class: Option<AttributeValue>,
/// ) -> impl IntoView {
/// view! {
/// <h1 class=class>{text}</h1>
/// }
/// }
/// ```
pub type AttributeValue = Box<dyn IntoAttribute>;
#[doc(hidden)]
pub trait Component<P> {}
#[doc(hidden)]
pub trait Props {
type Builder;
fn builder() -> Self::Builder;
}
#[doc(hidden)]
pub trait DynAttrs {
fn dyn_attrs(self, _args: Vec<(&'static str, Attribute)>) -> Self
where
Self: Sized,
{
self
}
}
impl DynAttrs for () {}
#[doc(hidden)]
pub trait DynBindings {
fn dyn_bindings<B: Into<Binding>>(
self,
_args: impl IntoIterator<Item = B>,
) -> Self
where
Self: Sized,
{
self
}
}
impl DynBindings for () {}
#[doc(hidden)]
pub trait PropsOrNoPropsBuilder {
type Builder;
fn builder_or_not() -> Self::Builder;
}
#[doc(hidden)]
#[derive(Copy, Clone, Debug, Default)]
pub struct EmptyPropsBuilder {}
impl EmptyPropsBuilder {
pub fn build(self) {}
}
impl<P: Props> PropsOrNoPropsBuilder for P {
type Builder = <P as Props>::Builder;
fn builder_or_not() -> Self::Builder {
Self::builder()
}
}
impl PropsOrNoPropsBuilder for EmptyPropsBuilder {
type Builder = EmptyPropsBuilder;
fn builder_or_not() -> Self::Builder {
EmptyPropsBuilder {}
}
}
impl<F, R> Component<EmptyPropsBuilder> for F where F: FnOnce() -> R {}
impl<P, F, R> Component<P> for F
where
F: FnOnce(P) -> R,
P: Props,
{
}
#[doc(hidden)]
pub fn component_props_builder<P: PropsOrNoPropsBuilder>(
_f: &impl Component<P>,
) -> <P as PropsOrNoPropsBuilder>::Builder {
<P as PropsOrNoPropsBuilder>::builder_or_not()
}
#[doc(hidden)]
pub fn component_view<P>(f: impl ComponentConstructor<P>, props: P) -> View {
f.construct(props)
}
#[doc(hidden)]
pub trait ComponentConstructor<P> {
fn construct(self, props: P) -> View;
}
impl<Func, V> ComponentConstructor<()> for Func
where
Func: FnOnce() -> V,
V: IntoView,
{
fn construct(self, (): ()) -> View {
(self)().into_view()
}
}
impl<Func, V, P> ComponentConstructor<P> for Func
where
Func: FnOnce(P) -> V,
V: IntoView,
P: PropsOrNoPropsBuilder,
{
fn construct(self, props: P) -> View {
(self)(props).into_view()
}
}*/

View File

@@ -5,8 +5,10 @@ use any_spawner::Executor;
use reactive_graph::owner::Owner;
#[cfg(debug_assertions)]
use std::cell::Cell;
use std::marker::PhantomData;
use tachys::{
dom::body,
renderer::{dom::Dom, Renderer},
view::{Mountable, Render},
};
#[cfg(feature = "hydrate")]
@@ -36,7 +38,10 @@ thread_local! {
#[cfg(feature = "hydrate")]
/// Runs the provided closure and mounts the result to the provided element.
pub fn hydrate_from<F, N>(parent: HtmlElement, f: F) -> UnmountHandle<N::State>
pub fn hydrate_from<F, N>(
parent: HtmlElement,
f: F,
) -> UnmountHandle<N::State, Dom>
where
F: FnOnce() -> N + 'static,
N: IntoView,
@@ -80,7 +85,11 @@ where
// returns a handle that owns the owner
// when this is dropped, it will clean up the reactive system and unmount the view
UnmountHandle { owner, mountable }
UnmountHandle {
owner,
mountable,
rndr: PhantomData,
}
}
/// Runs the provided closure and mounts the result to the `<body>`.
@@ -94,7 +103,7 @@ where
}
/// Runs the provided closure and mounts the result to the provided element.
pub fn mount_to<F, N>(parent: HtmlElement, f: F) -> UnmountHandle<N::State>
pub fn mount_to<F, N>(parent: HtmlElement, f: F) -> UnmountHandle<N::State, Dom>
where
F: FnOnce() -> N + 'static,
N: IntoView,
@@ -131,17 +140,22 @@ where
// returns a handle that owns the owner
// when this is dropped, it will clean up the reactive system and unmount the view
UnmountHandle { owner, mountable }
UnmountHandle {
owner,
mountable,
rndr: PhantomData,
}
}
/// Runs the provided closure and mounts the result to the provided element.
pub fn mount_to_renderer<F, N>(
parent: &tachys::renderer::types::Element,
pub fn mount_to_renderer<F, N, R>(
parent: &R::Element,
f: F,
) -> UnmountHandle<N::State>
) -> UnmountHandle<N::State, R>
where
F: FnOnce() -> N + 'static,
N: Render,
N: Render<R>,
R: Renderer,
{
// use wasm-bindgen-futures to drive the reactive system
// we ignore the return value because an Err here just means the wasm-bindgen executor is
@@ -159,7 +173,11 @@ where
// returns a handle that owns the owner
// when this is dropped, it will clean up the reactive system and unmount the view
UnmountHandle { owner, mountable }
UnmountHandle {
owner,
mountable,
rndr: PhantomData,
}
}
/// Hydrates any islands that are currently present on the page.
@@ -193,18 +211,21 @@ pub fn hydrate_islands() {
reactive system. You should either call `.forget()` to keep the \
view permanently mounted, or store the `UnmountHandle` somewhere \
and drop it when you'd like to unmount the view."]
pub struct UnmountHandle<M>
pub struct UnmountHandle<M, R>
where
M: Mountable,
M: Mountable<R>,
R: Renderer,
{
#[allow(dead_code)]
owner: Owner,
mountable: M,
rndr: PhantomData<R>,
}
impl<M> UnmountHandle<M>
impl<M, R> UnmountHandle<M, R>
where
M: Mountable,
M: Mountable<R>,
R: Renderer,
{
/// Leaks the handle, preventing the reactive system from being cleaned up and the view from
/// being unmounted. This should always be called when [`mount_to`] is used for the root of an
@@ -214,9 +235,10 @@ where
}
}
impl<M> Drop for UnmountHandle<M>
impl<M, R> Drop for UnmountHandle<M, R>
where
M: Mountable,
M: Mountable<R>,
R: Renderer,
{
fn drop(&mut self) {
self.mountable.unmount();

View File

@@ -6,7 +6,7 @@ use base64::{
};
use rand::{thread_rng, RngCore};
use std::{fmt::Display, ops::Deref, sync::Arc};
use tachys::html::attribute::AttributeValue;
use tachys::{html::attribute::AttributeValue, renderer::Renderer};
/// A cryptographic nonce ("number used once") which can be
/// used by Content Security Policy to determine whether or not a given
@@ -65,9 +65,12 @@ impl Display for Nonce {
}
}
impl AttributeValue for Nonce {
impl<R> AttributeValue<R> for Nonce
where
R: Renderer,
{
type AsyncOutput = Self;
type State = <Arc<str> as AttributeValue>::State;
type State = <Arc<str> as AttributeValue<R>>::State;
type Cloneable = Self;
type CloneableOwned = Self;
@@ -76,7 +79,7 @@ impl AttributeValue for Nonce {
}
fn to_html(self, key: &str, buf: &mut String) {
<Arc<str> as AttributeValue>::to_html(self.0, key, buf)
<Arc<str> as AttributeValue<R>>::to_html(self.0, key, buf)
}
fn to_template(_key: &str, _buf: &mut String) {}
@@ -84,21 +87,17 @@ impl AttributeValue for Nonce {
fn hydrate<const FROM_SERVER: bool>(
self,
key: &str,
el: &tachys::renderer::types::Element,
el: &<R as Renderer>::Element,
) -> Self::State {
<Arc<str> as AttributeValue>::hydrate::<FROM_SERVER>(self.0, key, el)
<Arc<str> as AttributeValue<R>>::hydrate::<FROM_SERVER>(self.0, key, el)
}
fn build(
self,
el: &tachys::renderer::types::Element,
key: &str,
) -> Self::State {
<Arc<str> as AttributeValue>::build(self.0, el, key)
fn build(self, el: &<R as Renderer>::Element, key: &str) -> Self::State {
<Arc<str> as AttributeValue<R>>::build(self.0, el, key)
}
fn rebuild(self, key: &str, state: &mut Self::State) {
<Arc<str> as AttributeValue>::rebuild(self.0, key, state)
<Arc<str> as AttributeValue<R>>::rebuild(self.0, key, state)
}
fn into_cloneable(self) -> Self::Cloneable {

View File

@@ -1,7 +1,7 @@
use crate::{children::TypedChildrenFn, mount, IntoView};
use leptos_dom::helpers::document;
use leptos_macro::component;
use reactive_graph::{effect::Effect, graph::untrack, owner::Owner};
use reactive_graph::{effect::Effect, owner::Owner, untrack};
use std::sync::Arc;
/// Renders components somewhere else in the DOM.

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