Compare commits

..

3 Commits

Author SHA1 Message Date
benwis
7789d58e7b v0.6.14 2024-08-13 19:16:17 -07:00
Saber Haj Rabiee
4c6f3d067f fix: update wasm-bindgen and web-sys for leptos_0.6 (#2830) 2024-08-13 16:19:24 -07:00
Greg Johnston
0388903110 fix: support complex struct form submits without JS (closes #2790) 2024-08-07 09:43:19 -04:00
761 changed files with 50679 additions and 71575 deletions

View File

@@ -1,13 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "cargo"
directories:
- "/"
- "/examples/*"
- "/benchmarks"
schedule:
interval: "daily"

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,25 +1,28 @@
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)
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
name: Run semver check (nightly-2024-07-21)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Semver Checks
uses: obi1kenobi/cargo-semver-checks-action@v2
with:
rust-toolchain: nightly-2024-08-01
rust-toolchain: nightly-2024-07-21

View File

@@ -1,27 +1,43 @@
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:
[
integrations/actix,
integrations/axum,
integrations/utils,
leptos,
leptos_config,
leptos_dom,
leptos_hot_reload,
leptos_macro,
leptos_reactive,
leptos_server,
meta,
router,
server_fn,
server_fn/server_fn_macro_default,
server_fn_macro,
]
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly-2024-08-01
toolchain: nightly-2024-07-21

View File

@@ -26,7 +26,7 @@ jobs:
- name: Get example project directories that changed
id: changed-dirs
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@v41
with:
dir_names: true
dir_names_max_depth: "2"

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,17 +18,21 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get example files that changed
id: changed-files
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@v43
with:
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,28 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@v43
with:
files_ignore: |
.*/**/*
cargo-make/**/*
examples/**/*
projects/**/*
benchmarks/**/*
docs/**/*
files: |
integrations/**
leptos/**
leptos_config/**
leptos_dom/**
leptos_hot_reload/**
leptos_macro/**
leptos_reactive/**
leptos_server/**
meta/**
router/**
server_fn/**
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
- uses: pnpm/action-setup@v3
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@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

@@ -3,28 +3,18 @@ resolver = "2"
members = [
# utilities
"oco",
"any_spawner",
"const_str_slice_concat",
"either_of",
"next_tuple",
"oco",
"or_poisoned",
# core
"hydration_context",
"leptos",
"leptos_dom",
"leptos_config",
"leptos_hot_reload",
"leptos_macro",
"leptos_reactive",
"leptos_server",
"reactive_graph",
"reactive_stores",
"reactive_stores_macro",
"server_fn",
"server_fn_macro",
"server_fn/server_fn_macro_default",
"tachys",
# integrations
"integrations/actix",
@@ -34,42 +24,28 @@ members = [
# libraries
"meta",
"router",
"router_macro",
"any_error",
]
exclude = ["benchmarks", "examples", "projects"]
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.7.0-gamma"
edition = "2021"
rust-version = "1.76"
version = "0.6.14"
rust-version = "1.75"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0-gamma" }
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-gamma" }
leptos = { path = "./leptos", version = "0.7.0-gamma" }
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma" }
leptos_router = { path = "./router", version = "0.7.0-gamma" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma" }
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma" }
leptos_meta = { path = "./meta", version = "0.7.0-gamma" }
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma" }
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-gamma" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma" }
server_fn = { path = "./server_fn", version = "0.7.0-gamma" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma" }
tachys = { path = "./tachys", version = "0.1.0-gamma" }
oco_ref = { path = "./oco", version = "0.1.0" }
leptos = { path = "./leptos", version = "0.6.14" }
leptos_dom = { path = "./leptos_dom", version = "0.6.14" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.14" }
leptos_macro = { path = "./leptos_macro", version = "0.6.14" }
leptos_reactive = { path = "./leptos_reactive", version = "0.6.14" }
leptos_server = { path = "./leptos_server", version = "0.6.14" }
server_fn = { path = "./server_fn", version = "0.6.14" }
server_fn_macro = { path = "./server_fn_macro", version = "0.6.14" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
leptos_config = { path = "./leptos_config", version = "0.6.14" }
leptos_router = { path = "./router", version = "0.6.14" }
leptos_meta = { path = "./meta", version = "0.6.14" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.14" }
[profile.release]
codegen-units = 1

View File

@@ -12,8 +12,6 @@
You can find a list of useful libraries and example projects at [`awesome-leptos`](https://github.com/leptos-rs/awesome-leptos).
# The `main` branch is currently undergoing major changes in preparation for the [0.7](https://github.com/leptos-rs/leptos/milestone/4) release. For a stable version, please use the [v0.6.13 tag](https://github.com/leptos-rs/leptos/tree/v0.6.13)
# Leptos
```rust

40
TODO.md
View File

@@ -1,40 +0,0 @@
- core examples
- [x] counter
- [x] counters
- [x] fetch
- [x] todomvc
- [x] error_boundary
- [x] parent\_child
- [x] on: on components
- [ ] router
- [ ] slots
- [ ] hackernews
- [ ] counter\_isomorphic
- [ ] todo\_app\_sqlite
- other ssr examples
- [ ] error boundary SSR
- reactivity
- Signal wrappers
- SignalDispose implementations on all Copy types
- untracked access warnings
- ErrorBoundary
- [ ] RenderHtml implementation
- [ ] Separate component?
- Suspense/Transition components?
- callbacks
- unsync StoredValue
- SSR
- escaping HTML correctly (attributes + text nodes)
- router
- nested routes
- trailing slashes
- \_meta package (and use in hackernews)
- integrations
- update tests
- hackernews example
- TODOs
- Suspense/Transition/Await components
- nicer routing components
- async routing (waiting for data to load before navigation)
- `<A>` component
- figure out rebuilding issues: list (needs new signal IDs) vs. regular rebuild

View File

@@ -1,13 +0,0 @@
[package]
name = "throw_error"
version = "0.2.0-gamma"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Utilities for wrapping, throwing, and catching errors."
rust-version.workspace = true
edition.workspace = true
[dependencies]
pin-project-lite = "0.2.14"

View File

@@ -1,2 +0,0 @@
A utility library for wrapping arbitrary errors, and for “throwing” errors in a way
that can be caught by user-defined error hooks.

View File

@@ -1,165 +0,0 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
//! A utility library for wrapping arbitrary errors, and for “throwing” errors in a way
//! that can be caught by user-defined error hooks.
use std::{
cell::RefCell,
error,
fmt::{self, Display},
future::Future,
mem, ops,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
/* Wrapper Types */
/// This is a result type into which any error can be converted.
///
/// Results are stored as [`Error`].
pub type Result<T, E = Error> = core::result::Result<T, E>;
/// A generic wrapper for any error.
#[derive(Debug, Clone)]
#[repr(transparent)]
pub struct Error(Arc<dyn error::Error + Send + Sync>);
impl Error {
/// Converts the wrapper into the inner reference-counted error.
pub fn into_inner(self) -> Arc<dyn error::Error + Send + Sync> {
Arc::clone(&self.0)
}
}
impl ops::Deref for Error {
type Target = Arc<dyn error::Error + Send + Sync>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<T> From<T> for Error
where
T: error::Error + Send + Sync + 'static,
{
fn from(value: T) -> Self {
Error(Arc::new(value))
}
}
/// Implements behavior that allows for global or scoped error handling.
///
/// This allows for both "throwing" errors to register them, and "clearing" errors when they are no
/// longer valid. This is useful for something like a user interface, in which an error can be
/// "thrown" on some invalid user input, and later "cleared" if the user corrects the input.
/// Keeping a unique identifier for each error allows the UI to be updated accordingly.
pub trait ErrorHook: Send + Sync {
/// Handles the given error, returning a unique identifier.
fn throw(&self, error: Error) -> ErrorId;
/// Clears the error associated with the given identifier.
fn clear(&self, id: &ErrorId);
}
/// A unique identifier for an error. This is returned when you call [`throw`], which calls a
/// global error handler.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Default)]
pub struct ErrorId(usize);
impl Display for ErrorId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl From<usize> for ErrorId {
fn from(value: usize) -> Self {
Self(value)
}
}
thread_local! {
static ERROR_HOOK: RefCell<Option<Arc<dyn ErrorHook>>> = RefCell::new(None);
}
/// Resets the error hook to its previous state when dropped.
pub struct ResetErrorHookOnDrop(Option<Arc<dyn ErrorHook>>);
impl Drop for ResetErrorHookOnDrop {
fn drop(&mut self) {
ERROR_HOOK.with_borrow_mut(|this| *this = self.0.take())
}
}
/// Returns the current error hook.
pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
ERROR_HOOK.with_borrow(Clone::clone)
}
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
ResetErrorHookOnDrop(
ERROR_HOOK.with_borrow_mut(|this| mem::replace(this, Some(hook))),
)
}
/// Invokes the error hook set by [`set_error_hook`] with the given error.
pub fn throw(error: impl Into<Error>) -> ErrorId {
ERROR_HOOK
.with_borrow(|hook| hook.as_ref().map(|hook| hook.throw(error.into())))
.unwrap_or_default()
}
/// Clears the given error from the current error hook.
pub fn clear(id: &ErrorId) {
ERROR_HOOK
.with_borrow(|hook| hook.as_ref().map(|hook| hook.clear(id)))
.unwrap_or_default()
}
pin_project_lite::pin_project! {
/// A [`Future`] that reads the error hook that is set when it is created, and sets this as the
/// current error hook whenever it is polled.
pub struct ErrorHookFuture<Fut> {
hook: Option<Arc<dyn ErrorHook>>,
#[pin]
inner: Fut
}
}
impl<Fut> ErrorHookFuture<Fut> {
/// Reads the current hook and wraps the given [`Future`], returning a new `Future` that will
/// set the error hook whenever it is polled.
pub fn new(inner: Fut) -> Self {
Self {
hook: ERROR_HOOK.with_borrow(Clone::clone),
inner,
}
}
}
impl<Fut> Future for ErrorHookFuture<Fut>
where
Fut: Future,
{
type Output = Fut::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let _hook = this
.hook
.as_ref()
.map(|hook| set_error_hook(Arc::clone(hook)));
this.inner.poll(cx)
}
}

