Compare commits

..

31 Commits

Author SHA1 Message Date
Greg Johnston
c317bc1e5c feat: add .write() and .read() for signals 2023-09-08 11:30:09 -04:00
Baptiste
b3a4c95dad feat: Rc-backed ChildrenFn (#1669) 2023-09-08 07:44:50 -04:00
Greg Johnston
de44b1f91f Merge pull request #1673 from martinfrances107/router_version_bump
Router version bump
2023-09-08 07:43:47 -04:00
Greg Johnston
689022661d change: move logging macros into a logging module to avoid name conflicts with log and tracing (#1658) 2023-09-08 07:42:58 -04:00
Joseph Cruz
905d46a09d refactor(examples): extract client process tasks (#1665) (#1666)
* doc(test-report): report trunk and node

* refactor(examples): extract client process tasks

* chore(exaples): force ci
2023-09-08 07:31:55 -04:00
martinfrances107
5585f20940 chore: Bumped a few outdated packages.
-cached = { version = "0.44.0", optional = true }
+cached = { version = "0.45.0", optional = true }
-lru = { version = "0.10", optional = true }
+lru = { version = "0.11", optional = true }
2023-09-08 09:30:13 +01:00
martinfrances107
5c3ed3f018 Chore: Bump to actions/checkout@v4 2023-09-08 08:28:01 +01:00
Greg Johnston
03cabf6ea3 chore: create SECURITY.md 2023-09-06 21:19:33 -04:00
SleeplessOne1917
2798dc455f examples: use cargo-leptos Tailwind support in Tailwind examples (#1625) 2023-09-06 07:25:00 -04:00
Florian Wickert
db20be5576 fix: compare path components to detect active link in router (#1656) 2023-09-06 06:49:10 -04:00
Nya
495862e9f9 fix: custom events on components (#1648) 2023-09-04 13:27:33 -04:00
Joseph Cruz
2ca1c51fdc test(error_boundary): add e2e testing (#1651)
* test(error_boundary): open app

* test(error_boundary): click up arrow

* test(error_boundary): click down arrow

* test(error_boundary): type number

* test(error_boundary): clear number

* fix(build): clean trunk directories

* fix(test-report): detect unit tests

* ci(build): echo stop trunk
2023-09-04 13:25:44 -04:00
Greg Johnston
70e1ad41e2 Merge pull request #1579 from leptos-rs/rusty
feat: start adding some Rustier interfaces for reactive types
2023-09-04 13:23:18 -04:00
Greg Johnston
53ec7ed272 feat: add Effect::with_value_mut() 2023-09-04 11:14:07 -04:00
Greg Johnston
d98a577740 feat: add Rustier interfaces for reactive system types 2023-09-04 11:05:23 -04:00
jquesada2016
fd834f48c2 change: rename .derived_signal() and .mapped_signal_setter() methods (#1637) 2023-09-04 08:41:41 -04:00
Greg Johnston
7be65a37c6 fix: versioned resources never decrement Suspense (closes #1640) (#1641) 2023-09-03 20:21:16 -04:00
Banzobotic
3b5e2d86fb docs: clean up messy spacing left over from cx replacements (#1626) 2023-09-03 20:21:05 -04:00
jquesada2016
716b9fb50b feat: add .into_X_boxed() for classes, properties, and styles as for attributes 2023-09-03 20:18:49 -04:00
jquesada2016
006ca13797 chore: hide get_property (#1638) 2023-09-03 20:15:15 -04:00
Village
6e008343c8 feat: add component generics (#1636) 2023-09-03 20:09:50 -04:00
Greg Johnston
2ca24883ac fix: memoize Suspense readiness to avoid rerendering children/fallback (#1642) 2023-09-03 20:07:20 -04:00
Village
4a43983f4e feat: implement spreading attributes onto elements (#1619) 2023-09-01 20:52:15 -04:00
IcosaHedron
d9e83121c1 feat: add reload websocket configuration and enable env configuration (#1613) 2023-09-01 20:51:46 -04:00
Antonin Peronnet
f5b4b97c9b feat: Callback types to make it easier to accept (optional) callback props (#1596) 2023-09-01 20:51:32 -04:00
Gareth
bcfa430a40 docs: fix incorrect variable name (#1623) 2023-09-01 07:39:41 -04:00
Lawrence Qupty
7c51815cf5 docs: remove extra space (#1622) 2023-09-01 07:39:05 -04:00
Dmitry Pytaylo
fee2fb953b docs: fix typo (#1618) 2023-09-01 07:37:52 -04:00
Sadra M
8ecb7f59c4 docs: update references to server binary in dockerfile (#1617) 2023-09-01 07:37:24 -04:00
martin frances
b85cb9fb3b docs: clarify how many times derived signals are called (#1614) 2023-09-01 07:36:15 -04:00
Joseph Cruz
a631c5ca1c doc(examples): report fantoccini use (#1616) 2023-09-01 07:35:29 -04:00
137 changed files with 3189 additions and 1995 deletions

View File

@@ -24,7 +24,7 @@ jobs:
steps:
# Setup environment
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Rust
uses: actions-rs/toolchain@v1

13
SECURITY.md Normal file
View File

@@ -0,0 +1,13 @@
# Security Policy
## Reporting a Vulnerability
To report a suspected security issue, please contact security@leptos.dev rather than opening
a public issue.
## Supported Versions
The most-recently-released version of the library is supported with security updates.
For example, if a security issue is discovered that affects 0.3.2 and all later releases,
a 0.4.x patch will be released but a new 0.3.x patch release will not be made. You should
plan to update to the latest version to receive any new features or bugfixes of any kind.

View File

@@ -65,7 +65,7 @@ And add a simple “Hello, world!” to your `main.rs`
use leptos::*;
fn main() {
mount_to_body(|| view! { <p>"Hello, world!"</p> })
mount_to_body(|| view! { <p>"Hello, world!"</p> })
}
```

View File

@@ -367,7 +367,6 @@ fn GlobalStateInput() -> impl IntoView {
// that we created in the other component
// neither of them will cause the other to rerun
let (name, set_name) = create_slice(
// we take a slice *from* `state`
state,
// our getter returns a "slice" of the data

View File

@@ -235,9 +235,9 @@ At the very most, you might consider memoizing the final node before running som
```rust
let text = create_memo(move |_| {
d()
d()
});
create_effect(move |_| {
engrave_text_into_bar_of_gold(&text());
engrave_text_into_bar_of_gold(&text());
});
```

View File

@@ -18,7 +18,7 @@ let async_data = create_resource(
count,
// every time `count` changes, this will run
|value| async move {
log!("loading data from API");
logging::log!("loading data from API");
load_data(value).await
},
);

View File

@@ -4,7 +4,7 @@ In the previous chapter, we showed how you can create a simple loading screen to
```rust
let (count, set_count) = create_signal(0);
let a = create_resource(count, |count| async move { load_a(count).await });
let once = create_resource(count, |count| async move { load_a(count).await });
view! {
<h1>"My Data"</h1>

View File

@@ -54,7 +54,7 @@ RUN cargo leptos build --release -vv
FROM rustlang/rust:nightly-bullseye as runner
# Copy the server binary to the /app directory
COPY --from=builder /app/target/server/release/leptos_website /app/
COPY --from=builder /app/target/server/release/leptos_start /app/
# /target/site contains our JS/WASM/CSS, etc.
COPY --from=builder /app/target/site /app/site
# Copy Cargo.toml if its needed at runtime
@@ -68,7 +68,7 @@ ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
ENV LEPTOS_SITE_ROOT="site"
EXPOSE 8080
# Run the server
CMD ["/app/leptos_website"]
CMD ["/app/leptos_start"]
```
> Read more: [`gnu` and `musl` build files for Leptos apps](https://github.com/leptos-rs/leptos/issues/1152#issuecomment-1634916088).

View File

@@ -50,7 +50,6 @@ If you want to really understand the issue here, it may help to look at the expa
```rust
Suspense(
::leptos::component_props_builder(&Suspense)
.fallback(|| ())
.children({
@@ -61,7 +60,6 @@ Suspense(
leptos::Fragment::lazy(|| {
vec![
(Show(
::leptos::component_props_builder(&Show)
.when(|| true)
// but fallback is moved into Show here

View File

@@ -50,7 +50,6 @@ let (use_last, set_use_last) = create_signal(true);
// any time one of the source signals changes
create_effect(move |_| {
log(
if use_last() {
format!("{} {}", first(), last())
} else {
@@ -122,7 +121,6 @@ Like `create_resource`, `watch` takes a first argument, which is reactively trac
let (num, set_num) = create_signal(0);
let stop = watch(
move || num.get(),
move |num, prev_num, _| {
log::debug!("Number: {}; Prev: {:?}", num, prev_num);

View File

@@ -20,7 +20,7 @@ let text = move || if count_is_odd() {
// an effect automatically tracks the signals it depends on
// and reruns when they change
create_effect(move |_| {
log!("text = {}", text());
logging::log!("text = {}", text());
});
view! {

View File

@@ -16,7 +16,7 @@ Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `Wr
```rust
let (count, set_count) = create_signal(0);
set_count(1);
log!(count());
logging::log!(count());
```
is the same as
@@ -24,7 +24,7 @@ is the same as
```rust
let (count, set_count) = create_signal(0);
set_count.set(1);
log!(count.get());
logging::log!(count.get());
```
You might notice that `.get()` and `.set()` can be implemented in terms of `.with()` and `.update()`. In other words, `count.get()` is identical with `count.with(|n| n.clone())`, and `count.set(1)` is implemented by doing `count.update(|n| *n = 1)`.
@@ -63,7 +63,7 @@ if names.with(Vec::is_empty) {
}
```
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unncessary closure.
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unnecessary closure.
## Making signals depend on each other

View File

@@ -33,7 +33,6 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
#[component]
pub fn BusyButton() -> impl IntoView {
view! {
<button on:click=move |_| {
spawn_local(async {
add_todo("So much to do!".to_string()).await;

View File

@@ -35,7 +35,6 @@ Heres a simplified example from our [`session_auth_axum` example](https://git
```rust
#[server(Login, "/api")]
pub async fn login(
username: String,
password: String,
remember: Option<String>,

View File

@@ -9,7 +9,7 @@ Put a log somewhere in your root component. (I usually call mine `<App/>`, but a
```rust
#[component]
pub fn App() -> impl IntoView {
leptos::log!("where do I run?");
logging::log!("where do I run?");
// ... whatever
}
```
@@ -166,7 +166,7 @@ For example, say that I want to store something in the browsers `localStorage
pub fn App() -> impl IntoView {
use gloo_storage::Storage;
let storage = gloo_storage::LocalStorage::raw();
leptos::log!("{storage:?}");
logging::log!("{storage:?}");
}
```
@@ -180,7 +180,7 @@ pub fn App() -> impl IntoView {
use gloo_storage::Storage;
create_effect(move |_| {
let storage = gloo_storage::LocalStorage::raw();
leptos::log!("{storage:?}");
logging::log!("{storage:?}");
});
}
```

View File

@@ -37,7 +37,7 @@ impl Todos {
#[cfg(test)]
mod tests {
#[test]
fn test_remaining {
fn test_remaining() {
// ...
}
}

View File

@@ -145,7 +145,7 @@ Derived signals let you create reactive computed values that can be used in mult
places in your application with minimal overhead.
Note: Using a derived signal like this means that the calculation runs once per
signal change per place we access `double_count`; in other words, twice. This is a
signal change and once per place we access `double_count`; in other words, twice. This is a
very cheap calculation, so thats fine. Well look at memos in a later chapter, which
are designed to solve this problem for expensive calculations.

View File

@@ -35,9 +35,7 @@ Instead, lets create a `<ProgressBar/>` component.
```rust
#[component]
fn ProgressBar(
) -> impl IntoView {
fn ProgressBar() -> impl IntoView {
view! {
<progress
max="50"
@@ -64,7 +62,6 @@ In Leptos, you define props by giving additional arguments to the component func
```rust
#[component]
fn ProgressBar(
progress: ReadSignal<i32>
) -> impl IntoView {
view! {
@@ -118,7 +115,6 @@ argument to the component function with `#[prop(optional)]`.
```rust
#[component]
fn ProgressBar(
// mark this prop optional
// you can specify it or not when you use <ProgressBar/>
#[prop(optional)]
@@ -149,7 +145,6 @@ with `#[prop(default = ...)`.
```rust
#[component]
fn ProgressBar(
#[prop(default = 100)]
max: u16,
progress: ReadSignal<i32>
@@ -199,7 +194,6 @@ implement the trait `Fn() -> i32`. So you could use a generic component:
```rust
#[component]
fn ProgressBar<F>(
#[prop(default = 100)]
max: u16,
progress: F
@@ -254,7 +248,6 @@ reactive value.
```rust
#[component]
fn ProgressBar(
#[prop(default = 100)]
max: u16,
#[prop(into)]
@@ -373,7 +366,6 @@ component function, and each one of the props:
/// Shows progress toward a goal.
#[component]
fn ProgressBar(
/// The maximum value of the progress bar.
#[prop(default = 100)]
max: u16,

View File

@@ -148,10 +148,10 @@ This _works_, for sure. But if you added a log, you might be surprised
```rust
let message = move || if value() > 5 {
log!("{}: rendering Big", value());
logging::log!("{}: rendering Big", value());
"Big"
} else {
log!("{}: rendering Small", value());
logging::log!("{}: rendering Small", value());
"Small"
};
```

View File

@@ -72,10 +72,7 @@ pub fn App() -> impl IntoView {
#[component]
pub fn ButtonB<F>(
on_click: F,
) -> impl IntoView
pub fn ButtonB<F>(on_click: F) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
{

View File

@@ -47,7 +47,6 @@ Lets define a component that takes some children and a render prop.
```rust
#[component]
pub fn TakesChildren<F, IV>(
/// Takes a function (type F) that returns anything that can be
/// converted into a View (type IV)
render_prop: F,

View File

@@ -49,9 +49,9 @@ jq -R -s -c 'split("\n")[:-1]')
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
'''
[tasks.test-runner-report]
[tasks.test-report]
workspace = false
description = "report ci test runners for each example - OPTION: [all]"
description = "report web testing technology used by examples - OPTION: [all]"
script = '''
set -emu
@@ -62,11 +62,10 @@ YELLOW="\e[0;33m"
RESET="\e[0m"
echo
echo "${YELLOW}Test Runner Report${RESET}"
echo "${ITALIC}Pass the option \"all\" to show all the examples${RESET}"
echo "${YELLOW}Web Test Technology${RESET}"
echo
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' |
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' -not -path '*/node_modules/*' |
sed 's%./%%' |
sed 's%/Makefile.toml%%' |
grep -v Makefile.toml |
@@ -77,56 +76,78 @@ start_path=$(pwd)
for path in $makefile_paths; do
cd $path
test_runner=
crate_symbols=
test_count=$(grep -rl -E "#\[test\]" | wc -l)
if [ $test_count -gt 0 ]; then
test_runner="T"
fi
pw_count=$(find . -name playwright.config.ts | wc -l)
while read -r line; do
case $line in
*"cucumber"*)
test_runner=$test_runner"C"
crate_symbols=$crate_symbols"C"
;;
*"rstest"*)
test_runner=$test_runner"R"
*"fantoccini"*)
crate_symbols=$crate_symbols"D"
;;
esac
done <"./Cargo.toml"
while read -r line; do
case $line in
*"wasm-test.toml"*)
test_runner=$test_runner"W"
*"cargo-make/wasm-test.toml"*)
crate_symbols=$crate_symbols"W"
;;
*"playwright-test.toml"*)
test_runner=$test_runner"P"
*"cargo-make/playwright-test.toml"*)
crate_symbols=$crate_symbols"P"
crate_symbols=$crate_symbols"N"
;;
*"cargo-leptos-test.toml"*)
test_runner=$test_runner"L"
*"cargo-make/playwright-trunk-test.toml"*)
crate_symbols=$crate_symbols"P"
crate_symbols=$crate_symbols"T"
;;
*"cargo-make/trunk_server.toml"*)
crate_symbols=$crate_symbols"T"
;;
*"cargo-make/cargo-leptos-webdriver-test.toml"*)
crate_symbols=$crate_symbols"L"
;;
*"cargo-make/cargo-leptos-test.toml"*)
crate_symbols=$crate_symbols"L"
if [ $pw_count -gt 0 ]; then
crate_symbols=$crate_symbols"P"
fi
;;
esac
done <"./Makefile.toml"
runners=$(echo ${test_runner} | grep -o . | sort | tr -d "\n")
# Sort list of tools
sorted_crate_symbols=$(echo ${crate_symbols} | grep -o . | sort | tr -d "\n")
formatted_crate_symbols="${BOLD}${YELLOW}${sorted_crate_symbols}${RESET}"
crate_line=$path
if [ ! -z ${1+x} ]; then
# Show all examples
echo "$path${BOLD}${runners}${RESET}"
elif [ ! -z $runners ]; then
if [ ! -z $crate_symbols ]; then
crate_line=$crate_line$formatted_crate_symbols
fi
echo $crate_line
elif [ ! -z $crate_symbols ]; then
# Filter out examples that do not run tests in `ci`
echo "$path${BOLD}${runners}${RESET}"
crate_line=$crate_line$formatted_crate_symbols
echo $crate_line
fi
cd ${start_path}
done
c="${BOLD}${YELLOW}C${RESET} = Cucumber"
d="${BOLD}${YELLOW}D${RESET} = WebDriver"
l="${BOLD}${YELLOW}L${RESET} = Cargo Leptos"
n="${BOLD}${YELLOW}N${RESET} = Node"
p="${BOLD}${YELLOW}P${RESET} = Playwright"
t="${BOLD}${YELLOW}T${RESET} = Trunk"
w="${BOLD}${YELLOW}W${RESET} = WASM"
echo
echo "${ITALIC}Runners: C = Cucumber, L = Cargo Leptos, P = Playwright, R = RS Test, T = Cargo, W = WASM${RESET}"
echo "${ITALIC}Keys:${RESET} $c, $d, $l, $n, $p, $t, $w"
echo
'''
# ALIASES
[tasks.tr]
alias = "test-runner-report"

View File

@@ -4,4 +4,4 @@ The examples in this directory are all built and tested against the current `mai
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch and not the current release.
To see the examples as they were at the time of the `0.3.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.3.0/examples).
To see the examples as they were at the time of the `0.3.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.3.0/examples).

View File

@@ -2,7 +2,3 @@ extend = { path = "./cargo-leptos.toml" }
[tasks.integration-test]
dependencies = ["install-cargo-leptos", "cargo-leptos-e2e"]
[tasks.cargo-leptos-e2e]
command = "cargo"
args = ["leptos", "end-to-end"]

View File

@@ -0,0 +1,7 @@
extend = [
{ path = "./cargo-leptos.toml" },
{ path = "../cargo-make/webdriver.toml" },
]
[tasks.integration-test]
dependencies = ["install-cargo-leptos", "start-webdriver", "cargo-leptos-e2e"]

View File

@@ -1,6 +1,12 @@
extend = { path = "../cargo-make/client-process.toml" }
[tasks.install-cargo-leptos]
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
[tasks.cargo-leptos-e2e]
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.build]
clear = true
command = "cargo"
@@ -25,31 +31,3 @@ install_crate = "cargo-all-features"
[tasks.start-client]
command = "cargo"
args = ["leptos", "watch"]
[tasks.stop-client]
condition = { env_set = ["APP_PROCESS_NAME"] }
script = '''
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
pkill -f todo_app_sqlite
fi
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
pkill -f cargo-leptos
fi
'''
[tasks.client-status]
condition = { env_set = ["APP_PROCESS_NAME"] }
script = '''
if [ -z $(pidof ${APP_PROCESS_NAME}) ]; then
echo " ${APP_PROCESS_NAME} is not running"
else
echo " ${APP_PROCESS_NAME} is up"
fi
if [ -z $(pidof cargo-leptos) ]; then
echo " cargo-leptos is not running"
else
echo " cargo-leptos is up"
fi
'''

View File

@@ -12,7 +12,7 @@ args = ["-rf", "target"]
[tasks.clean-trunk]
script = '''
find . -type d -name target | xargs rm -rf
find . -type d -name dist | xargs rm -rf
'''
[tasks.clean-node_modules]

View File

@@ -0,0 +1,35 @@
[tasks.start-client]
[tasks.stop-client]
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
script = '''
if [ ! -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
pkill -ef ${CLIENT_PROCESS_NAME}
fi
'''
[tasks.client-status]
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
script = '''
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
echo " ${CLIENT_PROCESS_NAME} is not running"
else
echo " ${CLIENT_PROCESS_NAME} is up"
fi
'''
[tasks.maybe-start-client]
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
script = '''
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
echo " Starting ${CLIENT_PROCESS_NAME}"
cargo make start-client ${@} &
else
echo " ${CLIENT_PROCESS_NAME} is already started"
fi
'''
# ALIASES
[tasks.dev]
alias = "maybe-start-client"

View File

@@ -0,0 +1,17 @@
extend = [
{ path = "../cargo-make/playwright.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]
[tasks.integration-test]
dependencies = [
"maybe-start-client",
"wait-one",
"test-playwright",
"stop-client",
]
[tasks.wait-one]
script = '''
sleep 1
'''

View File

@@ -1,18 +1,12 @@
extend = { path = "../cargo-make/client-process.toml" }
[env]
CLIENT_PROCESS_NAME = "trunk"
[tasks.build]
command = "trunk"
args = ["build"]
[tasks.start-trunk]
[tasks.start-client]
command = "trunk"
args = ["serve", "${@}"]
[tasks.stop-trunk]
script = '''
pkill -f "cargo-make"
pkill -f "trunk"
'''
# ALIASES
[tasks.dev]
dependencies = ["start-trunk"]

View File

@@ -1,4 +1,5 @@
use leptos::*;
use leptos::{SignalWrite, *};
use std::cell::{Ref, RefMut};
/// A simple counter component.
///
@@ -12,12 +13,20 @@ pub fn SimpleCounter(
) -> impl IntoView {
let (value, set_value) = create_signal(initial_value);
let something: Ref<'_, i32> = value.read();
spawn_local(async move {
let mut something_else: RefMut<'_, i32> = set_value.write();
async {}.await;
*something_else = 30;
});
view! {
<div>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
<button on:click=move |_| *set_value.write() -= step>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
<button on:click=move |_| *set_value.write() += step>"+1"</button>
</div>
}
}

View File

@@ -3,7 +3,7 @@ use leptos::{ev, html::*, *};
/// A simple counter view.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
let (count, set_count) = create_signal(Count::new(initial_value, step));
let count = RwSignal::new(Count::new(initial_value, step));
// the function name is the same as the HTML tag name
div()
@@ -17,18 +17,14 @@ pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_count.update(|count| count.clear()))
.on(ev::click, move |_| count.update(Count::clear))
.child("Clear"),
button()
.on(ev::click, move |_| {
set_count.update(|count| count.decrease())
})
.on(ev::click, move |_| count.update(Count::decrease))
.child("-1"),
span().child(("Value: ", move || count.get().value(), "!")),
button()
.on(ev::click, move |_| {
set_count.update(|count| count.increase())
})
.on(ev::click, move |_| count.update(Count::increase))
.child("+1"),
))
}

20
examples/error_boundary/.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# Support playwright testing
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
pnpm-lock.yaml
# Support trunk
dist

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
{
"private": "true",
"scripts": {},
"devDependencies": {
"@playwright/test": "^1.35.1"
}
}

View File

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

View File

@@ -0,0 +1,23 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Clear Number", () => {
test("should see the error message", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clearInput();
await expect(ui.errorMessage).toHaveText("Not a number! Errors: ");
});
test("should see the error list", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clearInput();
await expect(ui.errorList).toHaveText(
"cannot parse integer from empty string"
);
});
});

View File

@@ -0,0 +1,17 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Click Down Arrow", () => {
test("should see the negative number", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clickDownArrow();
await ui.clickDownArrow();
await ui.clickDownArrow();
await ui.clickDownArrow();
await ui.clickDownArrow();
await expect(ui.successMessage).toHaveText("You entered -5");
});
});

View File

@@ -0,0 +1,15 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Click Up Arrow", () => {
test("should see the positive number", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clickUpArrow();
await ui.clickUpArrow();
await ui.clickUpArrow();
await expect(ui.successMessage).toHaveText("You entered 3");
});
});

View File

@@ -0,0 +1,56 @@
import { expect, Locator, Page } from "@playwright/test";
export class HomePage {
readonly page: Page;
readonly pageTitle: Locator;
readonly numberInput: Locator;
readonly successMessage: Locator;
readonly errorMessage: Locator;
readonly errorList: Locator;
constructor(page: Page) {
this.page = page;
this.pageTitle = page.locator("h1");
this.numberInput = page.getByLabel(
"Type a number (or something that's not a number!)"
);
this.successMessage = page.locator("label p");
this.errorMessage = page.locator("div p");
this.errorList = page.getByRole("list");
}
async goto() {
await this.page.goto("/");
}
async enterNumber(count: string, index: number = 0) {
await Promise.all([
this.numberInput.waitFor(),
this.numberInput.fill(count),
]);
}
async clickUpArrow() {
await Promise.all([
this.numberInput.waitFor(),
this.numberInput.press("ArrowUp"),
]);
}
async clickDownArrow() {
await Promise.all([
this.numberInput.waitFor(),
this.numberInput.press("ArrowDown"),
]);
}
async clearInput() {
await Promise.all([
this.numberInput.waitFor(),
this.clickUpArrow(),
this.numberInput.press("Backspace"),
]);
}
}

View File

@@ -0,0 +1,11 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Open App", () => {
test("should see the page title", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await expect(ui.pageTitle).toHaveText("Error Handling");
});
});

View File

@@ -0,0 +1,13 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Type Number", () => {
test("should see the typed number", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.enterNumber("7");
await expect(ui.successMessage).toHaveText("You entered 7");
});
});

View File

@@ -1,6 +1,6 @@
use crate::errors::AppError;
use cfg_if::cfg_if;
use leptos::{Errors, *};
use leptos::{logging::log, Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;

View File

@@ -12,7 +12,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
Router,
};
use errors_axum::*;
use leptos::*;
use leptos::{logging::log, *};
use leptos_axum::{generate_route_list, LeptosRoutes};
}}

View File

@@ -1,5 +1,5 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos::{logging::log, *};
// boilerplate to run in different modes
cfg_if! {

View File

@@ -51,8 +51,11 @@ site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style/output.css"
# The tailwind input file.
#
# Optional, Activates the tailwind build
tailwind-input-file = "style/tailwind.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.

View File

@@ -1,642 +0,0 @@
/*
! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
*/
html {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.relative {
position: relative;
}
.m-1 {
margin: 0.25rem;
}
.m-auto {
margin: auto;
}
.flex {
display: flex;
}
.h-5 {
height: 1.25rem;
}
.min-h-screen {
min-height: 100vh;
}
.w-5 {
width: 1.25rem;
}
.flex-row-reverse {
flex-direction: row-reverse;
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.rounded {
border-radius: 0.25rem;
}
.border-b-4 {
border-bottom-width: 4px;
}
.border-l-2 {
border-left-width: 2px;
}
.border-blue-800 {
--tw-border-opacity: 1;
border-color: rgb(30 64 175 / var(--tw-border-opacity));
}
.border-blue-900 {
--tw-border-opacity: 1;
border-color: rgb(30 58 138 / var(--tw-border-opacity));
}
.bg-blue-700 {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
}
.bg-blue-800 {
--tw-bg-opacity: 1;
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
}
.bg-gradient-to-tl {
background-image: linear-gradient(to top left, var(--tw-gradient-stops));
}
.from-blue-800 {
--tw-gradient-from: #1e40af var(--tw-gradient-from-position);
--tw-gradient-to: rgb(30 64 175 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-blue-500 {
--tw-gradient-to: #3b82f6 var(--tw-gradient-to-position);
}
.fill-current {
fill: currentColor;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.text-center {
text-align: center;
}
.font-mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.shadow-lg {
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}

View File

@@ -38,7 +38,7 @@ where
}
api::Error::Api(err) => err.message,
};
error!(
log::error!(
"Unable to login with {}: {msg}",
credentials.email
);

View File

@@ -4,7 +4,7 @@ use crate::{
Page,
};
use api_boundary::*;
use leptos::*;
use leptos::{logging::log, *};
use leptos_router::*;
#[component]

View File

@@ -1,6 +1,6 @@
mod api;
use crate::api::*;
use leptos::*;
use leptos::{logging::log, *};
use leptos_router::*;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -122,9 +122,9 @@ pub struct ContactParams {
#[component]
pub fn Contact() -> impl IntoView {
log::debug!("rendering <Contact/>");
log!("rendering <Contact/>");
log::debug!(
log!(
"ExampleContext should be Some(42). It is {:?}",
use_context::<ExampleContext>()
);
@@ -178,13 +178,13 @@ pub fn Contact() -> impl IntoView {
#[component]
pub fn About() -> impl IntoView {
log::debug!("rendering <About/>");
log!("rendering <About/>");
on_cleanup(|| {
log!("cleaning up <About/>");
});
log::debug!(
log!(
"ExampleContext should be Some(0). It is {:?}",
use_context::<ExampleContext>()
);
@@ -209,7 +209,7 @@ pub fn About() -> impl IntoView {
#[component]
pub fn Settings() -> impl IntoView {
log::debug!("rendering <Settings/>");
log!("rendering <Settings/>");
on_cleanup(|| {
log!("cleaning up <Settings/>");

View File

@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
use session_auth_axum::state::AppState;
use session_auth_axum::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
use leptos::{log, view, provide_context, get_configuration};
use leptos::{logging::log, view, provide_context, get_configuration};
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};

View File

@@ -2,7 +2,7 @@
#[tokio::main]
async fn main() {
use axum::{routing::post, Router};
use leptos::*;
use leptos::{logging::log, *};
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};

View File

@@ -1,22 +1,10 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/webdriver.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
]
[env]
APP_PROCESS_NAME = "suspense_tests"
[tasks.integration-test]
dependencies = [
"install-cargo-leptos",
"start-webdriver",
"test-e2e-with-auto-start",
]
[tasks.test-e2e-with-auto-start]
command = "cargo"
args = ["leptos", "end-to-end"]
CLIENT_PROCESS_NAME = "suspense_tests"
[tasks.test-ui]
cwd = "./e2e"

View File

@@ -156,7 +156,7 @@ fn NestedResourceInside() -> impl IntoView {
{move || {
one_second.get().map(|_| {
let two_second = create_resource(|| (), move |_| async move {
leptos::log!("creating two_second resource");
logging::log!("creating two_second resource");
second_wait_fn(WAIT_TWO_SECONDS).await
});
view! {

View File

@@ -84,9 +84,10 @@ site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style/output.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
# The tailwind input file.
#
# Optional, Activates the tailwind build
tailwind-input-file = "style/tailwind.css"
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"

View File

@@ -1,4 +1,8 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/playwright.toml" },
{ path = "../cargo-make/cargo-leptos-test.toml" },
]
[env]
CLIENT_PROCESS_NAME = "tailwind"

View File

@@ -8,10 +8,6 @@ If you don't have `cargo-leptos` installed you can install it with
Then run
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
and
`cargo leptos watch`
in this directory.

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -12,7 +12,7 @@ cfg_if! {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log!("hydrate mode - hydrating");
logging::log!("hydrate mode - hydrating");
leptos::mount_to_body(|| {
view! { <App/> }
@@ -29,7 +29,7 @@ cfg_if! {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log!("csr mode - mounting to body");
logging::log!("csr mode - mounting to body");
mount_to_body(|| {
view! { <App /> }

View File

@@ -1,650 +0,0 @@
/*
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
*/
html {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.my-0 {
margin-top: 0px;
margin-bottom: 0px;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.max-w-3xl {
max-width: 48rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
.rounded-sm {
border-radius: 0.125rem;
}
.bg-amber-600 {
--tw-bg-opacity: 1;
background-color: rgb(217 119 6 / var(--tw-bg-opacity));
}
.bg-sky-600 {
--tw-bg-opacity: 1;
background-color: rgb(2 132 199 / var(--tw-bg-opacity));
}
.bg-sky-300 {
--tw-bg-opacity: 1;
background-color: rgb(125 211 252 / var(--tw-bg-opacity));
}
.bg-sky-500 {
--tw-bg-opacity: 1;
background-color: rgb(14 165 233 / var(--tw-bg-opacity));
}
.bg-amber-500 {
--tw-bg-opacity: 1;
background-color: rgb(245 158 11 / var(--tw-bg-opacity));
}
.p-6 {
padding: 1.5rem;
}
.px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
.px-5 {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.pb-10 {
padding-bottom: 2.5rem;
}
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.text-red-200 {
--tw-text-opacity: 1;
color: rgb(254 202 202 / var(--tw-text-opacity));
}
.text-sky-500 {
--tw-text-opacity: 1;
color: rgb(14 165 233 / var(--tw-text-opacity));
}
.text-sky-300 {
--tw-text-opacity: 1;
color: rgb(125 211 252 / var(--tw-text-opacity));
}
.text-sky-700 {
--tw-text-opacity: 1;
color: rgb(3 105 161 / var(--tw-text-opacity));
}
.text-sky-800 {
--tw-text-opacity: 1;
color: rgb(7 89 133 / var(--tw-text-opacity));
}
.text-red-800 {
--tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity));
}
.hover\:bg-sky-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
}

View File

@@ -7,7 +7,7 @@ pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log!("csr mode - mounting to body");
logging::log!("csr mode - mounting to body");
mount_to_body(|| {
view! { <App /> }

View File

@@ -1,22 +1,10 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/webdriver.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
]
[env]
APP_PROCESS_NAME = "todo_app_sqlite"
[tasks.integration-test]
dependencies = [
"install-cargo-leptos",
"start-webdriver",
"test-e2e-with-auto-start",
]
[tasks.test-e2e-with-auto-start]
command = "cargo"
args = ["leptos", "end-to-end"]
CLIENT_PROCESS_NAME = "todo_app_sqlite"
[tasks.test-ui]
cwd = "./e2e"

View File

@@ -1,22 +1,10 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/webdriver.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
]
[env]
APP_PROCESS_NAME = "todo_app_sqlite_axum"
[tasks.integration-test]
dependencies = [
"install-cargo-leptos",
"start-webdriver",
"test-e2e-with-auto-start",
]
[tasks.test-e2e-with-auto-start]
command = "cargo"
args = ["leptos", "end-to-end"]
CLIENT_PROCESS_NAME = "todo_app_sqlite_axum"
[tasks.test-ui]
cwd = "./e2e"

View File

@@ -60,7 +60,7 @@ cfg_if! {
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
logging::log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await

View File

@@ -67,7 +67,7 @@ cfg_if! {
// run our app with hyper
// `viz::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
logging::log!("listening on http://{}", &addr);
viz::Server::bind(&addr)
.serve(ServiceMaker::from(app))
.await

View File

@@ -11,14 +11,17 @@ fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
Some(val) => val,
None => options.reload_port,
};
let protocol = match options.reload_ws_protocol {
leptos_config::ReloadWSProtocol::WS => "'ws://'",
leptos_config::ReloadWSProtocol::WSS => "'wss://'",
};
match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin=""{nonce_str}>(function () {{
{}
let protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
let host = window.location.hostname;
let ws = new WebSocket(protocol + host + ':{reload_port}/live_reload');
let ws = new WebSocket({protocol} + host + ':{reload_port}/live_reload');
ws.onmessage = (ev) => {{
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();

View File

@@ -22,9 +22,6 @@ server_fn = { workspace = true }
web-sys = { version = "0.3.63", optional = true }
wasm-bindgen = { version = "0.2", optional = true }
[dev-dependencies]
leptos = { path = "." }
[features]
default = ["serde"]
template_macro = ["leptos_dom/web", "web-sys", "wasm-bindgen"]

62
leptos/src/children.rs Normal file
View File

@@ -0,0 +1,62 @@
use leptos_dom::Fragment;
use std::rc::Rc;
/// The most common type for the `children` property on components,
/// which can only be called once.
pub type Children = Box<dyn FnOnce() -> Fragment>;
/// A type for the `children` property on components that can be called
/// more than once.
pub type ChildrenFn = Rc<dyn Fn() -> Fragment>;
/// A type for the `children` property on components that can be called
/// more than once, but may mutate the children.
pub type ChildrenFnMut = Box<dyn FnMut() -> Fragment>;
// This is to still support components that accept `Box<dyn Fn() -> Fragment>` as a children.
type BoxedChildrenFn = Box<dyn Fn() -> Fragment>;
#[doc(hidden)]
pub trait ToChildren<F> {
fn to_children(f: F) -> Self;
}
impl<F> ToChildren<F> for Children
where
F: FnOnce() -> Fragment + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(f)
}
}
impl<F> ToChildren<F> for ChildrenFn
where
F: Fn() -> Fragment + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Rc::new(f)
}
}
impl<F> ToChildren<F> for ChildrenFnMut
where
F: FnMut() -> Fragment + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(f)
}
}
impl<F> ToChildren<F> for BoxedChildrenFn
where
F: Fn() -> Fragment + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(f)
}
}

View File

@@ -77,7 +77,7 @@ where
matches!(child, View::Suspense(_, _))
|| matches!(child, View::Component(repr) if repr.name() == "Transition")
}) {
crate::debug_warn!("You are using a <Suspense/> or \
leptos_dom::logging::console_warn("You are using a <Suspense/> or \
<Transition/> as the direct child of an <ErrorBoundary/>. To ensure correct \
hydration, these should be reorganized so that the <ErrorBoundary/> is a child \
of the <Suspense/> or <Transition/> instead: \n\

View File

@@ -18,7 +18,7 @@ use std::hash::Hash;
///
/// #[component]
/// fn Counters() -> impl IntoView {
/// let (counters, set_counters) = create_signal::<Vec<Counter>>( vec![]);
/// let (counters, set_counters) = create_signal::<Vec<Counter>>(vec![]);
///
/// view! {
/// <div>
@@ -28,7 +28,7 @@ use std::hash::Hash;
/// // a unique key for each item
/// key=|counter| counter.id
/// // renders each item to a view
/// view=move | counter: Counter| {
/// view=move |counter: Counter| {
/// view! {
/// <button>"Value: " {move || counter.count.get()}</button>
/// }

View File

@@ -155,11 +155,15 @@ pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
pub use leptos_dom::{
self, create_node_ref, debug_warn, document, error, ev, helpers::*, html,
log, math, mount_to, mount_to_body, nonce, svg, warn, window, Attribute,
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
self, create_node_ref, document, ev, helpers::*, html, math, mount_to,
mount_to_body, nonce, svg, window, Attribute, Class, CollectView, Errors,
Fragment, HtmlElement, IntoAttribute, IntoClass, IntoProperty, IntoStyle,
IntoView, NodeRef, Property, View,
};
/// Utilities for simple isomorphic logging to the console or terminal.
pub mod logging {
pub use leptos_dom::{debug_warn, error, log, warn};
}
/// Types to make it easier to handle errors in your application.
pub mod error {
@@ -200,20 +204,10 @@ pub use typed_builder;
pub use typed_builder::Optional;
#[doc(hidden)]
pub use typed_builder_macro;
mod children;
pub use children::*;
extern crate self as leptos;
/// The most common type for the `children` property on components,
/// which can only be called once.
pub type Children = Box<dyn FnOnce() -> Fragment>;
/// A type for the `children` property on components that can be called
/// more than once.
pub type ChildrenFn = Box<dyn Fn() -> Fragment>;
/// A type for the `children` property on components that can be called
/// more than once, but may mutate the children.
pub type ChildrenFnMut = Box<dyn FnMut() -> Fragment>;
/// A type for taking anything that implements [`IntoAttribute`].
///
/// ```rust

View File

@@ -1,5 +1,5 @@
use leptos::component;
use leptos_dom::{Fragment, IntoView};
use leptos::{component, ChildrenFn};
use leptos_dom::IntoView;
use leptos_reactive::{create_memo, signal_prelude::*};
/// A component that will show its children when the `when` condition is `true`,
@@ -38,7 +38,7 @@ pub fn Show<F, W, IV>(
/// The scope the component is running in
/// The components Show wraps
children: Box<dyn Fn() -> Fragment>,
children: ChildrenFn,
/// A closure that returns a bool that determines whether this thing runs
when: W,
/// A closure that returns what gets rendered if the when statement is false

View File

@@ -1,5 +1,7 @@
use leptos_dom::{DynChild, HydrationCtx, IntoView};
use leptos_macro::component;
#[cfg(any(feature = "csr", feature = "hydrate"))]
use leptos_reactive::SignalGet;
use leptos_reactive::{
create_memo, provide_context, SignalGetUntracked, SuspenseContext,
};
@@ -59,14 +61,14 @@ pub fn Suspense<F, E, V>(
/// Returns a fallback UI that will be shown while `async` [`Resource`](leptos_reactive::Resource)s are still loading.
fallback: F,
/// Children will be displayed once all `async` [`Resource`](leptos_reactive::Resource)s have resolved.
children: Box<dyn Fn() -> V>,
children: Rc<dyn Fn() -> V>,
) -> impl IntoView
where
F: Fn() -> E + 'static,
E: IntoView,
V: IntoView + 'static,
{
let orig_children = Rc::new(children);
let orig_children = children;
let context = SuspenseContext::new();
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
@@ -93,6 +95,9 @@ where
let current_id = HydrationCtx::next_component();
#[cfg(any(feature = "csr", feature = "hydrate"))]
let ready = context.ready();
let child = DynChild::new({
move || {
// pull lazy memo before checking if context is ready
@@ -100,7 +105,7 @@ where
#[cfg(any(feature = "csr", feature = "hydrate"))]
{
if context.ready() {
if ready.get() {
children_rendered
} else {
fallback.get_untracked()

View File

@@ -99,7 +99,9 @@ where
cfg!(feature = "csr") && first_run.get();
let is_first_run =
is_first_run(first_run, &suspense_context);
first_run.set(false);
if was_first_run {
first_run.set(false)
}
if let Some(prev_children) = &*prev_child.borrow() {
if is_first_run || was_first_run {
@@ -112,7 +114,7 @@ where
}
}
})
.children(Box::new(move || {
.children(Rc::new(move || {
let frag = children().into_view();
if let Some(suspense_context) = use_context::<SuspenseContext>()

View File

@@ -61,6 +61,11 @@ pub struct LeptosOptions {
#[builder(default)]
#[serde(default)]
pub reload_external_port: Option<u32>,
/// The protocol the Websocket watcher uses on the client: `ws` in most cases, `wss` when behind a reverse https proxy.
/// Defaults to `ws`
#[builder(default)]
#[serde(default)]
pub reload_ws_protocol: ReloadWSProtocol,
}
impl LeptosOptions {
@@ -84,7 +89,7 @@ impl LeptosOptions {
output_name,
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
env: Env::default(),
env: env_from_str(env_w_default("LEPTOS_ENV", "DEV")?.as_str())?,
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
.parse()?,
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?
@@ -95,6 +100,9 @@ impl LeptosOptions {
Some(val) => Some(val.parse()?),
None => None,
},
reload_ws_protocol: ws_from_str(
env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
)?,
})
}
}
@@ -151,45 +159,103 @@ impl Default for Env {
}
}
fn from_str(input: &str) -> Result<Env, String> {
fn env_from_str(input: &str) -> Result<Env, LeptosConfigError> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"dev" | "development" => Ok(Env::DEV),
"prod" | "production" => Ok(Env::PROD),
_ => Err(format!(
_ => Err(LeptosConfigError::EnvVarError(format!(
"{input} is not a supported environment. Use either `dev` or \
`production`.",
)),
))),
}
}
impl FromStr for Env {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
from_str(input).or_else(|_| Ok(Self::default()))
env_from_str(input).or_else(|_| Ok(Self::default()))
}
}
impl From<&str> for Env {
fn from(str: &str) -> Self {
from_str(str).unwrap_or_else(|err| panic!("{}", err))
env_from_str(str).unwrap_or_else(|err| panic!("{}", err))
}
}
impl From<&Result<String, VarError>> for Env {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => from_str(str).unwrap_or_else(|err| panic!("{}", err)),
Ok(str) => {
env_from_str(str).unwrap_or_else(|err| panic!("{}", err))
}
Err(_) => Self::default(),
}
}
}
impl TryFrom<String> for Env {
type Error = String;
type Error = LeptosConfigError;
fn try_from(s: String) -> Result<Self, Self::Error> {
from_str(s.as_str())
env_from_str(s.as_str())
}
}
/// An enum that can be used to define the websocket protocol Leptos uses for hotreloading
/// Defaults to `ws`.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub enum ReloadWSProtocol {
WS,
WSS,
}
impl Default for ReloadWSProtocol {
fn default() -> Self {
Self::WS
}
}
fn ws_from_str(input: &str) -> Result<ReloadWSProtocol, LeptosConfigError> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"ws" | "WS" => Ok(ReloadWSProtocol::WS),
"wss" | "WSS" => Ok(ReloadWSProtocol::WSS),
_ => Err(LeptosConfigError::EnvVarError(format!(
"{input} is not a supported websocket protocol. Use only `ws` or \
`wss`.",
))),
}
}
impl FromStr for ReloadWSProtocol {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
ws_from_str(input).or_else(|_| Ok(Self::default()))
}
}
impl From<&str> for ReloadWSProtocol {
fn from(str: &str) -> Self {
ws_from_str(str).unwrap_or_else(|err| panic!("{}", err))
}
}
impl From<&Result<String, VarError>> for ReloadWSProtocol {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => ws_from_str(str).unwrap_or_else(|err| panic!("{}", err)),
Err(_) => Self::default(),
}
}
}
impl TryFrom<String> for ReloadWSProtocol {
type Error = LeptosConfigError;
fn try_from(s: String) -> Result<Self, Self::Error> {
ws_from_str(s.as_str())
}
}

View File

@@ -1,18 +1,31 @@
use crate::{env_w_default, env_wo_default, from_str, Env, LeptosOptions};
use crate::{
env_from_str, env_w_default, env_wo_default, ws_from_str, Env,
LeptosOptions, ReloadWSProtocol,
};
use std::{net::SocketAddr, str::FromStr};
#[test]
fn from_str_env() {
assert!(matches!(from_str("dev").unwrap(), Env::DEV));
assert!(matches!(from_str("development").unwrap(), Env::DEV));
assert!(matches!(from_str("DEV").unwrap(), Env::DEV));
assert!(matches!(from_str("DEVELOPMENT").unwrap(), Env::DEV));
assert!(matches!(from_str("prod").unwrap(), Env::PROD));
assert!(matches!(from_str("production").unwrap(), Env::PROD));
assert!(matches!(from_str("PROD").unwrap(), Env::PROD));
assert!(matches!(from_str("PRODUCTION").unwrap(), Env::PROD));
assert!(from_str("TEST").is_err());
assert!(from_str("?").is_err());
fn env_from_str_test() {
assert!(matches!(env_from_str("dev").unwrap(), Env::DEV));
assert!(matches!(env_from_str("development").unwrap(), Env::DEV));
assert!(matches!(env_from_str("DEV").unwrap(), Env::DEV));
assert!(matches!(env_from_str("DEVELOPMENT").unwrap(), Env::DEV));
assert!(matches!(env_from_str("prod").unwrap(), Env::PROD));
assert!(matches!(env_from_str("production").unwrap(), Env::PROD));
assert!(matches!(env_from_str("PROD").unwrap(), Env::PROD));
assert!(matches!(env_from_str("PRODUCTION").unwrap(), Env::PROD));
assert!(env_from_str("TEST").is_err());
assert!(env_from_str("?").is_err());
}
#[test]
fn ws_from_str_test() {
assert!(matches!(ws_from_str("ws").unwrap(), ReloadWSProtocol::WS));
assert!(matches!(ws_from_str("WS").unwrap(), ReloadWSProtocol::WS));
assert!(matches!(ws_from_str("wss").unwrap(), ReloadWSProtocol::WSS));
assert!(matches!(ws_from_str("WSS").unwrap(), ReloadWSProtocol::WSS));
assert!(ws_from_str("TEST").is_err());
assert!(ws_from_str("?").is_err());
}
#[test]
@@ -49,6 +62,8 @@ fn try_from_env_test() {
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80");
std::env::set_var("LEPTOS_RELOAD_PORT", "8080");
std::env::set_var("LEPTOS_RELOAD_EXTERNAL_PORT", "8080");
std::env::set_var("LEPTOS_ENV", "PROD");
std::env::set_var("LEPTOS_RELOAD_WS_PROTOCOL", "WSS");
let config = LeptosOptions::try_from_env().unwrap();
assert_eq!(config.output_name, "app_test");
@@ -61,4 +76,6 @@ fn try_from_env_test() {
);
assert_eq!(config.reload_port, 8080);
assert_eq!(config.reload_external_port, Some(8080));
assert_eq!(config.env, Env::PROD);
assert_eq!(config.reload_ws_protocol, ReloadWSProtocol::WSS)
}

323
leptos_dom/src/callback.rs Normal file
View File

@@ -0,0 +1,323 @@
//! Callbacks define a standard way to store functions and closures,
//! in particular for component properties.
//!
//! # How to use them
//! You can always create a callback from a closure, but the prefered way is to use `prop(into)`
//! when you define your component:
//! ```
//! # use leptos::*;
//! # use leptos::leptos_dom::{Callback, Callable};
//! #[component]
//! fn MyComponent(
//! #[prop(into)] render_number: Callback<i32, String>,
//! ) -> impl IntoView {
//! view! {
//! <div>
//! {render_number.call(42)}
//! </div>
//! }
//! }
//! // now you can use it from a closure directly:
//! fn test() -> impl IntoView {
//! view! {
//! <MyComponent render_number = |x: i32| x.to_string()/>
//! }
//! }
//! ```
//!
//! *Notes*:
//! - in this example, you should use a generic type that implements `Fn(i32) -> String`.
//! Callbacks are more usefull when you want optional generic props.
//! - All callbacks implement the `Callable` trait. You have to write `my_callback.call(input)`
//!
//!
//! # Types
//! This modules defines:
//! - [Callback], the most basic callback type
//! - [SyncCallback] for scenarios when you need `Send` and `Sync`
//! - [HtmlCallback] for a function that returns a [HtmlElement]
//! - [ViewCallback] for a function that returns some kind of [view][IntoView]
//!
//! # Copying vs cloning
//! All callbacks type defined in this module are [Clone] but not [Copy].
//! To solve this issue, use [StoredValue]; see [StoredCallback] for more
//! ```
//! # use leptos::*;
//! # use leptos::leptos_dom::{Callback, Callable};
//! fn test() -> impl IntoView {
//! let callback: Callback<i32, String> =
//! Callback::new(|x: i32| x.to_string());
//! let stored_callback = store_value(callback);
//!
//! view! {
//! <div>
//! // `stored_callback` can be moved multiple times
//! {move || stored_callback.call(1)}
//! {move || stored_callback.call(42)}
//! </div>
//! }
//! }
//! ```
//!
//! Note that for each callback type `T`, `StoredValue<T>` implements `Call`, so you can call them
//! without even thinking about it.
use crate::{AnyElement, ElementDescriptor, HtmlElement, IntoView, View};
use leptos_reactive::StoredValue;
use std::{rc::Rc, sync::Arc};
/// A wrapper trait for calling callbacks.
pub trait Callable<In, Out = ()> {
/// calls the callback with the specified argument.
fn call(&self, input: In) -> Out;
}
/// The most basic leptos callback type.
/// For how to use callbacks, see [here][crate::callback]
///
/// # Example
/// ```
/// # use leptos::*;
/// # use leptos::leptos_dom::{Callable, Callback};
/// #[component]
/// fn MyComponent(
/// #[prop(into)] render_number: Callback<i32, String>,
/// ) -> impl IntoView {
/// view! {
/// <div>
/// {render_number.call(42)}
/// </div>
/// }
/// }
///
/// fn test() -> impl IntoView {
/// view! {
/// <MyComponent render_number=move |x: i32| x.to_string()/>
/// }
/// }
/// ```
///
/// # Cloning
/// See [StoredCallback]
#[derive(Clone)]
pub struct Callback<In, Out = ()>(Rc<dyn Fn(In) -> Out>);
impl<In, Out> Callback<In, Out> {
/// creates a new callback from the function or closure
pub fn new<F>(f: F) -> Callback<In, Out>
where
F: Fn(In) -> Out + 'static,
{
Self(Rc::new(f))
}
}
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
fn call(&self, input: In) -> Out {
(self.0)(input)
}
}
impl<F, In, Out> From<F> for Callback<In, Out>
where
F: Fn(In) -> Out + 'static,
{
fn from(f: F) -> Callback<In, Out> {
Callback::new(f)
}
}
/// A callback type that implements `Copy`.
/// `StoredCallback<In,Out>` is an alias for `StoredValue<Callback<In, Out>>`.
///
/// # Example
/// ```
/// # use leptos::*;
/// # use leptos::leptos_dom::{Callback, StoredCallback, Callable};
/// fn test() -> impl IntoView {
/// let callback: Callback<i32, String> =
/// Callback::new(|x: i32| x.to_string());
/// let stored_callback: StoredCallback<i32, String> =
/// store_value(callback);
/// view! {
/// <div>
/// {move || stored_callback.call(1)}
/// {move || stored_callback.call(42)}
/// </div>
/// }
/// }
/// ```
///
/// Note that in this example, you can replace `Callback` by `SyncCallback` or `ViewCallback`, and
/// it will work in the same way.
///
///
/// Note that a prop should never be a [StoredCallback]:
/// you have to call [store_value][leptos_reactive::store_value] inside your component code.
pub type StoredCallback<In, Out> = StoredValue<Callback<In, Out>>;
impl<F, In, Out> Callable<In, Out> for StoredValue<F>
where
F: Callable<In, Out>,
{
fn call(&self, input: In) -> Out {
self.with_value(|cb| cb.call(input))
}
}
/// a callback type that is `Send` and `Sync` if the input type is
#[derive(Clone)]
pub struct SyncCallback<In, Out = ()>(Arc<dyn Fn(In) -> Out>);
impl<In: 'static, Out: 'static> SyncCallback<In, Out> {
/// creates a new callback from the function or closure
pub fn new<F>(fun: F) -> Self
where
F: Fn(In) -> Out + 'static,
{
Self(Arc::new(fun))
}
}
/// A special callback type that returns any Html element.
/// You can use it exactly the same way as a classic callback.
///
/// For how to use callbacks, see [here][crate::callback]
///
/// # Example
///
/// ```
/// # use leptos::*;
/// # use leptos::leptos_dom::{Callable, HtmlCallback};
/// #[component]
/// fn MyComponent(
/// #[prop(into)] render_number: HtmlCallback<i32>,
/// ) -> impl IntoView {
/// view! {
/// <div>
/// {render_number.call(42)}
/// </div>
/// }
/// }
/// fn test() -> impl IntoView {
/// view! {
/// <MyComponent render_number=move |x: i32| view!{<span>{x}</span>}/>
/// }
/// }
/// ```
///
/// # `HtmlCallback` with empty input type.
/// Note that when `my_html_callback` is `HtmlCallback<()>`, you can use it more easily because it
/// implements [IntoView]
///
/// view!{
/// <div>
/// {render_number}
/// </div>
/// }
#[derive(Clone)]
pub struct HtmlCallback<In = ()>(Rc<dyn Fn(In) -> HtmlElement<AnyElement>>);
impl<In> HtmlCallback<In> {
/// creates a new callback from the function or closure
pub fn new<F, H>(f: F) -> Self
where
F: Fn(In) -> HtmlElement<H> + 'static,
H: ElementDescriptor + 'static,
{
Self(Rc::new(move |x| f(x).into_any()))
}
}
impl<In> Callable<In, HtmlElement<AnyElement>> for HtmlCallback<In> {
fn call(&self, input: In) -> HtmlElement<AnyElement> {
(self.0)(input)
}
}
impl<In, F, H> From<F> for HtmlCallback<In>
where
F: Fn(In) -> HtmlElement<H> + 'static,
H: ElementDescriptor + 'static,
{
fn from(f: F) -> Self {
HtmlCallback(Rc::new(move |x| f(x).into_any()))
}
}
impl IntoView for HtmlCallback<()> {
fn into_view(self) -> View {
self.call(()).into_view()
}
}
/// A special callback type that returns any [`View`].
///
/// You can use it exactly the same way as a classic callback.
/// For how to use callbacks, see [here][crate::callback]
///
/// ```
/// # use leptos::*;
/// # use leptos::leptos_dom::{ViewCallback, Callable};
/// #[component]
/// fn MyComponent(
/// #[prop(into)] render_number: ViewCallback<i32>,
/// ) -> impl IntoView {
/// view! {
/// <div>
/// {render_number.call(42)}
/// </div>
/// }
/// }
/// fn test() -> impl IntoView {
/// view! {
/// <MyComponent render_number=move |x: i32| view!{<span>{x}</span>}/>
/// }
/// }
/// ```
///
/// # `ViewCallback` with empty input type.
/// Note that when `my_view_callback` is `ViewCallback<()>`, you can use it more easily because it
/// implements [IntoView]
///
/// view!{
/// <div>
/// {render_number}
/// </div>
/// }
#[derive(Clone)]
pub struct ViewCallback<In>(Rc<dyn Fn(In) -> View>);
impl<In> ViewCallback<In> {
/// creates a new callback from the function or closure
fn new<F, V>(f: F) -> Self
where
F: Fn(In) -> V + 'static,
V: IntoView + 'static,
{
ViewCallback(Rc::new(move |x| f(x).into_view()))
}
}
impl<In> Callable<In, View> for ViewCallback<In> {
fn call(&self, input: In) -> View {
(self.0)(input)
}
}
impl<In, F, V> From<F> for ViewCallback<In>
where
F: Fn(In) -> V + 'static,
V: IntoView + 'static,
{
fn from(f: F) -> Self {
Self::new(f)
}
}
impl IntoView for ViewCallback<()> {
fn into_view(self) -> View {
self.call(()).into_view()
}
}

View File

@@ -19,6 +19,7 @@ pub fn set_property(
}
/// Gets the value of a property set on a DOM element.
#[doc(hidden)]
pub fn get_property(
el: &web_sys::Element,
prop_name: &str,
@@ -245,7 +246,7 @@ pub fn set_timeout_with_handle(
/// listeners to prevent them from firing constantly as you type.
///
/// ```
/// use leptos::{leptos_dom::helpers::debounce, *};
/// use leptos::{leptos_dom::helpers::debounce, logging::log, *};
///
/// #[component]
/// fn DebouncedButton() -> impl IntoView {
@@ -429,7 +430,7 @@ pub fn window_event_listener_untyped(
/// Creates a window event listener from a typed event, returning a
/// cancelable handle.
/// ```
/// use leptos::{leptos_dom::helpers::window_event_listener, *};
/// use leptos::{leptos_dom::helpers::window_event_listener, logging::log, *};
///
/// #[component]
/// fn App() -> impl IntoView {

View File

@@ -63,7 +63,9 @@ cfg_if! {
use crate::{
ev::EventDescriptor,
hydration::HydrationCtx,
macro_helpers::{IntoAttribute, IntoClass, IntoProperty, IntoStyle},
macro_helpers::{
Attribute, IntoAttribute, IntoClass, IntoProperty, IntoStyle,
},
Element, Fragment, IntoView, NodeRef, Text, View,
};
use leptos_reactive::Oco;
@@ -593,8 +595,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
use crate::macro_helpers::Attribute;
let mut this = self;
let mut attr = attr.into_attribute();
@@ -622,6 +622,18 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
}
/// Adds multiple attributes to the element
#[track_caller]
pub fn attrs(
mut self,
attrs: impl std::iter::IntoIterator<Item = (&'static str, Attribute)>,
) -> Self {
for (name, value) in attrs {
self = self.attr(name, value);
}
self
}
/// Adds a class to an element.
///
/// **Note**: In the builder syntax, this will be overwritten by the `class`

View File

@@ -9,12 +9,14 @@
#[cfg_attr(any(debug_assertions, feature = "ssr"), macro_use)]
pub extern crate tracing;
pub mod callback;
mod components;
mod events;
pub mod helpers;
pub mod html;
mod hydration;
mod logging;
/// Utilities for simple isomorphic logging to the console or terminal.
pub mod logging;
mod macro_helpers;
pub mod math;
mod node_ref;
@@ -24,6 +26,7 @@ pub mod ssr;
pub mod ssr_in_order;
pub mod svg;
mod transparent;
pub use callback::*;
use cfg_if::cfg_if;
pub use components::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -39,7 +42,6 @@ use leptos_reactive::Oco;
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
pub use logging::*;
pub use macro_helpers::*;
pub use node_ref::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -851,7 +853,7 @@ where
N: IntoView,
{
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_warn(
crate::logging::console_warn(
"You have both `csr` and `ssr` or `hydrate` and `ssr` enabled as \
features, which may cause issues like <Suspense/>` failing to work \
silently.",

View File

@@ -6,21 +6,21 @@ use wasm_bindgen::JsValue;
/// or via `println!()` (if not in the browser).
#[macro_export]
macro_rules! log {
($($t:tt)*) => ($crate::console_log(&format_args!($($t)*).to_string()))
($($t:tt)*) => ($crate::logging::console_log(&format_args!($($t)*).to_string()))
}
/// Uses `println!()`-style formatting to log warnings to the console (in the browser)
/// or via `eprintln!()` (if not in the browser).
#[macro_export]
macro_rules! warn {
($($t:tt)*) => ($crate::console_warn(&format_args!($($t)*).to_string()))
($($t:tt)*) => ($crate::logging::console_warn(&format_args!($($t)*).to_string()))
}
/// Uses `println!()`-style formatting to log errors to the console (in the browser)
/// or via `eprintln!()` (if not in the browser).
#[macro_export]
macro_rules! error {
($($t:tt)*) => ($crate::console_error(&format_args!($($t)*).to_string()))
($($t:tt)*) => ($crate::logging::console_error(&format_args!($($t)*).to_string()))
}
/// Uses `println!()`-style formatting to log warnings to the console (in the browser)

View File

@@ -105,6 +105,7 @@ impl std::fmt::Debug for Attribute {
pub trait IntoAttribute {
/// Converts the object into an [`Attribute`].
fn into_attribute(self) -> Attribute;
/// Helper function for dealing with `Box<dyn IntoAttribute>`.
fn into_attribute_boxed(self: Box<Self>) -> Attribute;
}

View File

@@ -21,6 +21,9 @@ pub enum Class {
pub trait IntoClass {
/// Converts the object into a [`Class`].
fn into_class(self) -> Class;
/// Helper function for dealing with `Box<dyn IntoClass>`.
fn into_class_boxed(self: Box<Self>) -> Class;
}
impl IntoClass for bool {
@@ -28,6 +31,10 @@ impl IntoClass for bool {
fn into_class(self) -> Class {
Class::Value(self)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
impl<T> IntoClass for T
@@ -39,6 +46,10 @@ where
let modified_fn = Box::new(self);
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
impl Class {
@@ -128,6 +139,10 @@ macro_rules! class_signal_type {
let modified_fn = Box::new(move || self.get());
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
};
}
@@ -141,6 +156,10 @@ macro_rules! class_signal_type_optional {
let modified_fn = Box::new(move || self.get().unwrap_or(false));
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
};
}

View File

@@ -15,7 +15,7 @@ use wasm_bindgen::UnwrapThrowExt;
pub enum Property {
/// A static JavaScript value.
Value(JsValue),
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
/// A (presumably reactive) function, which will be run inside an effect to update the property.
Fn(Box<dyn Fn() -> JsValue>),
}
@@ -25,6 +25,9 @@ pub enum Property {
pub trait IntoProperty {
/// Converts the object into a [`Property`].
fn into_property(self) -> Property;
/// Helper function for dealing with `Box<dyn IntoProperty>`.
fn into_property_boxed(self: Box<Self>) -> Property;
}
impl<T, U> IntoProperty for T
@@ -36,6 +39,10 @@ where
let modified_fn = Box::new(move || self().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
macro_rules! prop_type {
@@ -45,6 +52,10 @@ macro_rules! prop_type {
fn into_property(self) -> Property {
Property::Value(self.into())
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
impl IntoProperty for Option<$prop_type> {
@@ -52,6 +63,10 @@ macro_rules! prop_type {
fn into_property(self) -> Property {
Property::Value(self.into())
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}
@@ -67,6 +82,10 @@ macro_rules! prop_signal_type {
let modified_fn = Box::new(move || self.get().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}
@@ -83,6 +102,10 @@ macro_rules! prop_signal_type_optional {
let modified_fn = Box::new(move || self.get().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}

View File

@@ -41,6 +41,9 @@ impl std::fmt::Debug for Style {
pub trait IntoStyle {
/// Converts the object into a [`Style`].
fn into_style(self) -> Style;
/// Helper function for dealing with `Box<dyn IntoStyle>`.
fn into_style_boxed(self: Box<Self>) -> Style;
}
impl IntoStyle for &'static str {
@@ -48,6 +51,10 @@ impl IntoStyle for &'static str {
fn into_style(self) -> Style {
Style::Value(Oco::Borrowed(self))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for String {
@@ -55,6 +62,10 @@ impl IntoStyle for String {
fn into_style(self) -> Style {
Style::Value(Oco::Owned(self))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Rc<str> {
@@ -62,6 +73,10 @@ impl IntoStyle for Rc<str> {
fn into_style(self) -> Style {
Style::Value(Oco::Counted(self))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(self).into_style()
}
}
impl IntoStyle for Cow<'static, str> {
@@ -69,6 +84,10 @@ impl IntoStyle for Cow<'static, str> {
fn into_style(self) -> Style {
Style::Value(self.into())
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Oco<'static, str> {
@@ -76,6 +95,10 @@ impl IntoStyle for Oco<'static, str> {
fn into_style(self) -> Style {
Style::Value(self)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<&'static str> {
@@ -83,6 +106,10 @@ impl IntoStyle for Option<&'static str> {
fn into_style(self) -> Style {
Style::Option(self.map(Oco::Borrowed))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<String> {
@@ -90,6 +117,10 @@ impl IntoStyle for Option<String> {
fn into_style(self) -> Style {
Style::Option(self.map(Oco::Owned))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(self).into_style()
}
}
impl IntoStyle for Option<Rc<str>> {
@@ -97,6 +128,10 @@ impl IntoStyle for Option<Rc<str>> {
fn into_style(self) -> Style {
Style::Option(self.map(Oco::Counted))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<Cow<'static, str>> {
@@ -104,6 +139,10 @@ impl IntoStyle for Option<Cow<'static, str>> {
fn into_style(self) -> Style {
Style::Option(self.map(Oco::from))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<Oco<'static, str>> {
@@ -111,6 +150,10 @@ impl IntoStyle for Option<Oco<'static, str>> {
fn into_style(self) -> Style {
Style::Option(self)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl<T, U> IntoStyle for T
@@ -123,6 +166,10 @@ where
let modified_fn = Rc::new(move || (self)().into_style());
Style::Fn(modified_fn)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl Style {
@@ -221,12 +268,20 @@ macro_rules! style_type {
fn into_style(self) -> Style {
Style::Value(self.to_string().into())
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<$style_type> {
fn into_style(self) -> Style {
Style::Option(self.map(|n| n.to_string().into()))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
};
}
@@ -242,6 +297,10 @@ macro_rules! style_signal_type {
let modified_fn = Rc::new(move || self.get().into_style());
Style::Fn(modified_fn)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
};
}
@@ -258,6 +317,10 @@ macro_rules! style_signal_type_optional {
let modified_fn = Rc::new(move || self.get().into_style());
Style::Fn(modified_fn)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
};
}

View File

@@ -8,7 +8,7 @@ use std::cell::Cell;
/// macro to create your UI.
///
/// ```
/// # use leptos::*;
/// # use leptos::{*, logging::log};
///
/// use leptos::html::Input;
///
@@ -43,7 +43,7 @@ pub struct NodeRef<T: ElementDescriptor + 'static>(
/// macro to create your UI.
///
/// ```
/// # use leptos::*;
/// # use leptos::{*, logging::log};
///
/// use leptos::html::Input;
///

View File

@@ -365,7 +365,7 @@ impl View {
)]
pub fn render_to_string(self) -> Oco<'static, str> {
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_error(
crate::logging::console_error(
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
enabled as features, which may cause issues like <Suspense/>` \
failing to work silently.\n",

View File

@@ -62,7 +62,7 @@ pub fn render_to_stream_in_order_with_prefix(
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
) -> impl Stream<Item = String> {
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_error(
crate::logging::console_error(
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
enabled as features, which may cause issues like <Suspense/>` \
failing to work silently.\n",

View File

@@ -217,6 +217,27 @@ pub(crate) fn element_to_tokens(
None
}
});
let spread_attrs = node.attributes().iter().filter_map(|node| {
use rstml::node::NodeBlock;
use syn::{Expr, ExprRange, RangeLimits, Stmt};
if let NodeAttribute::Block(NodeBlock::ValidBlock(block)) = node {
match block.stmts.first()? {
Stmt::Expr(
Expr::Range(ExprRange {
start: None,
limits: RangeLimits::HalfOpen(_),
end: Some(end),
..
}),
_,
) => Some(quote! { .attrs(#[allow(unused_brace)] {#end}) }),
_ => None,
}
} else {
None
}
});
let class_attrs = node.attributes().iter().filter_map(|node| {
if let NodeAttribute::Attribute(node) = node {
let name = node.key.to_string();
@@ -332,6 +353,7 @@ pub(crate) fn element_to_tokens(
#(#ide_helper_close_tag)*
#name
#(#attrs)*
#(#spread_attrs)*
#(#class_attrs)*
#(#style_attrs)*
#global_class_expr

View File

@@ -124,7 +124,7 @@ pub(crate) fn component_to_tokens(
.children({
#(#clonables)*
Box::new(move || #children #view_marker)
::leptos::ToChildren::to_children(move || #children #view_marker)
})
}
}
@@ -147,11 +147,18 @@ pub(crate) fn component_to_tokens(
}
});
let generics = &node.open_tag.generics;
let generics = if generics.lt_token.is_some() {
quote! { ::#generics }
} else {
quote! {}
};
#[allow(unused_mut)] // used in debug
let mut component = quote! {
::leptos::component_view(
&#name,
::leptos::component_props_builder(&#name)
::leptos::component_props_builder(&#name #generics)
#(#props)*
#(#slots)*
#children

View File

@@ -522,17 +522,7 @@ pub(crate) fn event_from_attribute_node(
let handler = attribute_value(attr);
#[allow(unused_variables)]
let (name, name_undelegated) = parse_event(&event_name);
let event_type = TYPED_EVENTS
.binary_search(&name)
.map(|_| (name))
.unwrap_or(CUSTOM_EVENT);
let Ok(event_type) = event_type.parse::<TokenStream>() else {
abort!(attr.key, "couldn't parse event name");
};
let (event_type, _, name_undelegated) = parse_event_name(&event_name);
let event_type = if force_undelegated || name_undelegated {
quote! { ::leptos::leptos_dom::ev::undelegated(::leptos::leptos_dom::ev::#event_type) }

View File

@@ -265,6 +265,31 @@ fn element_to_tokens_ssr(
);
}
}
for attr in node.attributes() {
use syn::{Expr, ExprRange, RangeLimits, Stmt};
if let NodeAttribute::Block(NodeBlock::ValidBlock(block)) = attr {
if let Some(Stmt::Expr(
Expr::Range(ExprRange {
start: None,
limits: RangeLimits::HalfOpen(_),
end: Some(end),
..
}),
_,
)) = block.stmts.first()
{
// should basically be the resolved attributes, joined on spaces, placed into
// the template
template.push_str(" {}");
holes.push(quote! {
{#end}.into_iter().filter_map(|(name, attr)| {
Some(format!("{}={}", name, ::leptos::leptos_dom::ssr::escape_attr(&attr.as_nameless_value_string()?)))
}).collect::<Vec<_>>().join(" ")
});
};
}
}
// insert hydration ID
let hydration_id = if is_root {

View File

@@ -130,7 +130,7 @@ pub(crate) fn slot_to_tokens(
.children({
#(#clonables)*
Box::new(move || #children #view_marker)
::leptos::ToChildren::to_children(move || #children #view_marker)
})
}
}

View File

@@ -0,0 +1,19 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: pretty(result)
---
fn view() {
::leptos::component_view(
&ExternalComponent,
::leptos::component_props_builder(&ExternalComponent).build(),
)
.into_view()
.on(
::leptos::leptos_dom::ev::undelegated(
::leptos::leptos_dom::ev::Custom::new("custom.event.clear"),
),
move |_: Event| set_value(0),
)
}

View File

@@ -0,0 +1,272 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: result
---
TokenStream [
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: component_view,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '&',
spacing: Alone,
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
Punct {
char: ',',
spacing: Alone,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: component_props_builder,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '&',
spacing: Alone,
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
],
},
Punct {
char: '.',
spacing: Alone,
},
Ident {
sym: build,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [],
},
],
},
Punct {
char: '.',
spacing: Alone,
},
Ident {
sym: into_view,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [],
},
Punct {
char: '.',
spacing: Alone,
},
Ident {
sym: on,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos_dom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: ev,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: undelegated,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos_dom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: ev,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: Custom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: new,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
lit: "custom.event.clear",
},
],
},
],
},
Punct {
char: ',',
spacing: Alone,
},
Ident {
sym: move,
span: bytes(51..55),
},
Punct {
char: '|',
spacing: Alone,
span: bytes(56..57),
},
Ident {
sym: _,
span: bytes(57..58),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(58..59),
},
Ident {
sym: Event,
span: bytes(60..65),
},
Punct {
char: '|',
spacing: Alone,
span: bytes(65..66),
},
Ident {
sym: set_value,
span: bytes(67..76),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
lit: 0,
span: bytes(77..78),
},
],
span: bytes(76..79),
},
],
},
]

View File

@@ -0,0 +1,19 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: pretty(result)
---
fn view() {
::leptos::component_view(
&ExternalComponent,
::leptos::component_props_builder(&ExternalComponent).build(),
)
.into_view()
.on(
::leptos::leptos_dom::ev::undelegated(
::leptos::leptos_dom::ev::Custom::new("custom.event.clear"),
),
move |_: Event| set_value(0),
)
}

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