mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-29 10:33:35 -05:00
Compare commits
67 Commits
server-fn-
...
effect-sch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
642ab9baf9 | ||
|
|
ed58ac2c99 | ||
|
|
4f1fafaea3 | ||
|
|
7a35d33402 | ||
|
|
e6f053d39f | ||
|
|
b3a4c95dad | ||
|
|
de44b1f91f | ||
|
|
689022661d | ||
|
|
905d46a09d | ||
|
|
5585f20940 | ||
|
|
5c3ed3f018 | ||
|
|
03cabf6ea3 | ||
|
|
2798dc455f | ||
|
|
db20be5576 | ||
|
|
495862e9f9 | ||
|
|
2ca1c51fdc | ||
|
|
70e1ad41e2 | ||
|
|
53ec7ed272 | ||
|
|
d98a577740 | ||
|
|
fd834f48c2 | ||
|
|
7be65a37c6 | ||
|
|
3b5e2d86fb | ||
|
|
716b9fb50b | ||
|
|
006ca13797 | ||
|
|
6e008343c8 | ||
|
|
2ca24883ac | ||
|
|
4a43983f4e | ||
|
|
d9e83121c1 | ||
|
|
f5b4b97c9b | ||
|
|
bcfa430a40 | ||
|
|
7c51815cf5 | ||
|
|
fee2fb953b | ||
|
|
8ecb7f59c4 | ||
|
|
b85cb9fb3b | ||
|
|
a631c5ca1c | ||
|
|
bee9bd8f67 | ||
|
|
8d3874f8a9 | ||
|
|
bade16d227 | ||
|
|
e0a132bde3 | ||
|
|
4d7e1f4d26 | ||
|
|
700eee6604 | ||
|
|
694ed61e4c | ||
|
|
d7330097ba | ||
|
|
c65a3a6ca3 | ||
|
|
793c191619 | ||
|
|
6c3e2fe53e | ||
|
|
08c419e3ee | ||
|
|
736f4185b5 | ||
|
|
9cc0fc8c49 | ||
|
|
8f067dcde7 | ||
|
|
ad6eb58fe1 | ||
|
|
3f3ab1c3c8 | ||
|
|
9adae32847 | ||
|
|
b8098e7992 | ||
|
|
bef4d0dd3b | ||
|
|
a789100e22 | ||
|
|
abeca70625 | ||
|
|
cc293b1170 | ||
|
|
8ab62c17c6 | ||
|
|
cf14e857ca | ||
|
|
c322ef38fd | ||
|
|
c9cc493063 | ||
|
|
fb48f7f117 | ||
|
|
c344e54cf6 | ||
|
|
7306ecccbc | ||
|
|
b98174db7a | ||
|
|
e48f66694d |
1
.github/workflows/check-examples.yml
vendored
1
.github/workflows/check-examples.yml
vendored
@@ -26,3 +26,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "check"
|
||||
toolchain: nightly
|
||||
|
||||
47
.github/workflows/check-stable.yml
vendored
47
.github/workflows/check-stable.yml
vendored
@@ -2,50 +2,25 @@ name: Check stable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
test:
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
name: Check
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all examples
|
||||
run: cargo make --profile=github-actions check-stable
|
||||
directory: [examples/counters_stable, examples/counter_without_macros]
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "check"
|
||||
toolchain: stable
|
||||
|
||||
1
.github/workflows/ci-changed-examples.yml
vendored
1
.github/workflows/ci-changed-examples.yml
vendored
@@ -29,3 +29,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -41,3 +41,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
36
.github/workflows/run-cargo-make-task.yml
vendored
36
.github/workflows/run-cargo-make-task.yml
vendored
@@ -9,6 +9,9 @@ on:
|
||||
cargo_make_task:
|
||||
required: true
|
||||
type: string
|
||||
toolchain:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -16,23 +19,17 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Setup environment
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
@@ -98,14 +95,17 @@ jobs:
|
||||
|
||||
- name: Maybe install playwright browser dependencies
|
||||
run: |
|
||||
playwright_count=$(find ${{inputs.directory}} -name playwright.config.ts | wc -l)
|
||||
if [ $playwright_count -eq 1 ]; then
|
||||
echo playwright required
|
||||
sudo apt-get update
|
||||
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
|
||||
else
|
||||
echo playwright is not required
|
||||
fi
|
||||
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
if [ ! -v $pw_dir ]; then
|
||||
echo "Playwright required in $pw_dir"
|
||||
cd $pw_dir
|
||||
pnpm dlx playwright install --with-deps
|
||||
else
|
||||
echo Playwright is not required
|
||||
fi
|
||||
done
|
||||
|
||||
# Run Cargo Make Task
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
|
||||
1
.github/workflows/verify-all-examples.yml
vendored
1
.github/workflows/verify-all-examples.yml
vendored
@@ -23,3 +23,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ Cargo.lock
|
||||
.idea
|
||||
.direnv
|
||||
.envrc
|
||||
|
||||
.vscode
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.0"
|
||||
version = "0.5.0-beta2"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.5.0" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.0" }
|
||||
leptos_router = { path = "./router", version = "0.5.0" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0" }
|
||||
leptos = { path = "./leptos", version = "0.5.0-beta2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.0-beta2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0-beta2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.0-beta2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0-beta2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.0-beta2" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.0-beta2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0-beta2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0-beta2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.0-beta2" }
|
||||
leptos_router = { path = "./router", version = "0.5.0-beta2" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.0-beta2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0-beta2" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a suspected security issue, please contact security@leptos.dev rather than opening
|
||||
a public issue.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
The most-recently-released version of the library is supported with security updates.
|
||||
For example, if a security issue is discovered that affects 0.3.2 and all later releases,
|
||||
a 0.4.x patch will be released but a new 0.3.x patch release will not be made. You should
|
||||
plan to update to the latest version to receive any new features or bugfixes of any kind.
|
||||
@@ -65,7 +65,7 @@ And add a simple “Hello, world!” to your `main.rs`
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|| view! { <p>"Hello, world!"</p> })
|
||||
mount_to_body(|| view! { <p>"Hello, world!"</p> })
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -367,7 +367,6 @@ fn GlobalStateInput() -> impl IntoView {
|
||||
// that we created in the other component
|
||||
// neither of them will cause the other to rerun
|
||||
let (name, set_name) = create_slice(
|
||||
|
||||
// we take a slice *from* `state`
|
||||
state,
|
||||
// our getter returns a "slice" of the data
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
- [Error Handling](./view/07_errors.md)
|
||||
- [Parent-Child Communication](./view/08_parent_child.md)
|
||||
- [Passing Children to Components](./view/09_component_children.md)
|
||||
- [No Macros: The View Builder Syntax](./view/builder.md)
|
||||
- [Reactivity](./reactivity/README.md)
|
||||
- [Working with Signals](./reactivity/working_with_signals.md)
|
||||
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)
|
||||
|
||||
@@ -235,9 +235,9 @@ At the very most, you might consider memoizing the final node before running som
|
||||
|
||||
```rust
|
||||
let text = create_memo(move |_| {
|
||||
d()
|
||||
d()
|
||||
});
|
||||
create_effect(move |_| {
|
||||
engrave_text_into_bar_of_gold(&text());
|
||||
engrave_text_into_bar_of_gold(&text());
|
||||
});
|
||||
```
|
||||
|
||||
@@ -18,7 +18,7 @@ let async_data = create_resource(
|
||||
count,
|
||||
// every time `count` changes, this will run
|
||||
|value| async move {
|
||||
log!("loading data from API");
|
||||
logging::log!("loading data from API");
|
||||
load_data(value).await
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ In the previous chapter, we showed how you can create a simple loading screen to
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(0);
|
||||
let a = create_resource(count, |count| async move { load_a(count).await });
|
||||
let once = create_resource(count, |count| async move { load_a(count).await });
|
||||
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
|
||||
@@ -54,7 +54,7 @@ RUN cargo leptos build --release -vv
|
||||
|
||||
FROM rustlang/rust:nightly-bullseye as runner
|
||||
# Copy the server binary to the /app directory
|
||||
COPY --from=builder /app/target/server/release/leptos_website /app/
|
||||
COPY --from=builder /app/target/server/release/leptos_start /app/
|
||||
# /target/site contains our JS/WASM/CSS, etc.
|
||||
COPY --from=builder /app/target/site /app/site
|
||||
# Copy Cargo.toml if it’s needed at runtime
|
||||
@@ -68,7 +68,7 @@ ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
|
||||
ENV LEPTOS_SITE_ROOT="site"
|
||||
EXPOSE 8080
|
||||
# Run the server
|
||||
CMD ["/app/leptos_website"]
|
||||
CMD ["/app/leptos_start"]
|
||||
```
|
||||
|
||||
> Read more: [`gnu` and `musl` build files for Leptos apps](https://github.com/leptos-rs/leptos/issues/1152#issuecomment-1634916088).
|
||||
|
||||
@@ -50,7 +50,6 @@ If you want to really understand the issue here, it may help to look at the expa
|
||||
|
||||
```rust
|
||||
Suspense(
|
||||
|
||||
::leptos::component_props_builder(&Suspense)
|
||||
.fallback(|| ())
|
||||
.children({
|
||||
@@ -61,7 +60,6 @@ Suspense(
|
||||
leptos::Fragment::lazy(|| {
|
||||
vec![
|
||||
(Show(
|
||||
|
||||
::leptos::component_props_builder(&Show)
|
||||
.when(|| true)
|
||||
// but fallback is moved into Show here
|
||||
|
||||
@@ -50,7 +50,6 @@ let (use_last, set_use_last) = create_signal(true);
|
||||
// any time one of the source signals changes
|
||||
create_effect(move |_| {
|
||||
log(
|
||||
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
@@ -122,7 +121,6 @@ Like `create_resource`, `watch` takes a first argument, which is reactively trac
|
||||
let (num, set_num) = create_signal(0);
|
||||
|
||||
let stop = watch(
|
||||
|
||||
move || num.get(),
|
||||
move |num, prev_num, _| {
|
||||
log::debug!("Number: {}; Prev: {:?}", num, prev_num);
|
||||
|
||||
@@ -20,7 +20,7 @@ let text = move || if count_is_odd() {
|
||||
// an effect automatically tracks the signals it depends on
|
||||
// and reruns when they change
|
||||
create_effect(move |_| {
|
||||
log!("text = {}", text());
|
||||
logging::log!("text = {}", text());
|
||||
});
|
||||
|
||||
view! {
|
||||
|
||||
@@ -16,7 +16,7 @@ Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `Wr
|
||||
```rust
|
||||
let (count, set_count) = create_signal(0);
|
||||
set_count(1);
|
||||
log!(count());
|
||||
logging::log!(count());
|
||||
```
|
||||
|
||||
is the same as
|
||||
@@ -24,7 +24,7 @@ is the same as
|
||||
```rust
|
||||
let (count, set_count) = create_signal(0);
|
||||
set_count.set(1);
|
||||
log!(count.get());
|
||||
logging::log!(count.get());
|
||||
```
|
||||
|
||||
You might notice that `.get()` and `.set()` can be implemented in terms of `.with()` and `.update()`. In other words, `count.get()` is identical with `count.with(|n| n.clone())`, and `count.set(1)` is implemented by doing `count.update(|n| *n = 1)`.
|
||||
@@ -63,7 +63,7 @@ if names.with(Vec::is_empty) {
|
||||
}
|
||||
```
|
||||
|
||||
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unncessary closure.
|
||||
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unnecessary closure.
|
||||
|
||||
## Making signals depend on each other
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
#[component]
|
||||
pub fn BusyButton() -> impl IntoView {
|
||||
view! {
|
||||
|
||||
<button on:click=move |_| {
|
||||
spawn_local(async {
|
||||
add_todo("So much to do!".to_string()).await;
|
||||
@@ -70,6 +69,18 @@ There are a few things to note about the way you define a server function, too.
|
||||
- We provide the macro a path. This is a prefix for the path at which we’ll mount a server function handler on our server. (See examples for [Actix](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/main.rs#L44) and [Axum](https://github.com/leptos-rs/leptos/blob/598523cd9d0d775b017cb721e41ebae9349f01e2/examples/todo_app_sqlite_axum/src/main.rs#L51).)
|
||||
- You’ll need to have `serde` as a dependency with the `derive` featured enabled for the macro to work properly. You can easily add it to `Cargo.toml` with `cargo add serde --features=derive`.
|
||||
|
||||
## Server Function URL Prefixes
|
||||
|
||||
You can optionally define a specific URL prefix to be used in the definition of the server function.
|
||||
This is done by providing an optional 2nd argument to the `#[server]` macro.
|
||||
By default the URL prefix will be `/api`, if not specified.
|
||||
Here are some examples:
|
||||
|
||||
```rust
|
||||
#[server(AddTodo)] // will use the default URL prefix of `/api`
|
||||
#[server(AddTodo, "/foo")] // will use the URL prefix of `/foo`
|
||||
```
|
||||
|
||||
## Server Function Encodings
|
||||
|
||||
By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body of the request. (This means that server functions can be called from HTML forms, which we’ll see in a future chapter.) But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` macro to specify an alternate encoding:
|
||||
@@ -105,6 +116,21 @@ In other words, you have two choices:
|
||||
>
|
||||
> The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of your app is not available.
|
||||
|
||||
|
||||
## Server Functions Endpoint Paths
|
||||
|
||||
By default, a unique path will be generated. You can optionally define a specific endpoint path to be used in the URL. This is done by providing an optional 4th argument to the `#[server]` macro. Leptos will generate the complete path by concatenating the URL prefix (2nd argument) and the endpoint path (4th argument).
|
||||
For example,
|
||||
|
||||
```rust
|
||||
#[server(MyServerFnType, "/api", "Url", "hello")]
|
||||
```
|
||||
will generate a server function endpoint at `/api/hello` that accepts a POST request.
|
||||
|
||||
> **Can I use the same server function endpoint path with multiple encodings?**
|
||||
>
|
||||
> No. Different server functions must have unique paths. The `#[server]` macro automatically generates unique paths, but you need to be careful if you choose to specify the complete path manually, as the server looks up server functions by their path.
|
||||
|
||||
## An Important Note on Security
|
||||
|
||||
Server functions are a cool technology, but it’s very important to remember. **Server functions are not magic; they’re syntax sugar for defining a public API.** The _body_ of a server function is never made public; it’s just part of your server binary. But the server function is a publicly accessible API endpoint, and it’s return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function.
|
||||
|
||||
@@ -35,7 +35,6 @@ Here’s a simplified example from our [`session_auth_axum` example](https://git
|
||||
```rust
|
||||
#[server(Login, "/api")]
|
||||
pub async fn login(
|
||||
|
||||
username: String,
|
||||
password: String,
|
||||
remember: Option<String>,
|
||||
|
||||
@@ -9,7 +9,7 @@ Put a log somewhere in your root component. (I usually call mine `<App/>`, but a
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
leptos::log!("where do I run?");
|
||||
logging::log!("where do I run?");
|
||||
// ... whatever
|
||||
}
|
||||
```
|
||||
@@ -87,43 +87,6 @@ The WASM version of your app, running in the browser, expects to find three item
|
||||
|
||||
It’s pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If you’re seeing warnings like this and you don’t think it’s your fault, it’s much more likely that it’s a bug with `<Suspense/>` or something. Feel free to go ahead and open an [issue](https://github.com/leptos-rs/leptos/issues) or [discussion](https://github.com/leptos-rs/leptos/discussions) on GitHub for help.
|
||||
|
||||
### Mutating the DOM during rendering
|
||||
|
||||
This is a slightly more common way to create a client/server mismatch: updating a signal _during rendering_ in a way that mutates the view.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let (loaded, set_loaded) = create_signal(false);
|
||||
|
||||
// create_effect only runs on the client
|
||||
create_effect(move |_| {
|
||||
// do something like reading from localStorage
|
||||
set_loaded(true);
|
||||
});
|
||||
|
||||
move || {
|
||||
if loaded() {
|
||||
view! { <p>"Hello, world!"</p> }.into_any()
|
||||
} else {
|
||||
view! { <div class="loading">"Loading..."</div> }.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This one gives us the scary panic
|
||||
|
||||
```
|
||||
panicked at 'assertion failed: `(left == right)`
|
||||
left: `"DIV"`,
|
||||
right: `"P"`: SSR and CSR elements have the same hydration key but different node kinds.
|
||||
```
|
||||
|
||||
And a handy link to this page!
|
||||
|
||||
The problem here is that `create_effect` runs **immediately** and **synchronously**, but only in the browser. As a result, on the server, `loaded` is false, and a `<div>` is rendered. But on the browser, by the time the view is being rendered, `loaded` has already been set to `true`, and the browser is expecting to find a `<p>`.
|
||||
|
||||
#### Solution
|
||||
|
||||
You can simply tell the effect to wait a tick before updating the signal, by using something like `request_animation_frame`, which will set a short timeout and then update the signal before the next frame.
|
||||
@@ -166,7 +129,7 @@ For example, say that I want to store something in the browser’s `localStorage
|
||||
pub fn App() -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
logging::log!("{storage:?}");
|
||||
}
|
||||
```
|
||||
|
||||
@@ -180,7 +143,7 @@ pub fn App() -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
create_effect(move |_| {
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
logging::log!("{storage:?}");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -37,7 +37,7 @@ impl Todos {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_remaining {
|
||||
fn test_remaining() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ Derived signals let you create reactive computed values that can be used in mult
|
||||
places in your application with minimal overhead.
|
||||
|
||||
Note: Using a derived signal like this means that the calculation runs once per
|
||||
signal change per place we access `double_count`; in other words, twice. This is a
|
||||
signal change and once per place we access `double_count`; in other words, twice. This is a
|
||||
very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||
are designed to solve this problem for expensive calculations.
|
||||
|
||||
|
||||
@@ -35,9 +35,7 @@ Instead, let’s create a `<ProgressBar/>` component.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
) -> impl IntoView {
|
||||
fn ProgressBar() -> impl IntoView {
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
@@ -64,7 +62,6 @@ In Leptos, you define props by giving additional arguments to the component func
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
@@ -118,7 +115,6 @@ argument to the component function with `#[prop(optional)]`.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
// mark this prop optional
|
||||
// you can specify it or not when you use <ProgressBar/>
|
||||
#[prop(optional)]
|
||||
@@ -149,7 +145,6 @@ with `#[prop(default = ...)`.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: ReadSignal<i32>
|
||||
@@ -199,7 +194,6 @@ implement the trait `Fn() -> i32`. So you could use a generic component:
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar<F>(
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: F
|
||||
@@ -254,7 +248,6 @@ reactive value.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
#[prop(into)]
|
||||
@@ -373,7 +366,6 @@ component function, and each one of the props:
|
||||
/// Shows progress toward a goal.
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
/// The maximum value of the progress bar.
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
|
||||
@@ -148,10 +148,10 @@ This _works_, for sure. But if you added a log, you might be surprised
|
||||
|
||||
```rust
|
||||
let message = move || if value() > 5 {
|
||||
log!("{}: rendering Big", value());
|
||||
logging::log!("{}: rendering Big", value());
|
||||
"Big"
|
||||
} else {
|
||||
log!("{}: rendering Small", value());
|
||||
logging::log!("{}: rendering Small", value());
|
||||
"Small"
|
||||
};
|
||||
```
|
||||
|
||||
@@ -72,10 +72,7 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn ButtonB<F>(
|
||||
|
||||
on_click: F,
|
||||
) -> impl IntoView
|
||||
pub fn ButtonB<F>(on_click: F) -> impl IntoView
|
||||
where
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
{
|
||||
|
||||
@@ -47,7 +47,6 @@ Let’s define a component that takes some children and a render prop.
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TakesChildren<F, IV>(
|
||||
|
||||
/// Takes a function (type F) that returns anything that can be
|
||||
/// converted into a View (type IV)
|
||||
render_prop: F,
|
||||
|
||||
98
docs/book/src/view/builder.md
Normal file
98
docs/book/src/view/builder.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# No Macros: The View Builder Syntax
|
||||
|
||||
> If you’re perfectly happy with the `view!` macro syntax described so far, you’re welcome to skip this chapter. The builder syntax described in this section is always available, but never required.
|
||||
|
||||
For one reason or another, many developers would prefer to avoid macros. Perhaps you don’t like the limited `rustfmt` support. (Although, you should check out [`leptosfmt`](https://github.com/bram209/leptosfmt), which is an excellent tool!) Perhaps you worry about the effect of macros on compile time. Perhaps you prefer the aesthetics of pure Rust syntax, or you have trouble context-switching between an HTML-like syntax and your Rust code. Or perhaps you want more flexibility in how you create and manipulate HTML elements than the `view` macro provides.
|
||||
|
||||
If you fall into any of those camps, the builder syntax may be for you.
|
||||
|
||||
The `view` macro expands an HTML-like syntax to a series of Rust functions and method calls. If you’d rather not use the `view` macro, you can simply use that expanded syntax yourself. And it’s actually pretty nice!
|
||||
|
||||
First off, if you want you can even drop the `#[component]` macro: a component is just a setup function that creates your view, so you can define a component as a simple function call:
|
||||
|
||||
```rust
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView { }
|
||||
```
|
||||
|
||||
Elements are created by calling a function with the same name as the HTML element:
|
||||
|
||||
```rust
|
||||
p()
|
||||
```
|
||||
|
||||
You can add children to the element with [`.child()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child), which takes a single child or a tuple or array of types that implement [`IntoView`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html).
|
||||
|
||||
```rust
|
||||
p().child((em().child("Big, "), strong().child("bold "), "text"))
|
||||
```
|
||||
|
||||
Attributes are added with [`.attr()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.attr). This can take any of the same types that you could pass as an attribute into the view macro (types that implement [`IntoAttribute`](https://docs.rs/leptos/latest/leptos/trait.IntoAttribute.html)).
|
||||
|
||||
```rust
|
||||
p().attr("id", "foo").attr("data-count", move || count().to_string())
|
||||
```
|
||||
|
||||
Similarly, the `class:`, `prop:`, and `style:` syntaxes map directly onto [`.class()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.class), [`.prop()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.prop), and [`.style()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.style) methods.
|
||||
|
||||
Event listeners can be added with [`.on()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.on). Typed events found in [`leptos::ev`](https://docs.rs/leptos/latest/leptos/ev/index.html) prevent typos in event names and allow for correct type inference in the callback function.
|
||||
|
||||
```rust
|
||||
button()
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.child("Clear")
|
||||
```
|
||||
|
||||
> Many additional methods can be found in the [`HtmlElement`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child) docs, including some methods that are not directly available in the `view` macro.
|
||||
|
||||
All of this adds up to a very Rusty syntax to build full-featured views, if you prefer this style.
|
||||
|
||||
```rust
|
||||
/// A simple counter view.
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
div()
|
||||
.child((
|
||||
button()
|
||||
// typed events found in leptos::ev
|
||||
// 1) prevent typos in event names
|
||||
// 2) allow for correct type inference in callbacks
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.child("Clear"),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.child("-1"),
|
||||
span().child(("Value: ", move || count.get().value(), "!")),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
.child("+1"),
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
This also has the benefit of being more flexible: because these are all plain Rust functions and methods, it’s easier to use them in things like iterator adapters without any additional “magic”:
|
||||
|
||||
```rust
|
||||
// take some set of attribute names and values
|
||||
let attrs: Vec<(&str, AttributeValue)> = todo!();
|
||||
// you can use the builder syntax to “spread” these onto the
|
||||
// element in a way that’s not possible with the view macro
|
||||
let p = attrs
|
||||
.into_iter()
|
||||
.fold(p(), |el, (name, value)| el.attr(name, value));
|
||||
|
||||
```
|
||||
|
||||
> ## Performance Note
|
||||
>
|
||||
> One caveat: the `view` macro applies significant optimizations in server-side-rendering (SSR) mode to improve HTML rendering performance significantly (think 2-4x faster, depending on the characteristics of any given app). It does this by analyzing your `view` at compile time and converting the static parts into simple HTML strings, rather than expanding them into the builder syntax.
|
||||
>
|
||||
> This means two things:
|
||||
>
|
||||
> 1. The builder syntax and `view` macro should not be mixed, or should only be mixed very carefully: at least in SSR mode, the output of the `view` should be treated as a “black box” that can’t have additional builder methods applied to it without causing inconsistencies.
|
||||
> 2. Using the builder syntax will result in less-than-optimal SSR performance. It won’t be slow, by any means (and it’s worth running your own benchmarks in any case), just slower than the `view`-optimized version.
|
||||
@@ -49,10 +49,12 @@ jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
|
||||
'''
|
||||
|
||||
[tasks.test-runner-report]
|
||||
[tasks.test-report]
|
||||
workspace = false
|
||||
description = "report ci test runners for each example - OPTION: [all]"
|
||||
description = "report web testing technology used by examples - OPTION: [all]"
|
||||
script = '''
|
||||
set -emu
|
||||
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
ITALIC="\e[3m"
|
||||
@@ -60,11 +62,10 @@ YELLOW="\e[0;33m"
|
||||
RESET="\e[0m"
|
||||
|
||||
echo
|
||||
echo "${YELLOW}Test Runner Report${RESET}"
|
||||
echo "${ITALIC}Pass the option \"all\" to show all the examples${RESET}"
|
||||
echo "${YELLOW}Web Test Technology${RESET}"
|
||||
echo
|
||||
|
||||
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' |
|
||||
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' -not -path '*/node_modules/*' |
|
||||
sed 's%./%%' |
|
||||
sed 's%/Makefile.toml%%' |
|
||||
grep -v Makefile.toml |
|
||||
@@ -75,38 +76,78 @@ start_path=$(pwd)
|
||||
for path in $makefile_paths; do
|
||||
cd $path
|
||||
|
||||
test_runner=
|
||||
crate_symbols=
|
||||
|
||||
test_count=$(grep -rl -E "#\[(test|rstest)\]" | wc -l)
|
||||
if [ $test_count -gt 0 ]; then
|
||||
test_runner="-C"
|
||||
fi
|
||||
pw_count=$(find . -name playwright.config.ts | wc -l)
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"cucumber"*)
|
||||
crate_symbols=$crate_symbols"C"
|
||||
;;
|
||||
*"fantoccini"*)
|
||||
crate_symbols=$crate_symbols"D"
|
||||
;;
|
||||
esac
|
||||
done <"./Cargo.toml"
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"wasm-test.toml"*)
|
||||
test_runner=$test_runner"-W"
|
||||
*"cargo-make/wasm-test.toml"*)
|
||||
crate_symbols=$crate_symbols"W"
|
||||
;;
|
||||
*"playwright-test.toml"*)
|
||||
test_runner=$test_runner"-P"
|
||||
*"cargo-make/playwright-test.toml"*)
|
||||
crate_symbols=$crate_symbols"P"
|
||||
crate_symbols=$crate_symbols"N"
|
||||
;;
|
||||
*"cargo-leptos-test.toml"*)
|
||||
test_runner=$test_runner"-L"
|
||||
*"cargo-make/playwright-trunk-test.toml"*)
|
||||
crate_symbols=$crate_symbols"P"
|
||||
crate_symbols=$crate_symbols"T"
|
||||
;;
|
||||
*"cargo-make/trunk_server.toml"*)
|
||||
crate_symbols=$crate_symbols"T"
|
||||
;;
|
||||
*"cargo-make/cargo-leptos-webdriver-test.toml"*)
|
||||
crate_symbols=$crate_symbols"L"
|
||||
;;
|
||||
*"cargo-make/cargo-leptos-test.toml"*)
|
||||
crate_symbols=$crate_symbols"L"
|
||||
if [ $pw_count -gt 0 ]; then
|
||||
crate_symbols=$crate_symbols"P"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done <"./Makefile.toml"
|
||||
|
||||
if [ ! -z "$1" ]; then
|
||||
# Sort list of tools
|
||||
sorted_crate_symbols=$(echo ${crate_symbols} | grep -o . | sort | tr -d "\n")
|
||||
|
||||
formatted_crate_symbols=" ➤ ${BOLD}${YELLOW}${sorted_crate_symbols}${RESET}"
|
||||
crate_line=$path
|
||||
if [ ! -z ${1+x} ]; then
|
||||
# Show all examples
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
elif [ ! -z $test_runner ]; then
|
||||
if [ ! -z $crate_symbols ]; then
|
||||
crate_line=$crate_line$formatted_crate_symbols
|
||||
fi
|
||||
echo $crate_line
|
||||
elif [ ! -z $crate_symbols ]; then
|
||||
# Filter out examples that do not run tests in `ci`
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
crate_line=$crate_line$formatted_crate_symbols
|
||||
echo $crate_line
|
||||
fi
|
||||
|
||||
cd ${start_path}
|
||||
done
|
||||
|
||||
c="${BOLD}${YELLOW}C${RESET} = Cucumber"
|
||||
d="${BOLD}${YELLOW}D${RESET} = WebDriver"
|
||||
l="${BOLD}${YELLOW}L${RESET} = Cargo Leptos"
|
||||
n="${BOLD}${YELLOW}N${RESET} = Node"
|
||||
p="${BOLD}${YELLOW}P${RESET} = Playwright"
|
||||
t="${BOLD}${YELLOW}T${RESET} = Trunk"
|
||||
w="${BOLD}${YELLOW}W${RESET} = WASM"
|
||||
|
||||
echo
|
||||
echo "${ITALIC}Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, W = WASM Test${RESET}"
|
||||
echo "${ITALIC}Keys:${RESET} $c, $d, $l, $n, $p, $t, $w"
|
||||
echo
|
||||
'''
|
||||
|
||||
@@ -4,4 +4,4 @@ The examples in this directory are all built and tested against the current `mai
|
||||
|
||||
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch and not the current release.
|
||||
|
||||
To see the examples as they were at the time of the `0.3.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.3.0/examples).
|
||||
To see the examples as they were at the time of the `0.3.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.3.0/examples).
|
||||
|
||||
@@ -2,7 +2,3 @@ extend = { path = "./cargo-leptos.toml" }
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = ["install-cargo-leptos", "cargo-leptos-e2e"]
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
|
||||
7
examples/cargo-make/cargo-leptos-webdriver-test.toml
Normal file
7
examples/cargo-make/cargo-leptos-webdriver-test.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
extend = [
|
||||
{ path = "./cargo-leptos.toml" },
|
||||
{ path = "../cargo-make/webdriver.toml" },
|
||||
]
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = ["install-cargo-leptos", "start-webdriver", "cargo-leptos-e2e"]
|
||||
@@ -1,6 +1,12 @@
|
||||
extend = { path = "../cargo-make/client-process.toml" }
|
||||
|
||||
[tasks.install-cargo-leptos]
|
||||
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
|
||||
[tasks.build]
|
||||
clear = true
|
||||
command = "cargo"
|
||||
@@ -25,31 +31,3 @@ install_crate = "cargo-all-features"
|
||||
[tasks.start-client]
|
||||
command = "cargo"
|
||||
args = ["leptos", "watch"]
|
||||
|
||||
[tasks.stop-client]
|
||||
condition = { env_set = ["APP_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
|
||||
pkill -f todo_app_sqlite
|
||||
fi
|
||||
|
||||
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
|
||||
pkill -f cargo-leptos
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.client-status]
|
||||
condition = { env_set = ["APP_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${APP_PROCESS_NAME}) ]; then
|
||||
echo " ${APP_PROCESS_NAME} is not running"
|
||||
else
|
||||
echo " ${APP_PROCESS_NAME} is up"
|
||||
fi
|
||||
|
||||
if [ -z $(pidof cargo-leptos) ]; then
|
||||
echo " cargo-leptos is not running"
|
||||
else
|
||||
echo " cargo-leptos is up"
|
||||
fi
|
||||
'''
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
[tasks.clean]
|
||||
dependencies = [
|
||||
"clean-cargo",
|
||||
"clean-trunk",
|
||||
"clean-node_modules",
|
||||
"clean-playwright",
|
||||
"clean-cargo",
|
||||
"clean-trunk",
|
||||
"clean-node_modules",
|
||||
"clean-playwright",
|
||||
]
|
||||
|
||||
[tasks.clean-cargo]
|
||||
command = "cargo"
|
||||
args = ["clean"]
|
||||
command = "rm"
|
||||
args = ["-rf", "target"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
script = '''
|
||||
find . -type d -name dist | xargs rm -rf
|
||||
'''
|
||||
|
||||
[tasks.clean-node_modules]
|
||||
script = '''
|
||||
|
||||
35
examples/cargo-make/client-process.toml
Normal file
35
examples/cargo-make/client-process.toml
Normal file
@@ -0,0 +1,35 @@
|
||||
[tasks.start-client]
|
||||
|
||||
[tasks.stop-client]
|
||||
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ ! -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
|
||||
pkill -ef ${CLIENT_PROCESS_NAME}
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.client-status]
|
||||
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
|
||||
echo " ${CLIENT_PROCESS_NAME} is not running"
|
||||
else
|
||||
echo " ${CLIENT_PROCESS_NAME} is up"
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.maybe-start-client]
|
||||
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
|
||||
echo " Starting ${CLIENT_PROCESS_NAME}"
|
||||
cargo make start-client ${@} &
|
||||
else
|
||||
echo " ${CLIENT_PROCESS_NAME} is already started"
|
||||
fi
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.dev]
|
||||
alias = "maybe-start-client"
|
||||
17
examples/cargo-make/playwright-trunk-test.toml
Normal file
17
examples/cargo-make/playwright-trunk-test.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/playwright.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = [
|
||||
"maybe-start-client",
|
||||
"wait-one",
|
||||
"test-playwright",
|
||||
"stop-client",
|
||||
]
|
||||
|
||||
[tasks.wait-one]
|
||||
script = '''
|
||||
sleep 1
|
||||
'''
|
||||
@@ -1,18 +1,12 @@
|
||||
extend = { path = "../cargo-make/client-process.toml" }
|
||||
|
||||
[env]
|
||||
CLIENT_PROCESS_NAME = "trunk"
|
||||
|
||||
[tasks.build]
|
||||
command = "trunk"
|
||||
args = ["build"]
|
||||
|
||||
[tasks.start-trunk]
|
||||
[tasks.start-client]
|
||||
command = "trunk"
|
||||
args = ["serve", "${@}"]
|
||||
|
||||
[tasks.stop-trunk]
|
||||
script = '''
|
||||
pkill -f "cargo-make"
|
||||
pkill -f "trunk"
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.dev]
|
||||
dependencies = ["start-trunk"]
|
||||
|
||||
@@ -3,9 +3,8 @@ use leptos::{ev, html::*, *};
|
||||
/// A simple counter view.
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(Count::new(initial_value, step));
|
||||
let count = RwSignal::new(Count::new(initial_value, step));
|
||||
|
||||
// elements are created by calling a function with a Scope argument
|
||||
// the function name is the same as the HTML tag name
|
||||
div()
|
||||
// children can be added with .child()
|
||||
@@ -18,27 +17,16 @@ pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
// typed events found in leptos::ev
|
||||
// 1) prevent typos in event names
|
||||
// 2) allow for correct type inference in callbacks
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.on(ev::click, move |_| count.update(Count::clear))
|
||||
.child("Clear"),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.on(ev::click, move |_| count.update(Count::decrease))
|
||||
.child("-1"),
|
||||
span()
|
||||
.child("Value: ")
|
||||
// reactive values are passed to .child() as a tuple
|
||||
// (Scope, [child function]) so an effect can be created
|
||||
.child(move || count.get().value())
|
||||
.child("!"),
|
||||
))
|
||||
.child(
|
||||
span().child(("Value: ", move || count.get().value(), "!")),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
.on(ev::click, move |_| count.update(Count::increase))
|
||||
.child("+1"),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -5,12 +5,15 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
leptos_meta = { path = "../../meta", features = ["csr"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
wasm-bindgen = "0.2.87"
|
||||
wasm-bindgen-test = "0.3.37"
|
||||
pretty_assertions = "1.4.0"
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
features = [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-test.toml" },
|
||||
]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Counters (Stable)</title>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
||||
106
examples/counters_stable/src/lib.rs
Normal file
106
examples/counters_stable/src/lib.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct CounterUpdater {
|
||||
set_counters: WriteSignal<CounterHolder>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters() -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
|
||||
provide_context(CounterUpdater { set_counters });
|
||||
|
||||
let add_counter = move |_| {
|
||||
let id = next_counter_id.get();
|
||||
let sig = create_signal(0);
|
||||
set_counters.update(move |counters| counters.push((id, sig)));
|
||||
set_next_counter_id.update(|id| *id += 1);
|
||||
};
|
||||
|
||||
let add_many_counters = move |_| {
|
||||
let next_id = next_counter_id.get();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(0);
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
set_counters.update(move |counters| counters.extend(new_counters));
|
||||
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
|
||||
};
|
||||
|
||||
let clear_counters = move |_| {
|
||||
set_counters.update(|counters| counters.clear());
|
||||
};
|
||||
|
||||
view! {
|
||||
<Title text="Counters (Stable)" />
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
<button on:click=add_many_counters>
|
||||
{format!("Add {MANY_COUNTERS} Counters")}
|
||||
</button>
|
||||
<button on:click=clear_counters>
|
||||
"Clear Counters"
|
||||
</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span data-testid="total">{move ||
|
||||
counters.get()
|
||||
.iter()
|
||||
.map(|(_, (count, _))| count.get())
|
||||
.sum::<i32>()
|
||||
.to_string()
|
||||
}</span>
|
||||
" from "
|
||||
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
<For
|
||||
each={move || counters.get()}
|
||||
key={|counter| counter.0}
|
||||
view=move |(id, (value, set_value))| {
|
||||
view! {
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Counter(
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context().unwrap();
|
||||
|
||||
let input = move |ev| {
|
||||
set_value
|
||||
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
view! {
|
||||
<li>
|
||||
<button data-testid="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input data-testid="counter_input" type="text"
|
||||
prop:value={move || value.get().to_string()}
|
||||
on:input=input
|
||||
/>
|
||||
<span>{value}</span>
|
||||
<button data-testid="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button data-testid="remove_counter" on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use counters_stable::Counters;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
@@ -5,106 +6,3 @@ fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| view! { <Counters/> })
|
||||
}
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct CounterUpdater {
|
||||
set_counters: WriteSignal<CounterHolder>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters() -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
|
||||
provide_context(CounterUpdater { set_counters });
|
||||
|
||||
let add_counter = move |_| {
|
||||
let id = next_counter_id.get();
|
||||
let sig = create_signal(0);
|
||||
set_counters.update(move |counters| counters.push((id, sig)));
|
||||
set_next_counter_id.update(|id| *id += 1);
|
||||
};
|
||||
|
||||
let add_many_counters = move |_| {
|
||||
let next_id = next_counter_id.get();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(0);
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
set_counters.update(move |counters| counters.extend(new_counters));
|
||||
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
|
||||
};
|
||||
|
||||
let clear_counters = move |_| {
|
||||
set_counters.update(|counters| counters.clear());
|
||||
};
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
<button on:click=add_many_counters>
|
||||
{format!("Add {MANY_COUNTERS} Counters")}
|
||||
</button>
|
||||
<button on:click=clear_counters>
|
||||
"Clear Counters"
|
||||
</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span data-testid="total">{move ||
|
||||
counters.get()
|
||||
.iter()
|
||||
.map(|(_, (count, _))| count.get())
|
||||
.sum::<i32>()
|
||||
.to_string()
|
||||
}</span>
|
||||
" from "
|
||||
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
<For
|
||||
each={move || counters.get()}
|
||||
key={|counter| counter.0}
|
||||
view=move |(id, (value, set_value))| {
|
||||
view! {
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Counter(
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context().unwrap();
|
||||
|
||||
let input = move |ev| {
|
||||
set_value
|
||||
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
view! {
|
||||
<li>
|
||||
<button id="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input type="text"
|
||||
prop:value={move || value.get().to_string()}
|
||||
on:input=input
|
||||
/>
|
||||
<span>{value}</span>
|
||||
<button id="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
17
examples/counters_stable/tests/web/add_1k_counters.rs
Normal file
17
examples/counters_stable/tests/web/add_1k_counters.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
|
||||
// When
|
||||
ui::add_1k_counters();
|
||||
ui::add_1k_counters();
|
||||
ui::add_1k_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 3000);
|
||||
}
|
||||
17
examples/counters_stable/tests/web/add_counter.rs
Normal file
17
examples/counters_stable/tests/web/add_counter.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
|
||||
// When
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 3);
|
||||
}
|
||||
19
examples/counters_stable/tests/web/clear_counters.rs
Normal file
19
examples/counters_stable/tests/web/clear_counters.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_reset_the_counts() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::clear_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 0);
|
||||
assert_eq!(ui::counters(), 0);
|
||||
}
|
||||
18
examples/counters_stable/tests/web/decrement_counter.rs
Normal file
18
examples/counters_stable/tests/web/decrement_counter.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrease_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::decrement_counter(1);
|
||||
ui::decrement_counter(1);
|
||||
ui::decrement_counter(1);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), -3);
|
||||
}
|
||||
112
examples/counters_stable/tests/web/fixtures/counters_page.rs
Normal file
112
examples/counters_stable/tests/web/fixtures/counters_page.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use counters_stable::Counters;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Element, Event, EventInit, HtmlElement, HtmlInputElement};
|
||||
|
||||
// Actions
|
||||
|
||||
pub fn add_1k_counters() {
|
||||
find_by_text("Add 1000 Counters").click();
|
||||
}
|
||||
|
||||
pub fn add_counter() {
|
||||
find_by_text("Add Counter").click();
|
||||
}
|
||||
|
||||
pub fn clear_counters() {
|
||||
find_by_text("Clear Counters").click();
|
||||
}
|
||||
|
||||
pub fn decrement_counter(index: u32) {
|
||||
counter_html_element(index, "decrement_count").click();
|
||||
}
|
||||
|
||||
pub fn enter_count(index: u32, count: i32) {
|
||||
let input = counter_input_element(index, "counter_input");
|
||||
input.set_value(count.to_string().as_str());
|
||||
let mut event_init = EventInit::new();
|
||||
event_init.bubbles(true);
|
||||
let event = Event::new_with_event_init_dict("input", &event_init).unwrap();
|
||||
input.dispatch_event(&event).unwrap();
|
||||
}
|
||||
|
||||
pub fn increment_counter(index: u32) {
|
||||
counter_html_element(index, "increment_count").click();
|
||||
}
|
||||
|
||||
pub fn remove_counter(index: u32) {
|
||||
counter_html_element(index, "remove_counter").click();
|
||||
}
|
||||
|
||||
pub fn view_counters() {
|
||||
remove_existing_counters();
|
||||
mount_to_body(|| view! { <Counters/> });
|
||||
}
|
||||
|
||||
// Results
|
||||
|
||||
pub fn counters() -> i32 {
|
||||
data_test_id("counters").parse::<i32>().unwrap()
|
||||
}
|
||||
|
||||
pub fn title() -> String {
|
||||
leptos::document().title()
|
||||
}
|
||||
|
||||
pub fn total() -> i32 {
|
||||
data_test_id("total").parse::<i32>().unwrap()
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
fn counter_element(index: u32, text: &str) -> Element {
|
||||
let selector =
|
||||
format!("li:nth-child({}) [data-testid=\"{}\"]", index, text);
|
||||
leptos::document()
|
||||
.query_selector(&selector)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn counter_html_element(index: u32, text: &str) -> HtmlElement {
|
||||
counter_element(index, text)
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn counter_input_element(index: u32, text: &str) -> HtmlInputElement {
|
||||
counter_element(index, text)
|
||||
.dyn_into::<HtmlInputElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn data_test_id(id: &str) -> String {
|
||||
let selector = format!("[data-testid=\"{}\"]", id);
|
||||
leptos::document()
|
||||
.query_selector(&selector)
|
||||
.unwrap()
|
||||
.expect("counters not found")
|
||||
.text_content()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn find_by_text(text: &str) -> HtmlElement {
|
||||
let xpath = format!("//*[text()='{}']", text);
|
||||
let document = leptos::document();
|
||||
document
|
||||
.evaluate(&xpath, &document)
|
||||
.unwrap()
|
||||
.iterate_next()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn remove_existing_counters() {
|
||||
if let Some(counter) =
|
||||
leptos::document().query_selector("body div").unwrap()
|
||||
{
|
||||
counter.remove();
|
||||
}
|
||||
}
|
||||
1
examples/counters_stable/tests/web/fixtures/mod.rs
Normal file
1
examples/counters_stable/tests/web/fixtures/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod counters_page;
|
||||
18
examples/counters_stable/tests/web/increment_counter.rs
Normal file
18
examples/counters_stable/tests/web/increment_counter.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::increment_counter(1);
|
||||
ui::increment_counter(1);
|
||||
ui::increment_counter(1);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 3);
|
||||
}
|
||||
16
examples/counters_stable/tests/web/main.rs
Normal file
16
examples/counters_stable/tests/web/main.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
// Test Suites
|
||||
pub mod add_1k_counters;
|
||||
pub mod add_counter;
|
||||
pub mod clear_counters;
|
||||
pub mod decrement_counter;
|
||||
pub mod enter_count;
|
||||
pub mod increment_counter;
|
||||
pub mod remove_counter;
|
||||
pub mod view_counters;
|
||||
|
||||
pub mod fixtures;
|
||||
pub use fixtures::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
18
examples/counters_stable/tests/web/remove_counter.rs
Normal file
18
examples/counters_stable/tests/web/remove_counter.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrement_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::remove_counter(2);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 2);
|
||||
}
|
||||
22
examples/counters_stable/tests/web/view_counters.rs
Normal file
22
examples/counters_stable/tests/web/view_counters.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_see_the_initial_counts() {
|
||||
// When
|
||||
ui::view_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 0);
|
||||
assert_eq!(ui::counters(), 0);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_see_the_title() {
|
||||
// When
|
||||
ui::view_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::title(), "Counters (Stable)");
|
||||
}
|
||||
20
examples/error_boundary/.gitignore
vendored
Normal file
20
examples/error_boundary/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Support playwright testing
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Support trunk
|
||||
dist
|
||||
@@ -1 +1,4 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/playwright-trunk-test.toml" },
|
||||
]
|
||||
|
||||
4
examples/error_boundary/e2e/.gitignore
vendored
Normal file
4
examples/error_boundary/e2e/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
83
examples/error_boundary/e2e/package-lock.json
generated
Normal file
83
examples/error_boundary/e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "grip",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "grip",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
},
|
||||
"node_modules/.pnpm/@playwright+test@1.33.0": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
|
||||
"version": "20.2.1",
|
||||
"extraneous": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
|
||||
"version": "1.33.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
|
||||
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.35.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
|
||||
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
|
||||
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
examples/error_boundary/e2e/package.json
Normal file
7
examples/error_boundary/e2e/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"private": "true",
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
}
|
||||
77
examples/error_boundary/e2e/playwright.config.ts
Normal file
77
examples/error_boundary/e2e/playwright.config.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !process.env.DEV,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.DEV ? 0 : 2,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.DEV ? 1 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [["html", { open: "never" }], ["list"]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1:8080",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "firefox",
|
||||
// use: { ...devices["Desktop Firefox"] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: { ...devices["Desktop Safari"] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: "cd ../ && trunk serve",
|
||||
// url: "http://127.0.0.1:8080",
|
||||
// reuseExistingServer: false, //!process.env.CI,
|
||||
// },
|
||||
});
|
||||
23
examples/error_boundary/e2e/tests/clear_number.spec.ts
Normal file
23
examples/error_boundary/e2e/tests/clear_number.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { HomePage } from "./fixtures/home_page";
|
||||
|
||||
test.describe("Clear Number", () => {
|
||||
test("should see the error message", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.clearInput();
|
||||
|
||||
await expect(ui.errorMessage).toHaveText("Not a number! Errors: ");
|
||||
});
|
||||
test("should see the error list", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.clearInput();
|
||||
|
||||
await expect(ui.errorList).toHaveText(
|
||||
"cannot parse integer from empty string"
|
||||
);
|
||||
});
|
||||
});
|
||||
17
examples/error_boundary/e2e/tests/click_down_arrow.spec.ts
Normal file
17
examples/error_boundary/e2e/tests/click_down_arrow.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { HomePage } from "./fixtures/home_page";
|
||||
|
||||
test.describe("Click Down Arrow", () => {
|
||||
test("should see the negative number", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.clickDownArrow();
|
||||
await ui.clickDownArrow();
|
||||
await ui.clickDownArrow();
|
||||
await ui.clickDownArrow();
|
||||
await ui.clickDownArrow();
|
||||
|
||||
await expect(ui.successMessage).toHaveText("You entered -5");
|
||||
});
|
||||
});
|
||||
15
examples/error_boundary/e2e/tests/click_up_arrow.spec.ts
Normal file
15
examples/error_boundary/e2e/tests/click_up_arrow.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { HomePage } from "./fixtures/home_page";
|
||||
|
||||
test.describe("Click Up Arrow", () => {
|
||||
test("should see the positive number", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.clickUpArrow();
|
||||
await ui.clickUpArrow();
|
||||
await ui.clickUpArrow();
|
||||
|
||||
await expect(ui.successMessage).toHaveText("You entered 3");
|
||||
});
|
||||
});
|
||||
56
examples/error_boundary/e2e/tests/fixtures/home_page.ts
vendored
Normal file
56
examples/error_boundary/e2e/tests/fixtures/home_page.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
|
||||
export class HomePage {
|
||||
readonly page: Page;
|
||||
readonly pageTitle: Locator;
|
||||
readonly numberInput: Locator;
|
||||
readonly successMessage: Locator;
|
||||
readonly errorMessage: Locator;
|
||||
|
||||
readonly errorList: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.pageTitle = page.locator("h1");
|
||||
this.numberInput = page.getByLabel(
|
||||
"Type a number (or something that's not a number!)"
|
||||
);
|
||||
this.successMessage = page.locator("label p");
|
||||
this.errorMessage = page.locator("div p");
|
||||
this.errorList = page.getByRole("list");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/");
|
||||
}
|
||||
|
||||
async enterNumber(count: string, index: number = 0) {
|
||||
await Promise.all([
|
||||
this.numberInput.waitFor(),
|
||||
this.numberInput.fill(count),
|
||||
]);
|
||||
}
|
||||
|
||||
async clickUpArrow() {
|
||||
await Promise.all([
|
||||
this.numberInput.waitFor(),
|
||||
this.numberInput.press("ArrowUp"),
|
||||
]);
|
||||
}
|
||||
|
||||
async clickDownArrow() {
|
||||
await Promise.all([
|
||||
this.numberInput.waitFor(),
|
||||
this.numberInput.press("ArrowDown"),
|
||||
]);
|
||||
}
|
||||
|
||||
async clearInput() {
|
||||
await Promise.all([
|
||||
this.numberInput.waitFor(),
|
||||
this.clickUpArrow(),
|
||||
this.numberInput.press("Backspace"),
|
||||
]);
|
||||
}
|
||||
}
|
||||
11
examples/error_boundary/e2e/tests/open_app.spec.ts
Normal file
11
examples/error_boundary/e2e/tests/open_app.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { HomePage } from "./fixtures/home_page";
|
||||
|
||||
test.describe("Open App", () => {
|
||||
test("should see the page title", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await expect(ui.pageTitle).toHaveText("Error Handling");
|
||||
});
|
||||
});
|
||||
13
examples/error_boundary/e2e/tests/type_number.spec.ts
Normal file
13
examples/error_boundary/e2e/tests/type_number.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { HomePage } from "./fixtures/home_page";
|
||||
|
||||
test.describe("Type Number", () => {
|
||||
test("should see the typed number", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.enterNumber("7");
|
||||
|
||||
await expect(ui.successMessage).toHaveText("You entered 7");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::errors::AppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{Errors, *};
|
||||
use leptos::{logging::log, Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
Router,
|
||||
};
|
||||
use errors_axum::*;
|
||||
use leptos::*;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
}}
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ pub fn Stories() -> impl IntoView {
|
||||
let (pending, set_pending) = create_signal(false);
|
||||
|
||||
let hide_more_link = move || {
|
||||
pending()
|
||||
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|
||||
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|
||||
|| pending()
|
||||
};
|
||||
|
||||
view! {
|
||||
@@ -65,20 +65,16 @@ pub fn Stories() -> impl IntoView {
|
||||
}}
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<Transition
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
>
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</Transition>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
|
||||
@@ -25,48 +25,45 @@ pub fn Story() -> impl IntoView {
|
||||
};
|
||||
|
||||
view! {
|
||||
<>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<Meta name="description" content=meta_description/>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
{move || story.get().map(|story| match story {
|
||||
None => view! { <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! {
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
{move || story.get().map(|story| match story {
|
||||
None => view! { <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! {
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
}})
|
||||
}
|
||||
</Suspense>
|
||||
</>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}})}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos::{logging::log, *};
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
|
||||
@@ -36,12 +36,11 @@ pub fn Stories() -> impl IntoView {
|
||||
let (pending, set_pending) = create_signal(false);
|
||||
|
||||
let hide_more_link = move || {
|
||||
pending()
|
||||
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|
||||
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|
||||
|| pending()
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
<div class="news-view">
|
||||
<div class="news-list-nav">
|
||||
<span>
|
||||
@@ -65,20 +64,16 @@ pub fn Stories() -> impl IntoView {
|
||||
}}
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<Transition
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
>
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</Transition>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
|
||||
@@ -25,48 +25,45 @@ pub fn Story() -> impl IntoView {
|
||||
};
|
||||
|
||||
view! {
|
||||
<>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<Meta name="description" content=meta_description/>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
{move || story.get().map(|story| match story {
|
||||
None => view! { <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! {
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move | comment| view! { <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
{move || story.get().map(|story| match story {
|
||||
None => view! { <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! {
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
}})
|
||||
}
|
||||
</Suspense>
|
||||
</>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move | comment| view! { <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}})}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,8 +51,11 @@ site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style/output.css"
|
||||
# The tailwind input file.
|
||||
#
|
||||
# Optional, Activates the tailwind build
|
||||
tailwind-input-file = "style/tailwind.css"
|
||||
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
|
||||
@@ -1,642 +0,0 @@
|
||||
/*
|
||||
! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
6. Use the user's configured `sans` font-variation-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
font-variation-settings: normal;
|
||||
/* 6 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.m-1 {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
.m-auto {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.h-5 {
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-5 {
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.flex-row-reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.border-b-4 {
|
||||
border-bottom-width: 4px;
|
||||
}
|
||||
|
||||
.border-l-2 {
|
||||
border-left-width: 2px;
|
||||
}
|
||||
|
||||
.border-blue-800 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(30 64 175 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-blue-900 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(30 58 138 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-800 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gradient-to-tl {
|
||||
background-image: linear-gradient(to top left, var(--tw-gradient-stops));
|
||||
}
|
||||
|
||||
.from-blue-800 {
|
||||
--tw-gradient-from: #1e40af var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(30 64 175 / 0) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||
}
|
||||
|
||||
.to-blue-500 {
|
||||
--tw-gradient-to: #3b82f6 var(--tw-gradient-to-position);
|
||||
}
|
||||
|
||||
.fill-current {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.px-3 {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ where
|
||||
}
|
||||
api::Error::Api(err) => err.message,
|
||||
};
|
||||
error!(
|
||||
log::error!(
|
||||
"Unable to login with {}: {msg}",
|
||||
credentials.email
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
Page,
|
||||
};
|
||||
use api_boundary::*;
|
||||
use leptos::*;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod api;
|
||||
use crate::api::*;
|
||||
use leptos::*;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_router::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -122,9 +122,9 @@ pub struct ContactParams {
|
||||
|
||||
#[component]
|
||||
pub fn Contact() -> impl IntoView {
|
||||
log::debug!("rendering <Contact/>");
|
||||
log!("rendering <Contact/>");
|
||||
|
||||
log::debug!(
|
||||
log!(
|
||||
"ExampleContext should be Some(42). It is {:?}",
|
||||
use_context::<ExampleContext>()
|
||||
);
|
||||
@@ -178,13 +178,13 @@ pub fn Contact() -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn About() -> impl IntoView {
|
||||
log::debug!("rendering <About/>");
|
||||
log!("rendering <About/>");
|
||||
|
||||
on_cleanup(|| {
|
||||
log!("cleaning up <About/>");
|
||||
});
|
||||
|
||||
log::debug!(
|
||||
log!(
|
||||
"ExampleContext should be Some(0). It is {:?}",
|
||||
use_context::<ExampleContext>()
|
||||
);
|
||||
@@ -209,7 +209,7 @@ pub fn About() -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn Settings() -> impl IntoView {
|
||||
log::debug!("rendering <Settings/>");
|
||||
log!("rendering <Settings/>");
|
||||
|
||||
on_cleanup(|| {
|
||||
log!("cleaning up <Settings/>");
|
||||
|
||||
@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use session_auth_axum::state::AppState;
|
||||
use session_auth_axum::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
|
||||
use leptos::{log, view, provide_context, get_configuration};
|
||||
use leptos::{logging::log, view, provide_context, get_configuration};
|
||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{routing::post, Router};
|
||||
use leptos::*;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
|
||||
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/webdriver.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
APP_PROCESS_NAME = "suspense_tests"
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = [
|
||||
"install-cargo-leptos",
|
||||
"start-webdriver",
|
||||
"test-e2e-with-auto-start",
|
||||
]
|
||||
|
||||
[tasks.test-e2e-with-auto-start]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
CLIENT_PROCESS_NAME = "suspense_tests"
|
||||
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
|
||||
@@ -156,7 +156,7 @@ fn NestedResourceInside() -> impl IntoView {
|
||||
{move || {
|
||||
one_second.get().map(|_| {
|
||||
let two_second = create_resource(|| (), move |_| async move {
|
||||
leptos::log!("creating two_second resource");
|
||||
logging::log!("creating two_second resource");
|
||||
second_wait_fn(WAIT_TWO_SECONDS).await
|
||||
});
|
||||
view! {
|
||||
|
||||
@@ -84,9 +84,10 @@ site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style/output.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
# The tailwind input file.
|
||||
#
|
||||
# Optional, Activates the tailwind build
|
||||
tailwind-input-file = "style/tailwind.css"
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/playwright.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
CLIENT_PROCESS_NAME = "tailwind"
|
||||
|
||||
@@ -8,10 +8,6 @@ If you don't have `cargo-leptos` installed you can install it with
|
||||
|
||||
Then run
|
||||
|
||||
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
|
||||
|
||||
and
|
||||
|
||||
`cargo leptos watch`
|
||||
|
||||
in this directory.
|
||||
|
||||
2
examples/tailwind/rust-toolchain.toml
Normal file
2
examples/tailwind/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
@@ -12,7 +12,7 @@ cfg_if! {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
log!("hydrate mode - hydrating");
|
||||
logging::log!("hydrate mode - hydrating");
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
@@ -29,7 +29,7 @@ cfg_if! {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
log!("csr mode - mounting to body");
|
||||
logging::log!("csr mode - mounting to body");
|
||||
|
||||
mount_to_body(|| {
|
||||
view! { <App /> }
|
||||
|
||||
@@ -1,650 +0,0 @@
|
||||
/*
|
||||
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
.my-0 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.max-w-3xl {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-sm {
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.bg-amber-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(217 119 6 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(2 132 199 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(125 211 252 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(14 165 233 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(245 158 11 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.px-10 {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.px-5 {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.py-3 {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.pb-10 {
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-200 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 202 202 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(14 165 233 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(125 211 252 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(3 105 161 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(7 89 133 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(153 27 27 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-sky-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
|
||||
}
|
||||
@@ -7,7 +7,7 @@ pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
log!("csr mode - mounting to body");
|
||||
logging::log!("csr mode - mounting to body");
|
||||
|
||||
mount_to_body(|| {
|
||||
view! { <App /> }
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/webdriver.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
APP_PROCESS_NAME = "todo_app_sqlite"
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = [
|
||||
"install-cargo-leptos",
|
||||
"start-webdriver",
|
||||
"test-e2e-with-auto-start",
|
||||
]
|
||||
|
||||
[tasks.test-e2e-with-auto-start]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
CLIENT_PROCESS_NAME = "todo_app_sqlite"
|
||||
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/webdriver.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
APP_PROCESS_NAME = "todo_app_sqlite_axum"
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = [
|
||||
"install-cargo-leptos",
|
||||
"start-webdriver",
|
||||
"test-e2e-with-auto-start",
|
||||
]
|
||||
|
||||
[tasks.test-e2e-with-auto-start]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
CLIENT_PROCESS_NAME = "todo_app_sqlite_axum"
|
||||
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
|
||||
@@ -60,7 +60,7 @@ cfg_if! {
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
logging::log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
|
||||
@@ -67,7 +67,7 @@ cfg_if! {
|
||||
|
||||
// run our app with hyper
|
||||
// `viz::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
logging::log!("listening on http://{}", &addr);
|
||||
viz::Server::bind(&addr)
|
||||
.serve(ServiceMaker::from(app))
|
||||
.await
|
||||
|
||||
@@ -210,13 +210,7 @@ pub fn TodoMVC() -> impl IntoView {
|
||||
// focus the main input on load
|
||||
create_effect(move |_| {
|
||||
if let Some(input) = input_ref.get() {
|
||||
// We use request_animation_frame here because the NodeRef
|
||||
// is filled when the element is created, but before it's mounted
|
||||
// to the DOM. Calling .focus() before it's mounted does nothing.
|
||||
// So inside, we wait a tick for the browser to mount it, then .focus()
|
||||
request_animation_frame(move || {
|
||||
let _ = input.focus();
|
||||
});
|
||||
let _ = input.focus();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
use regex::Regex;
|
||||
use std::{fmt::Display, future::Future, sync::Arc};
|
||||
#[cfg(debug_assertions)]
|
||||
use tracing::instrument;
|
||||
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
|
||||
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
|
||||
|
||||
@@ -7,14 +7,21 @@ extern crate tracing;
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
|
||||
let site_ip = &options.site_addr.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
let reload_port = match options.reload_external_port {
|
||||
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 ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
let host = window.location.hostname;
|
||||
let ws = new WebSocket({protocol} + host + ':{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
let msg = JSON.parse(ev.data);
|
||||
if (msg.all) window.location.reload();
|
||||
|
||||
@@ -16,14 +16,12 @@ leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.14"
|
||||
typed-builder = "0.16"
|
||||
typed-builder-macro = "0.16"
|
||||
server_fn = { workspace = true }
|
||||
web-sys = { version = "0.3.63", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "." }
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
template_macro = ["leptos_dom/web", "web-sys", "wasm-bindgen"]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user