View File

@@ -1,33 +0,0 @@
[package]
name = "any_spawner"
version = "0.1.1"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Spawn asynchronous tasks in an executor-independent way."
edition.workspace = true
[dependencies]
futures = "0.3.30"
glib = { version = "0.20.0", optional = true }
thiserror = "1.0"
tokio = { version = "1.39", optional = true, default-features = false, features = [
"rt",
] }
tracing = { version = "0.1.40", optional = true }
wasm-bindgen-futures = { version = "0.4.42", optional = true }
[features]
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"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

View File

@@ -1,26 +0,0 @@
This crate makes it easier to write asynchronous code that is executor-agnostic, by providing a
utility that can be used to spawn tasks in a variety of executors.
It only supports single executor per program, but that executor can be set at runtime, anywhere
in your crate (or an application that depends on it).
This can be extended to support any executor or runtime that supports spawning [`Future`]s.
This is a least common denominator implementation in many ways. Limitations include:
- setting an executor is a one-time, global action
- no "join handle" or other result is returned from the spawn
- the `Future` must output `()`
```rust
use any_spawner::Executor;
Executor::init_futures_executor()
.expect("executor should only be initialized once");
// spawn a thread-safe Future
Executor::spawn(async { /* ... */ });
// spawn a Future that is !Send
Executor::spawn_local(async { /* ... */ });
```

View File

@@ -1,245 +0,0 @@
//! This crate makes it easier to write asynchronous code that is executor-agnostic, by providing a
//! utility that can be used to spawn tasks in a variety of executors.
//!
//! It only supports single executor per program, but that executor can be set at runtime, anywhere
//! in your crate (or an application that depends on it).
//!
//! This can be extended to support any executor or runtime that supports spawning [`Future`]s.
//!
//! This is a least common denominator implementation in many ways. Limitations include:
//! - setting an executor is a one-time, global action
//! - no "join handle" or other result is returned from the spawn
//! - the `Future` must output `()`
//!
//! ```rust
//! use any_spawner::Executor;
//!
//! // make sure an Executor has been initialized with one of the init_ functions
//!
//! # if false {
//! // spawn a thread-safe Future
//! Executor::spawn(async { /* ... */ });
//!
//! // spawn a Future that is !Send
//! Executor::spawn_local(async { /* ... */ });
//! # }
//! ```
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use std::{future::Future, pin::Pin, sync::OnceLock};
use thiserror::Error;
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();
/// Errors that can occur when using the executor.
#[derive(Error, Debug)]
pub enum ExecutorError {
/// The executor has already been set.
#[error("Executor has already been set.")]
AlreadySet,
}
/// A global async executor that can spawn tasks.
pub struct Executor;
impl Executor {
/// Spawns a thread-safe [`Future`].
/// ```rust
/// use any_spawner::Executor;
/// # if false {
/// // spawn a thread-safe Future
/// Executor::spawn(async { /* ... */ });
/// # }
/// ```
#[track_caller]
pub fn spawn(fut: impl Future<Output = ()> + Send + 'static) {
if let Some(spawner) = SPAWN.get() {
spawner(Box::pin(fut))
} else {
#[cfg(all(debug_assertions, feature = "tracing"))]
tracing::error!(
"At {}, tried to spawn a Future with Executor::spawn() before \
the Executor had been set.",
std::panic::Location::caller()
);
#[cfg(all(debug_assertions, not(feature = "tracing")))]
panic!(
"At {}, tried to spawn a Future with Executor::spawn() before \
the Executor had been set.",
std::panic::Location::caller()
);
}
}
/// Spawns a [`Future`] that cannot be sent across threads.
/// ```rust
/// use any_spawner::Executor;
///
/// # if false {
/// // spawn a thread-safe Future
/// Executor::spawn_local(async { /* ... */ });
/// # }
/// ```
#[track_caller]
pub fn spawn_local(fut: impl Future<Output = ()> + 'static) {
if let Some(spawner) = SPAWN_LOCAL.get() {
spawner(Box::pin(fut))
} else {
#[cfg(all(debug_assertions, feature = "tracing"))]
tracing::error!(
"At {}, tried to spawn a Future with Executor::spawn_local() \
before the Executor had been set.",
std::panic::Location::caller()
);
#[cfg(all(debug_assertions, not(feature = "tracing")))]
panic!(
"At {}, tried to spawn a Future with Executor::spawn_local() \
before the Executor had been set.",
std::panic::Location::caller()
);
}
}
/// Waits until the next "tick" of the current async executor.
pub async fn tick() {
let (tx, rx) = futures::channel::oneshot::channel();
Executor::spawn(async move {
_ = tx.send(());
});
_ = rx.await;
}
}
impl Executor {
/// Globally sets the [`tokio`] runtime as the executor used to spawn tasks.
///
/// Returns `Err(_)` if an executor has already been set.
///
/// Requires the `tokio` feature to be activated on this crate.
#[cfg(feature = "tokio")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
pub fn init_tokio() -> Result<(), ExecutorError> {
SPAWN
.set(|fut| {
tokio::spawn(fut);
})
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
tokio::task::spawn_local(fut);
})
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
/// Globally sets the [`wasm-bindgen-futures`] runtime as the executor used to spawn tasks.
///
/// Returns `Err(_)` if an executor has already been set.
///
/// Requires the `wasm-bindgen` feature to be activated on this crate.
#[cfg(feature = "wasm-bindgen")]
#[cfg_attr(docsrs, doc(cfg(feature = "wasm-bindgen")))]
pub fn init_wasm_bindgen() -> Result<(), ExecutorError> {
SPAWN
.set(|fut| {
wasm_bindgen_futures::spawn_local(fut);
})
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
wasm_bindgen_futures::spawn_local(fut);
})
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
/// Globally sets the [`glib`] runtime as the executor used to spawn tasks.
///
/// Returns `Err(_)` if an executor has already been set.
///
/// Requires the `glib` feature to be activated on this crate.
#[cfg(feature = "glib")]
#[cfg_attr(docsrs, doc(cfg(feature = "glib")))]
pub fn init_glib() -> Result<(), ExecutorError> {
SPAWN
.set(|fut| {
let main_context = glib::MainContext::default();
main_context.spawn(fut);
})
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
let main_context = glib::MainContext::default();
main_context.spawn_local(fut);
})
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
/// Globally sets the [`futures`] 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 `futures-executor` feature to be activated on this crate.
#[cfg(feature = "futures-executor")]
#[cfg_attr(docsrs, doc(cfg(feature = "futures-executor")))]
pub fn init_futures_executor() -> Result<(), ExecutorError> {
use futures::{
executor::{LocalPool, ThreadPool},
task::{LocalSpawnExt, SpawnExt},
};
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
thread_local! {
static LOCAL_POOL: LocalPool = LocalPool::new();
}
fn get_thread_pool() -> &'static ThreadPool {
THREAD_POOL.get_or_init(|| {
ThreadPool::new()
.expect("could not create futures executor ThreadPool")
})
}
SPAWN
.set(|fut| {
get_thread_pool()
.spawn(fut)
.expect("failed to spawn future");
})
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
LOCAL_POOL.with(|pool| {
let spawner = pool.spawner();
spawner.spawn_local(fut).expect("failed to spawn future");
});
})
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
}
#[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

@@ -2,36 +2,35 @@
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 = [
"nightly",
"ssr",
"nightly",
"ssr",
] }
leptos = { path = "../leptos", features = ["ssr", "nightly"] }
leptos_reactive = { path = "../leptos_reactive", features = ["ssr", "nightly"] }
tachydom = { git = "https://github.com/gbj/tachys", features = [
"nightly",
"leptos",
"nightly",
"leptos",
] }
tachy_maccy = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
sycamore = { version = "0.8.0", features = ["ssr"] }
yew = { version = "0.20.0", features = ["ssr"] }
tokio-test = "0.4.0"
miniserde = "0.1.0"
gloo = "0.8.0"
uuid = { version = "1.0", features = ["serde", "v4", "wasm-bindgen"] }
wasm-bindgen = "0.2.0"
lazy_static = "1.0"
log = "0.4.0"
strum = "0.24.0"
strum_macros = "0.24.0"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
tera = "1.0"
sycamore = { version = "0.8", features = ["ssr"] }
yew = { version = "0.20", features = ["ssr"] }
tokio-test = "0.4"
miniserde = "0.1"
gloo = "0.8"
uuid = { version = "1", features = ["serde", "v4", "wasm-bindgen"] }
wasm-bindgen = "0.2"
lazy_static = "1"
log = "0.4"
strum = "0.24"
strum_macros = "0.24"
serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
tera = "1"
[dependencies.web-sys]
version = "0.3.0"
version = "0.3"
features = ["Window", "Document", "HtmlElement", "HtmlInputElement"]

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

@@ -1,12 +0,0 @@
[package]
name = "const_str_slice_concat"
version = "0.1.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Utilities for const concatenation of string slices."
rust-version.workspace = true
edition.workspace = true
[dependencies]

View File

@@ -1 +0,0 @@
extend = { path = "../cargo-make/main.toml" }

View File

@@ -1,139 +0,0 @@
#![no_std]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
//! Utilities for const concatenation of string slices.
pub(crate) const MAX_TEMPLATE_SIZE: usize = 4096;
/// Converts a zero-terminated buffer of bytes into a UTF-8 string.
pub const fn str_from_buffer(buf: &[u8; MAX_TEMPLATE_SIZE]) -> &str {
match core::ffi::CStr::from_bytes_until_nul(buf) {
Ok(cstr) => match cstr.to_str() {
Ok(str) => str,
Err(_) => panic!("TEMPLATE FAILURE"),
},
Err(_) => panic!("TEMPLATE FAILURE"),
}
}
/// Concatenates any number of static strings into a single array.
// credit to Rainer Stropek, "Constant fun," Rust Linz, June 2022
pub const fn const_concat(
strs: &'static [&'static str],
) -> [u8; MAX_TEMPLATE_SIZE] {
let mut buffer = [0; MAX_TEMPLATE_SIZE];
let mut position = 0;
let mut remaining = strs;
while let [current, tail @ ..] = remaining {
let x = current.as_bytes();
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable refernces in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;
i += 1;
}
remaining = tail;
}
buffer
}
/// Converts a zero-terminated buffer of bytes into a UTF-8 string with the given prefix.
pub const fn const_concat_with_prefix(
strs: &'static [&'static str],
prefix: &'static str,
suffix: &'static str,
) -> [u8; MAX_TEMPLATE_SIZE] {
let mut buffer = [0; MAX_TEMPLATE_SIZE];
let mut position = 0;
let mut remaining = strs;
while let [current, tail @ ..] = remaining {
let x = current.as_bytes();
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable refernces in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;
i += 1;
}
remaining = tail;
}
if buffer[0] == 0 {
buffer
} else {
let mut new_buf = [0; MAX_TEMPLATE_SIZE];
let prefix = prefix.as_bytes();
let suffix = suffix.as_bytes();
let mut position = 0;
let mut i = 0;
while i < prefix.len() {
new_buf[position] = prefix[i];
position += 1;
i += 1;
}
i = 0;
while i < buffer.len() {
if buffer[i] == 0 {
break;
}
new_buf[position] = buffer[i];
position += 1;
i += 1;
}
i = 0;
while i < suffix.len() {
new_buf[position] = suffix[i];
position += 1;
i += 1;
}
new_buf
}
}
/// Converts any number of strings into a UTF-8 string, separated by the given string.
pub const fn const_concat_with_separator(
strs: &[&str],
separator: &'static str,
) -> [u8; MAX_TEMPLATE_SIZE] {
let mut buffer = [0; MAX_TEMPLATE_SIZE];
let mut position = 0;
let mut remaining = strs;
while let [current, tail @ ..] = remaining {
let x = current.as_bytes();
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable refernces in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;
i += 1;
}
if !x.is_empty() {
let mut position = 0;
let separator = separator.as_bytes();
while i < separator.len() {
buffer[position] = separator[i];
position += 1;
i += 1;
}
}
remaining = tail;
}
buffer
}

View File

@@ -1,13 +0,0 @@
[package]
name = "either_of"
version = "0.1.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Utilities for working with enumerated types that contain one of 2..n other types."
rust-version.workspace = true
edition.workspace = true
[dependencies]
pin-project-lite = "0.2.14"

View File

@@ -1 +0,0 @@
extend = { path = "../cargo-make/main.toml" }

View File

@@ -1 +0,0 @@
Utilities for working with enumerated types that contain one of `2..n` other types.

View File

@@ -1,236 +0,0 @@
#![no_std]
#![forbid(unsafe_code)]
//! Utilities for working with enumerated types that contain one of `2..n` other types.
use core::{
fmt::Display,
future::Future,
pin::Pin,
task::{Context, Poll},
};
use pin_project_lite::pin_project;
#[derive(Debug, Clone, Copy)]
pub enum Either<A, B> {
Left(A),
Right(B),
}
impl<Item, A, B> Iterator for Either<A, B>
where
A: Iterator<Item = Item>,
B: Iterator<Item = Item>,
{
type Item = Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
Either::Left(i) => i.next(),
Either::Right(i) => i.next(),
}
}
}
pin_project! {
#[project = EitherFutureProj]
pub enum EitherFuture<A, B> {
Left { #[pin] inner: A },
Right { #[pin] inner: B },
}
}
impl<A, B> Future for EitherFuture<A, B>
where
A: Future,
B: Future,
{
type Output = Either<A::Output, B::Output>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this {
EitherFutureProj::Left { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready(Either::Left(inner)),
},
EitherFutureProj::Right { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready(Either::Right(inner)),
},
}
}
}
macro_rules! tuples {
($name:ident + $fut_name:ident + $fut_proj:ident => $($ty:ident),*) => {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum $name<$($ty,)*> {
$($ty ($ty),)*
}
impl<$($ty,)*> Display for $name<$($ty,)*>
where
$($ty: Display,)*
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
$($name::$ty(this) => this.fmt(f),)*
}
}
}
impl<Item, $($ty,)*> Iterator for $name<$($ty,)*>
where
$($ty: Iterator<Item = Item>,)*
{
type Item = Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
$($name::$ty(i) => i.next(),)*
}
}
}
pin_project! {
#[project = $fut_proj]
pub enum $fut_name<$($ty,)*> {
$($ty { #[pin] inner: $ty },)*
}
}
impl<$($ty,)*> Future for $fut_name<$($ty,)*>
where
$($ty: Future,)*
{
type Output = $name<$($ty::Output,)*>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this {
$($fut_proj::$ty { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready($name::$ty(inner)),
},)*
}
}
}
}
}
tuples!(EitherOf3 + EitherOf3Future + EitherOf3FutureProj => A, B, C);
tuples!(EitherOf4 + EitherOf4Future + EitherOf4FutureProj => A, B, C, D);
tuples!(EitherOf5 + EitherOf5Future + EitherOf5FutureProj => A, B, C, D, E);
tuples!(EitherOf6 + EitherOf6Future + EitherOf6FutureProj => A, B, C, D, E, F);
tuples!(EitherOf7 + EitherOf7Future + EitherOf7FutureProj => A, B, C, D, E, F, G);
tuples!(EitherOf8 + EitherOf8Future + EitherOf8FutureProj => A, B, C, D, E, F, G, H);
tuples!(EitherOf9 + EitherOf9Future + EitherOf9FutureProj => A, B, C, D, E, F, G, H, I);
tuples!(EitherOf10 + EitherOf10Future + EitherOf10FutureProj => A, B, C, D, E, F, G, H, I, J);
tuples!(EitherOf11 + EitherOf11Future + EitherOf11FutureProj => A, B, C, D, E, F, G, H, I, J, K);
tuples!(EitherOf12 + EitherOf12Future + EitherOf12FutureProj => A, B, C, D, E, F, G, H, I, J, K, L);
tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M);
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

@@ -7,19 +7,21 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6.6", optional = true }
actix-web = { version = "4.8", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1.7"
cfg-if = "1.0"
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
cfg-if = "1"
http = { version = "1.0", optional = true }
leptos = { path = "../../leptos" }
leptos_meta = { path = "../../meta" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
wasm-bindgen = "0.2.93"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = "0.2"
serde = { version = "1", features = ["derive"] }
[features]
hydrate = ["leptos/hydrate"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",

View File

@@ -1,9 +1,68 @@
# Action Form Error Handling Example
<picture>
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)">
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
</picture>
## Getting Started
# Leptos Starter Template
See the [Examples README](../README.md) for setup and run instructions.
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
## Quick Start
## Creating your template repo
Execute `cargo leptos watch` to run this example.
If you don't have `cargo-leptos` installed you can install it with
`cargo install cargo-leptos`
Then run
`cargo leptos new --git leptos-rs/start`
to generate a new project template (you will be prompted to enter a project name).
`cd {projectname}`
to go to your newly created project.
Of course, you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`.
## Running your project
`cargo leptos watch`
By default, you can access your local project at `http://localhost:3000`
## Installing Additional Tools
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
4. `npm install -g sass` - install `dart-sass` (should be optional in future)
## Executing a Server on a Remote Machine Without the Toolchain
After running a `cargo leptos build --release` the minimum files needed are:
1. The server binary located in `target/server/release`
2. The `site` directory and all files within located in `target/site`
Copy these files to your remote server. The directory structure should be:
```text
leptos_start
site/
```
Set the following environment variables (updating for your project as needed):
```sh
export LEPTOS_OUTPUT_NAME="leptos_start"
export LEPTOS_SITE_ROOT="site"
export LEPTOS_SITE_PKG_DIR="pkg"
export LEPTOS_SITE_ADDR="127.0.0.1:3000"
export LEPTOS_RELOAD_PORT="3001"
```
Finally, run the server binary.
## Notes about CSR and Trunk:
Although it is not recommended, you can also run your project without server integration using the feature `csr` and `trunk serve`:
`trunk serve --open --features csr`
This may be useful for integrating external tools which require a static site, e.g. `tauri`.

View File

@@ -1,18 +1,27 @@
use leptos::{logging, prelude::*};
use leptos_router::{
components::{FlatRoutes, Route, Router},
StaticSegment,
};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
// injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/leptos_start.css"/>
// sets the document title
<Title text="Welcome to Leptos"/>
// content for this welcome page
<Router>
<main id="app">
<FlatRoutes fallback=NotFound>
<Route path=StaticSegment("") view=HomePage/>
</FlatRoutes>
<Routes>
<Route path="" view=HomePage/>
<Route path="/*any" view=NotFound/>
</Routes>
</main>
</Router>
}
@@ -34,7 +43,7 @@ async fn do_something(
/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
let do_something_action = ServerAction::<DoSomething>::new();
let do_something_action = Action::<DoSomething, _>::server();
let value = Signal::derive(move || {
do_something_action
.value()
@@ -48,12 +57,17 @@ fn HomePage() -> impl IntoView {
view! {
<h1>"Test the action form!"</h1>
<ErrorBoundary fallback=move |error| {
move || format!("{:#?}", error.get())
}>
<pre>{value}</pre>
<ActionForm action=do_something_action attr:class="form">
<label>"Should error: "<input type="checkbox" name="should_error"/></label>
<ErrorBoundary fallback=move |error| format!("{:#?}", error
.get()
.into_iter()
.next()
.unwrap()
.1.into_inner()
.to_string())
>
{value}
<ActionForm action=do_something_action class="form">
<label>Should error: <input type="checkbox" name="should_error"/></label>
<button type="submit">Submit</button>
</ActionForm>
</ErrorBoundary>
@@ -77,5 +91,7 @@ fn NotFound() -> impl IntoView {
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
}
view! { <h1>"Not Found"</h1> }
view! {
<h1>"Not Found"</h1>
}
}

View File

@@ -1,11 +1,18 @@
pub mod app;
use cfg_if::cfg_if;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
cfg_if! {
if #[cfg(feature = "hydrate")] {
console_error_panic_hook::set_once();
use wasm_bindgen::prelude::wasm_bindgen;
leptos::mount::hydrate_body(App);
#[wasm_bindgen]
pub fn hydrate() {
use app::*;
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
}
}

View File

@@ -4,47 +4,25 @@ async fn main() -> std::io::Result<()> {
use action_form_error_handling::app::*;
use actix_files::Files;
use actix_web::*;
use leptos::prelude::*;
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
use leptos_meta::MetaTags;
// Generate the list of routes in your Leptos App
let conf = get_configuration(None).unwrap();
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
println!("listening on http://{}", &addr);
HttpServer::new(move || {
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
// serve JS/WASM/CSS from `pkg`
.service(Files::new("/pkg", format!("{site_root}/pkg")))
.leptos_routes(routes, {
let leptos_options = leptos_options.clone();
move || {
use leptos::prelude::*;
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<AutoReload options=leptos_options.clone()/>
<HydrationScripts options=leptos_options.clone()/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}})
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
.app_data(web::Data::new(leptos_options.to_owned()))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
@@ -52,10 +30,24 @@ async fn main() -> std::io::Result<()> {
.await
}
#[cfg(not(feature = "ssr"))]
#[cfg(not(any(feature = "ssr", feature = "csr")))]
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
// see optional feature `csr` instead
}
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
pub fn main() {
// a client-side main function is required for using `trunk serve`
// prefer using `cargo leptos serve` instead
// to run: `trunk serve --open --features csr`
use action_form_error_handling::app::*;
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}

View File

@@ -0,0 +1,14 @@
[package]
name = "animated-show"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -1,4 +1,4 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]

View File

@@ -0,0 +1,14 @@
# Animated Show Example
This is a very simple example of the `<AnimatedShow>` component.
The `<AnimatedShow>` component is an extension for the `<Show>` component and it will not take in a fallback, but it will unmount the component from the DOM after a given duration. This makes it possible to have really easy unmount animations with just
CSS.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
<style>
.hover-me {
width: 100px;
margin: 1rem;
padding: 1rem;
text-align: center;
cursor: pointer;
border: 1px solid grey;
}
.here-i-am {
width: 100px;
margin: 1rem;
padding: 1rem;
text-align: center;
color: white;
font-weight: bold;
background: black;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
.fade-in-1000 {
animation: 1000ms fade-in forwards;
}
.fade-out-1000 {
animation: 1000ms fade-out forwards;
}
</style>
</head>
<body></body>
</html>

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,34 @@
use core::time::Duration;
use leptos::*;
#[component]
pub fn App() -> impl IntoView {
let show = create_rw_signal(false);
// the CSS classes in this example are just written directly inside the `index.html`
view! {
<div
class="hover-me"
on:mouseenter=move |_| show.set(true)
on:mouseleave=move |_| show.set(false)
>
"Hover Me"
</div>
<AnimatedShow
when=show
// optional CSS class which will be applied if `when == true`
show_class="fade-in-1000"
// optional CSS class which will be applied if `when == false` and before the
// `hide_delay` starts -> makes CSS unmount animations really easy
hide_class="fade-out-1000"
// the given unmount delay which should match your unmount animation duration
hide_delay=Duration::from_millis(1000)
>
// provide any `Children` inside here
<div class="here-i-am">
"Here I Am!"
</div>
</AnimatedShow>
}
}

View File

@@ -0,0 +1,8 @@
use animated_show::App;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(App);
}

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.

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

@@ -4,18 +4,16 @@ version = "0.1.0"
edition = "2021"
[profile.release]
opt-level = 'z'
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
console_log = "1.0"
log = "0.4.22"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
gloo-timers = { version = "0.3.0", features = ["futures"] }
[dev-dependencies]
wasm-bindgen = "0.2.93"
wasm-bindgen-test = "0.3.42"
web-sys = "0.3.70"
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"
web-sys = "0.3"

View File

@@ -1,4 +1,4 @@
use leptos::prelude::*;
use leptos::*;
/// A simple counter component.
///
@@ -10,12 +10,12 @@ pub fn SimpleCounter(
/// The change that should be applied each time the button is clicked.
step: i32,
) -> impl IntoView {
let (value, set_value) = signal(initial_value);
let (value, set_value) = create_signal(initial_value);
view! {
<div>
<button on:click=move |_| set_value.set(0)>"Clear"</button>
<button on:click=move |_| *set_value.write() -= step>"-1"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
</div>

View File

@@ -1,10 +1,15 @@
use counter::SimpleCounter;
use leptos::prelude::*;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! { <SimpleCounter initial_value=0 step=1/> }
view! {
<SimpleCounter
initial_value=0
step=1
/>
}
})
}

View File

@@ -1,21 +1,19 @@
use counter::*;
use leptos::mount::mount_to;
use leptos::prelude::*;
use leptos::task::tick;
use leptos::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn clear() {
let document = document();
fn clear() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
let _ = document.body().unwrap().append_child(&test_wrapper);
// start by rendering our counter and mounting it to the DOM
// note that we start at the initial value of 10
let _dispose = mount_to(
mount_to(
test_wrapper.clone().unchecked_into(),
|| view! { <SimpleCounter initial_value=10 step=1/> },
);
@@ -32,63 +30,59 @@ async fn clear() {
// now let's click the `clear` button
clear.click();
// the reactive system is built on top of the async system, so changes are not reflected
// synchronously in the DOM
// in order to detect the changes here, we'll just yield for a brief time after each change,
// allowing the effects that update the view to run
tick().await;
// now let's test the <div> against the expected value
// we can do this by testing its `outerHTML`
assert_eq!(div.outer_html(), {
// it's as if we're creating it with a value of 0, right?
let (value, _set_value) = signal(0);
let runtime = create_runtime();
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system, just to render the
// test case
{
// it's as if we're creating it with a value of 0, right?
let (value, _set_value) = create_signal(0);
// we can remove the event listeners because they're not rendered to HTML
view! {
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
// we can remove the event listeners because they're not rendered to HTML
view! {
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
// the view returned an HtmlElement<Div>, which is a smart pointer for
// a DOM element. So we can still just call .outer_html()
.outer_html()
}
// Leptos supports multiple backend renderers for HTML elements
// .into_view() here is just a convenient way of specifying "use the regular DOM renderer"
.into_view()
// views are lazy -- they describe a DOM tree but don't create it yet
// calling .build() will actually build the DOM elements
.build()
// .build() returned an ElementState, which is a smart pointer for
// a DOM element. So we can still just call .outer_html(), which access the outerHTML on
// the actual DOM element
.outer_html()
});
);
// There's actually an easier way to do this...
// We can just test against a <SimpleCounter/> with the initial value 0
assert_eq!(test_wrapper.inner_html(), {
let comparison_wrapper = document.create_element("section").unwrap();
let _dispose = mount_to(
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|| view! { <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
runtime.dispose();
}
#[wasm_bindgen_test]
async fn inc() {
let document = document();
fn inc() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
let _ = document.body().unwrap().append_child(&test_wrapper);
let _dispose = mount_to(
mount_to(
test_wrapper.clone().unchecked_into(),
|| view! { <SimpleCounter initial_value=0 step=1/> },
);
// You can do testing with vanilla DOM operations
let _document = leptos::document();
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = div
.first_child()
@@ -114,8 +108,6 @@ async fn inc() {
inc.click();
inc.click();
tick().await;
assert_eq!(text.text_content(), Some("Value: 2!".to_string()));
dec.click();
@@ -123,21 +115,19 @@ async fn inc() {
dec.click();
dec.click();
tick().await;
assert_eq!(text.text_content(), Some("Value: -2!".to_string()));
clear.click();
tick().await;
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
let runtime = create_runtime();
// Or you can test against a sample view!
assert_eq!(
div.outer_html(),
{
let (value, _) = signal(0);
let (value, _) = create_signal(0);
view! {
<div>
<button>"Clear"</button>
@@ -147,20 +137,16 @@ async fn inc() {
</div>
}
}
.into_view()
.build()
.outer_html()
);
inc.click();
tick().await;
assert_eq!(
div.outer_html(),
{
// because we've clicked, it's as if the signal is starting at 1
let (value, _) = signal(1);
let (value, _) = create_signal(1);
view! {
<div>
<button>"Clear"</button>
@@ -170,8 +156,8 @@ async fn inc() {
</div>
}
}
.into_view()
.build()
.outer_html()
);
runtime.dispose();
}

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"]
@@ -13,32 +11,34 @@ codegen-units = 1
lto = true
[dependencies]
actix-files = { version = "0.6.6", optional = true }
actix-web = { version = "4.8", optional = true, features = ["macros"] }
broadcaster = "1.0"
console_log = "1.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
broadcaster = "1"
console_log = "1"
console_error_panic_hook = "0.1"
futures = "0.3"
lazy_static = "1"
leptos = { path = "../../leptos" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
log = "0.4.22"
once_cell = "1.19"
gloo-net = { version = "0.6.0" }
wasm-bindgen = "0.2.93"
serde = { version = "1.0", features = ["derive"] }
simple_logger = "5.0"
tracing = { version = "0.1.40", optional = true }
send_wrapper = "0.6.0"
log = "0.4"
once_cell = "1.18"
gloo-net = { version = "0.6" }
wasm-bindgen = "0.2"
serde = { version = "1", features = ["derive"] }
simple_logger = "4.3"
tracing = { version = "0.1", optional = true }
[features]
hydrate = ["leptos/hydrate"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:tracing",
"leptos/ssr",
"leptos_actix",
"leptos_meta/ssr",
"leptos_router/ssr",
]
@@ -47,13 +47,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,21 +1,28 @@
use leptos::prelude::*;
use leptos_router::{
components::{FlatRoutes, Route, Router, A},
StaticSegment,
};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[cfg(feature = "ssr")]
use tracing::instrument;
#[cfg(feature = "ssr")]
pub mod ssr_imports {
pub use broadcaster::BroadcastChannel;
pub use once_cell::sync::OnceCell;
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();
}
static LOG_INIT: OnceCell<()> = OnceCell::new();
pub fn init_logging() {
LOG_INIT.get_or_init(|| {
simple_logger::SimpleLogger::new().env().init().unwrap();
});
}
}
#[server]
@@ -52,6 +59,10 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
}
#[component]
pub fn Counters() -> impl IntoView {
#[cfg(feature = "ssr")]
ssr_imports::init_logging();
provide_meta_context();
view! {
<Router>
<header>
@@ -74,12 +85,28 @@ pub fn Counters() -> impl IntoView {
</li>
</ul>
</nav>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<main>
<FlatRoutes fallback=|| "Not found.">
<Route path=StaticSegment("") view=Counter/>
<Route path=StaticSegment("form") view=FormCounter/>
<Route path=StaticSegment("multi") view=MultiuserCounter/>
</FlatRoutes>
<Routes>
<Route
path=""
view=|| {
view! { <Counter/> }
}
/>
<Route
path="form"
view=|| {
view! { <FormCounter/> }
}
/>
<Route
path="multi"
view=|| {
view! { <MultiuserCounter/> }
}
/>
</Routes>
</main>
</Router>
}
@@ -91,10 +118,10 @@ pub fn Counters() -> impl IntoView {
// This is the typical pattern for a CRUD app
#[component]
pub fn Counter() -> impl IntoView {
let dec = Action::new(|_: &()| adjust_server_count(-1, "decing".into()));
let inc = Action::new(|_: &()| adjust_server_count(1, "incing".into()));
let clear = Action::new(|_: &()| clear_server_count());
let counter = Resource::new(
let dec = create_action(|_: &()| adjust_server_count(-1, "decing".into()));
let inc = create_action(|_: &()| adjust_server_count(1, "incing".into()));
let clear = create_action(|_: &()| clear_server_count());
let counter = create_resource(
move || {
(
dec.version().get(),
@@ -111,14 +138,27 @@ pub fn Counter() -> impl IntoView {
<p>
"This counter sets the value on the server and automatically reloads the new value."
</p>
<ErrorBoundary fallback=|errors| move || format!("Error: {:#?}", errors.get())>
<div>
<button on:click=move |_| { clear.dispatch(()); }>"Clear"</button>
<button on:click=move |_| { dec.dispatch(()); }>"-1"</button>
<span>"Value: " <Suspense>{counter} "!"</Suspense></span>
<button on:click=move |_| { inc.dispatch(()); }>"+1"</button>
</div>
</ErrorBoundary>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>
"Value: "
<Suspense>
{move || counter.and_then(|count| *count)} "!"
</Suspense>
</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
<Suspense>
{move || {
counter.get().and_then(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
}).map(|msg| {
view! { <p>"Error: " {msg.to_string()}</p> }
})
}}
</Suspense>
</div>
}
}
@@ -130,10 +170,10 @@ pub fn Counter() -> impl IntoView {
pub fn FormCounter() -> impl IntoView {
// these struct names are auto-generated by #[server]
// they are just the PascalCased versions of the function names
let adjust = ServerAction::<AdjustServerCount>::new();
let clear = ServerAction::<ClearServerCount>::new();
let adjust = create_server_action::<AdjustServerCount>();
let clear = create_server_action::<ClearServerCount>();
let counter = Resource::new(
let counter = create_resource(
move || (adjust.version().get(), clear.version().get()),
|_| {
log::debug!("FormCounter running fetcher");
@@ -164,7 +204,7 @@ pub fn FormCounter() -> impl IntoView {
<input type="hidden" name="msg" value="form value down"/>
<input type="submit" value="-1"/>
</ActionForm>
<span>"Value: " <Suspense>{value} "!"</Suspense></span>
<span>"Value: " <Suspense>{move || value().to_string()} "!"</Suspense></span>
<ActionForm action=adjust>
<input type="hidden" name="delta" value="1"/>
<input type="hidden" name="msg" value="form value up"/>
@@ -182,21 +222,19 @@ pub fn FormCounter() -> impl IntoView {
#[component]
pub fn MultiuserCounter() -> impl IntoView {
let dec =
Action::new(|_: &()| adjust_server_count(-1, "dec dec goose".into()));
create_action(|_: &()| adjust_server_count(-1, "dec dec goose".into()));
let inc =
Action::new(|_: &()| adjust_server_count(1, "inc inc moose".into()));
let clear = Action::new(|_: &()| clear_server_count());
create_action(|_: &()| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(|_: &()| clear_server_count());
#[cfg(not(feature = "ssr"))]
let multiplayer_value = {
use futures::StreamExt;
use send_wrapper::SendWrapper;
let mut source = SendWrapper::new(
let mut source =
gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect("couldn't connect to SSE stream"),
);
let s = ReadSignal::from_stream_unsync(
.expect("couldn't connect to SSE stream");
let s = create_signal_from_stream(
source
.subscribe("message")
.unwrap()
@@ -210,12 +248,12 @@ pub fn MultiuserCounter() -> impl IntoView {
}),
);
on_cleanup(move || source.take().close());
on_cleanup(move || source.close());
s
};
#[cfg(feature = "ssr")]
let (multiplayer_value, _) = signal(None::<i32>);
let (multiplayer_value, _) = create_signal(None::<i32>);
view! {
<div>
@@ -224,12 +262,12 @@ pub fn MultiuserCounter() -> impl IntoView {
"This one uses server-sent events (SSE) to live-update when other users make changes."
</p>
<div>
<button on:click=move |_| { clear.dispatch(()); }>"Clear"</button>
<button on:click=move |_| { dec.dispatch(()); }>"-1"</button>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>
"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default()}
</span>
<button on:click=move |_| { inc.dispatch(()); }>"+1"</button>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
</div>
}

View File

@@ -3,10 +3,11 @@ pub mod counters;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::counters::Counters;
use crate::counters::*;
use leptos::*;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(Counters);
mount_to_body(Counters);
}

View File

@@ -3,6 +3,7 @@ mod counters;
use crate::counters::*;
use actix_files::Files;
use actix_web::*;
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
#[get("/api/events")]
@@ -26,44 +27,26 @@ async fn counter_events() -> impl Responder {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
use leptos::prelude::*;
// Setting this to None means we'll be using cargo-leptos and its env vars.
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
let conf = get_configuration(None).unwrap();
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
println!("listening on http://{}", &addr);
let routes = generate_route_list(Counters);
HttpServer::new(move || {
// Generate the list of routes in your Leptos App
let routes = generate_route_list(Counters);
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.service(counter_events)
.leptos_routes(routes, {
let leptos_options = leptos_options.clone();
move || {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<AutoReload options=leptos_options.clone()/>
<HydrationScripts options=leptos_options.clone()/>
</head>
<body>
<Counters/>
</body>
</html>
}
}})
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
Counters,
)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()

View File

@@ -9,10 +9,12 @@ lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
leptos_router = { path = "../../router", features = [] }
leptos_router = { path = "../../router", features = ["csr"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen = "0.2.93"
wasm-bindgen-test = "0.3.42"
web-sys = "0.3.70"
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"
web-sys = "0.3"

View File

@@ -1,17 +1,17 @@
use leptos::prelude::*;
use leptos_router::hooks::query_signal;
use leptos::*;
use leptos_router::*;
/// A simple counter component.
///
/// You can use doc comments like this to document your component.
#[component]
pub fn SimpleQueryCounter() -> impl IntoView {
let (count, set_count) = query_signal::<i32>("count");
let (count, set_count) = create_query_signal::<i32>("count");
let clear = move |_| set_count.set(None);
let decrement = move |_| set_count.set(Some(count.get().unwrap_or(0) - 1));
let increment = move |_| set_count.set(Some(count.get().unwrap_or(0) + 1));
let (msg, set_msg) = query_signal::<String>("message");
let (msg, set_msg) = create_query_signal::<String>("message");
let update_msg = move |ev| {
let new_msg = event_target_value(&ev);
if new_msg.is_empty() {

View File

@@ -1,13 +1,16 @@
use counter_url_query::SimpleQueryCounter;
use leptos::prelude::*;
use leptos_router::components::Router;
use leptos::*;
use leptos_router::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount::mount_to_body(|| {
mount_to_body(|| {
view! {
<Router>
<SimpleQueryCounter/>
<Routes>
<Route path="" view=SimpleQueryCounter />
</Routes>
</Router>
}
})

View File

@@ -10,13 +10,15 @@ lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.42"
pretty_assertions = "1.4"
rstest = "0.22.0"
wasm-bindgen-test = "0.3.34"
pretty_assertions = "1.3.0"
rstest = "0.17.0"
[dev-dependencies.web-sys]
features = ["HtmlElement", "XPathResult"]

View File

@@ -1,16 +1,9 @@
use leptos::{
ev,
html::{button, div, span},
prelude::*,
};
use leptos::{html::*, *};
/// A simple counter view.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
let count = RwSignal::new(Count::new(initial_value, step));
Effect::new(move |_| {
leptos::logging::log!("count = {:?}", count.get());
});
// the function name is the same as the HTML tag name
div()
@@ -51,7 +44,6 @@ impl Count {
}
pub fn value(&self) -> i32 {
leptos::logging::log!("value = {}", self.value);
self.value
}

View File

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

View File

@@ -1,5 +1,5 @@
use counter_without_macros::counter;
use leptos::{prelude::*, task::tick};
use leptos::*;
use pretty_assertions::assert_eq;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
@@ -8,32 +8,27 @@ use web_sys::HtmlElement;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn should_increment_counter() {
fn should_increment_counter() {
open_counter();
click_increment();
click_increment();
// reactive changes run asynchronously, so yield briefly before observing the DOM
tick().await;
assert_eq!(see_text(), Some("Value: 2!".to_string()));
}
#[wasm_bindgen_test]
async fn should_decrement_counter() {
fn should_decrement_counter() {
open_counter();
click_decrement();
click_decrement();
tick().await;
assert_eq!(see_text(), Some("Value: -2!".to_string()));
}
#[wasm_bindgen_test]
async fn should_clear_counter() {
fn should_clear_counter() {
open_counter();
click_increment();
@@ -41,18 +36,18 @@ async fn should_clear_counter() {
click_clear();
tick().await;
assert_eq!(see_text(), Some("Value: 0!".to_string()));
}
fn open_counter() {
remove_existing_counter();
leptos::mount::mount_to_body(move || counter(0, 1));
mount_to_body(move || counter(0, 1));
}
fn remove_existing_counter() {
if let Some(counter) = document().query_selector("body div").unwrap() {
if let Some(counter) =
leptos::document().query_selector("body div").unwrap()
{
counter.remove();
}
}
@@ -79,7 +74,7 @@ fn see_text() -> Option<String> {
fn find_by_text(text: &str) -> HtmlElement {
let xpath = format!("//*[text()='{}']", text);
let document = document();
let document = leptos::document();
document
.evaluate(&xpath, &document)
.unwrap()

View File

@@ -1,6 +0,0 @@
# Support playwright testing
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
pnpm-lock.yaml

View File

@@ -4,10 +4,12 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
leptos = { path = "../../leptos", features = ["csr"] }
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.42"
wasm-bindgen = "0.2.93"
web-sys = "0.3.70"
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2"
web-sys = "0.3"

View File

@@ -2,5 +2,4 @@ extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/playwright-trunk-test.toml" },
]

View File

@@ -1,4 +0,0 @@
node_modules/
/test-results/
/playwright-report/
/playwright/.cache/

View File

@@ -1,83 +0,0 @@
{
"name": "grip",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "grip",
"devDependencies": {
"@playwright/test": "^1.35.1"
}
},
"node_modules/.pnpm/@playwright+test@1.33.0": {
"extraneous": true
},
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
"version": "20.2.1",
"extraneous": true,
"license": "MIT"
},
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
"version": "1.33.0",
"extraneous": true,
"license": "Apache-2.0",
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@playwright/test": {
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.35.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/@types/node": {
"version": "20.3.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright-core": {
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
}
}
}

View File

@@ -1,10 +0,0 @@
{
"private": "true",
"scripts": {},
"devDependencies": {
"@playwright/test": "^1.46.1"
},
"dependencies": {
"pnpm": "^9.7.1"
}
}

View File

@@ -1,77 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !process.env.DEV,
/* Retry on CI only */
retries: process.env.DEV ? 0 : 10,
/* Opt out of parallel tests on CI. */
workers: process.env.DEV ? 1 : 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [["html", { open: "never" }], ["list"]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:8080",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
// {
// name: "firefox",
// use: { ...devices["Desktop Firefox"] },
// },
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: "cd ../ && trunk serve",
// url: "http://127.0.0.1:8080",
// reuseExistingServer: false, //!process.env.CI,
// },
});

View File

@@ -1,19 +0,0 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Add 1000 Counters", () => {
test("should increase the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await Promise.all([
await ui.goto(),
await ui.addOneThousandCountersButton.waitFor(),
]);
await ui.addOneThousandCounters();
await ui.addOneThousandCounters();
await ui.addOneThousandCounters();
await expect(ui.counters).toHaveText("3000");
});
});

View File

@@ -1,15 +0,0 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Add Counter", () => {
test("should increase the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await expect(ui.counters).toHaveText("3");
});
});

View File

@@ -1,18 +0,0 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Clear Counters", () => {
test("should reset the counts", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.clearCounters();
await expect(ui.total).toHaveText("0");
await expect(ui.counters).toHaveText("0");
});
});

View File

@@ -1,16 +0,0 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Decrement Count", () => {
test("should decrease the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.decrementCount();
await ui.decrementCount();
await ui.decrementCount();
await expect(ui.total).toHaveText("-3");
});
});

View File

@@ -1,30 +0,0 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Enter Count", () => {
test("should increase the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.enterCount("5");
await expect(ui.total).toHaveText("5");
await expect(ui.counters).toHaveText("1");
});
test("should decrease the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.enterCount("100");
await ui.enterCount("100", 1);
await ui.enterCount("100", 2);
await ui.enterCount("50", 1);
await expect(ui.total).toHaveText("250");
});
});

View File

@@ -1,98 +0,0 @@
import { expect, Locator, Page } from "@playwright/test";
export class CountersPage {
readonly page: Page;
readonly addCounterButton: Locator;
readonly addOneThousandCountersButton: Locator;
readonly clearCountersButton: Locator;
readonly incrementCountButton: Locator;
readonly counterInput: Locator;
readonly decrementCountButton: Locator;
readonly removeCountButton: Locator;
readonly total: Locator;
readonly counters: Locator;
constructor(page: Page) {
this.page = page;
this.addCounterButton = page.locator("button", { hasText: "Add Counter" });
this.addOneThousandCountersButton = page.locator("button", {
hasText: "Add 1000 Counters",
});
this.clearCountersButton = page.locator("button", {
hasText: "Clear Counters",
});
this.decrementCountButton = page.locator("button", {
hasText: "-1",
});
this.incrementCountButton = page.locator("button", {
hasText: "+1",
});
this.removeCountButton = page.locator("button", {
hasText: "x",
});
this.total = page.getByTestId("total");
this.counters = page.getByTestId("counters");
this.counterInput = page.getByRole("textbox");
}
async goto() {
await this.page.goto("/");
}
async addCounter() {
await Promise.all([
this.addCounterButton.waitFor(),
this.addCounterButton.click(),
]);
}
async addOneThousandCounters() {
this.addOneThousandCountersButton.click();
}
async decrementCount(index: number = 0) {
await Promise.all([
this.decrementCountButton.nth(index).waitFor(),
this.decrementCountButton.nth(index).click(),
]);
}
async incrementCount(index: number = 0) {
await Promise.all([
this.incrementCountButton.nth(index).waitFor(),
this.incrementCountButton.nth(index).click(),
]);
}
async clearCounters() {
await Promise.all([
this.clearCountersButton.waitFor(),
this.clearCountersButton.click(),
]);
}
async enterCount(count: string, index: number = 0) {
await Promise.all([
this.counterInput.nth(index).waitFor(),
this.counterInput.nth(index).fill(count),
]);
}
async removeCounter(index: number = 0) {
await Promise.all([
this.removeCountButton.nth(index).waitFor(),
this.removeCountButton.nth(index).click(),
]);
}
}

View File

@@ -1,16 +0,0 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Increment Count", () => {
test("should increase the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.incrementCount();
await ui.incrementCount();
await ui.incrementCount();
await expect(ui.total).toHaveText("3");
});
});

View File

@@ -1,17 +0,0 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Remove Counter", () => {
test("should decrement the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.removeCounter(1);
await expect(ui.counters).toHaveText("2");
});
});

View File

@@ -1,19 +0,0 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("View Counters", () => {
test("should see the title", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await expect(page).toHaveTitle("Counters");
});
test("should see the initial counts", async ({ page }) => {
const counters = new CountersPage(page);
await counters.goto();
await expect(counters.total).toHaveText("0");
await expect(counters.counters).toHaveText("0");
});
});

View File

@@ -2,7 +2,6 @@
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
<title>Counters</title>
</head>
<body></body>
</html>
</html>

View File

@@ -1,8 +1,8 @@
use leptos::prelude::*;
use leptos::*;
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, ArcRwSignal<i32>)>;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
@@ -11,13 +11,13 @@ struct CounterUpdater {
#[component]
pub fn Counters() -> impl IntoView {
let (next_counter_id, set_next_counter_id) = signal(0);
let (counters, set_counters) = signal::<CounterHolder>(vec![]);
let (next_counter_id, set_next_counter_id) = create_signal(0);
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
provide_context(CounterUpdater { set_counters });
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = ArcRwSignal::new(0);
let sig = create_signal(0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
@@ -25,7 +25,7 @@ pub fn Counters() -> impl IntoView {
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = ArcRwSignal::new(0);
let signal = create_signal(0);
(id, signal)
});
@@ -39,56 +39,78 @@ pub fn Counters() -> impl IntoView {
view! {
<div>
<button on:click=add_counter>"Add Counter"</button>
<button on:click=add_many_counters>{format!("Add {MANY_COUNTERS} Counters")}</button>
<button on:click=clear_counters>"Clear Counters"</button>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span data-testid="total">
{move || {
counters.get().iter().map(|(_, count)| count.get()).sum::<i32>().to_string()
}}
</span> " from "
<span data-testid="counters">{move || counters.get().len().to_string()}</span>
<span>{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span>{move || counters.get().len().to_string()}</span>
" counters."
</p>
<ul>
<For
each=move || counters.get()
key=|counter| counter.0
children=move |(id, value)| {
view! { <Counter id value/> }
children=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! {
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(id: usize, value: ArcRwSignal<i32>) -> impl IntoView {
let value = RwSignal::from(value);
fn Counter(
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context().unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
// this will run when the scope is disposed, i.e., when this row is deleted
// because the signal was created in the parent scope, it won't be disposed
// of until the parent scope is. but we no longer need it, so we'll dispose of
// it when this row is deleted, instead. if we don't dispose of it here,
// this memory will "leak," i.e., the signal will continue to exist until the
// parent component is removed. in the case of this component, where it's the
// root, that's the lifetime of the program.
on_cleanup(move || {
log::debug!("deleted a row");
value.dispose();
});
view! {
<li>
<button on:click=move |_| value.update(move |value| *value -= 1)>"-1"</button>
<input
type="text"
prop:value=value
on:input:target=move |ev| {
value.set(ev.target().value().parse::<i32>().unwrap_or_default())
}
<button on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input type="text"
prop:value={value}
on:input=input
/>
<span>{value}</span>
<button on:click=move |_| value.update(move |value| *value += 1)>"+1"</button>
<button on:click=move |_| {
set_counters
.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))
}>"x"</button>
<button on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

@@ -1,6 +1,8 @@
use counters::Counters;
use leptos::*;
fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount::mount_to_body(Counters)
mount_to_body(|| view! { <Counters/> })
}

View File

@@ -3,15 +3,14 @@ use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counters::Counters;
use leptos::prelude::*;
use leptos::task::tick;
use leptos::*;
use web_sys::HtmlElement;
#[wasm_bindgen_test]
async fn inc() {
mount_to_body(Counters);
fn inc() {
mount_to_body(|| view! { <Counters/> });
let document = document();
let document = leptos::document();
let div = document.query_selector("div").unwrap().unwrap();
let add_counter = div
.first_child()
@@ -19,33 +18,31 @@ async fn inc() {
.dyn_into::<HtmlElement>()
.unwrap();
assert_eq!(
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: \
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">0</span> counters.</p><ul><!----></ul>"
);
// add 3 counters
add_counter.click();
add_counter.click();
add_counter.click();
tick().await;
// check HTML
assert_eq!(
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: \
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">3</span> \
counters.</p><ul><li><button>-1</button><input \
type=\"text\"><span>0</span><button>+1</button><button>x</button></\
li><li><button>-1</button><input \
type=\"text\"><span>0</span><button>+1</button><button>x</button></\
li><li><button>-1</button><input \
type=\"text\"><span>0</span><button>+1</button><button>x</button></\
li><!----></ul>"
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
<DynChild> -->0<!-- </DynChild> --></span> from <span><!-- \
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
<Each> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->0<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->0<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->0<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- </Each> --></ul>"
);
let counters = div
@@ -74,20 +71,25 @@ async fn inc() {
}
}
tick().await;
assert_eq!(
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: \
<span data-testid=\"total\">6</span> from <span data-testid=\"counters\">3</span> \
counters.</p><ul><li><button>-1</button><input \
type=\"text\"><span>1</span><button>+1</button><button>x</button></\
li><li><button>-1</button><input \
type=\"text\"><span>2</span><button>+1</button><button>x</button></\
li><li><button>-1</button><input \
type=\"text\"><span>3</span><button>+1</button><button>x</button></\
li><!----></ul>"
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
<DynChild> -->6<!-- </DynChild> --></span> from <span><!-- \
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
<Each> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->1<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->2<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->3<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- </Each> --></ul>"
);
// remove the first counter
@@ -99,17 +101,20 @@ async fn inc() {
.unchecked_into::<HtmlElement>()
.click();
tick().await;
assert_eq!(
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: \
<span data-testid=\"total\">5</span> from <span data-testid=\"counters\">2</span> \
counters.</p><ul><li><button>-1</button><input \
type=\"text\"><span>2</span><button>+1</button><button>x</button></\
li><li><button>-1</button><input \
type=\"text\"><span>3</span><button>+1</button><button>x</button></\
li><!----></ul>"
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
<DynChild> -->5<!-- </DynChild> --></span> from <span><!-- \
<DynChild> -->2<!-- </DynChild> --></span> counters.</p><ul><!-- \
<Each> --><!-- <EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->2<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->3<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- </Each> --></ul>"
);
}

View File

@@ -5,12 +5,13 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
log = "0.4.22"
console_log = "1.0"
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"
web-sys = { version = "0.3.70", features = ["Clipboard", "Navigator"] }
web-sys = { version = "0.3", features = ["Clipboard", "Navigator"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.42"
wasm-bindgen = "0.2.93"
web-sys = { version = "0.3.70", features = ["NodeList"] }
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2"
web-sys = "0.3"
gloo-timers = { version = "0.3", features = ["futures"] }

View File

@@ -1,34 +1,32 @@
use leptos::{ev::click, prelude::*};
use web_sys::Element;
use leptos::{ev::click, html::AnyElement, *};
// no extra parameter
pub fn highlight(el: Element) {
pub fn highlight(el: HtmlElement<AnyElement>) {
let mut highlighted = false;
let handle = el.clone().on(click, move |_| {
let _ = el.clone().on(click, move |_| {
highlighted = !highlighted;
if highlighted {
el.style(("background-color", "yellow"));
let _ = el.clone().style("background-color", "yellow");
} else {
el.style(("background-color", "transparent"));
let _ = el.clone().style("background-color", "transparent");
}
});
on_cleanup(move || drop(handle));
}
// one extra parameter
pub fn copy_to_clipboard(el: Element, content: &str) {
let content = content.to_owned();
let handle = el.clone().on(click, move |evt| {
pub fn copy_to_clipboard(el: HtmlElement<AnyElement>, content: &str) {
let content = content.to_string();
let _ = el.clone().on(click, move |evt| {
evt.prevent_default();
evt.stop_propagation();
let _ = window().navigator().clipboard().write_text(&content);
el.set_inner_html(&format!("Copied \"{}\"", &content));
let _ = el.clone().inner_html(format!("Copied \"{}\"", &content));
});
on_cleanup(move || drop(handle));
}
// custom parameter
@@ -49,18 +47,15 @@ impl From<()> for Amount {
}
}
pub fn add_dot(el: Element, amount: Amount) {
use leptos::wasm_bindgen::JsCast;
let el = el.unchecked_into::<web_sys::HtmlElement>();
let handle = el.clone().on(click, move |_| {
// .into() will automatically be called on the parameter
pub fn add_dot(el: HtmlElement<AnyElement>, amount: Amount) {
_ = el.clone().on(click, move |_| {
el.set_inner_text(&format!(
"{}{}",
el.inner_text(),
".".repeat(amount.0)
))
});
on_cleanup(move || drop(handle));
})
}
#[component]
@@ -86,8 +81,7 @@ pub fn App() -> impl IntoView {
<SomeComponent use:highlight/>
// no value will default to `().into()`
<button use:add_dot>"Add a dot"</button>
// can manually call `.into()` to convert to the correct type
// (automatically calling `.into()` prevents using generics in directive functions)
<button use:add_dot=5.into()>"Add 5 dots"</button>
// `5.into()` automatically called
<button use:add_dot=5>"Add 5 dots"</button>
}
}

View File

@@ -1,5 +1,5 @@
use directives::App;
use leptos::prelude::*;
use leptos::*;
fn main() {
_ = console_log::init_with_level(log::Level::Debug);

View File

@@ -1,16 +1,19 @@
use directives::App;
use leptos::{prelude::*, task::tick};
use gloo_timers::future::sleep;
use std::time::Duration;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
use web_sys::HtmlElement;
wasm_bindgen_test_configure!(run_in_browser);
use directives::App;
use leptos::*;
use web_sys::HtmlElement;
#[wasm_bindgen_test]
async fn test_directives() {
leptos::mount::mount_to_body(App);
tick().await;
mount_to_body(|| view! { <App/> });
sleep(Duration::ZERO).await;
let document = document();
let document = leptos::document();
let paragraphs = document.query_selector_all("p").unwrap();
assert_eq!(paragraphs.length(), 3);

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