Compare commits

..

45 Commits
1457 ... arena

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

* refactor(examples): extract client process tasks

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

* test(error_boundary): click up arrow

* test(error_boundary): click down arrow

* test(error_boundary): type number

* test(error_boundary): clear number

* fix(build): clean trunk directories

* fix(test-report): detect unit tests

* ci(build): echo stop trunk
2023-09-04 13:25:44 -04:00
Greg Johnston
70e1ad41e2 Merge pull request #1579 from leptos-rs/rusty
feat: start adding some Rustier interfaces for reactive types
2023-09-04 13:23:18 -04:00
Greg Johnston
53ec7ed272 feat: add Effect::with_value_mut() 2023-09-04 11:14:07 -04:00
Greg Johnston
d98a577740 feat: add Rustier interfaces for reactive system types 2023-09-04 11:05:23 -04:00
jquesada2016
fd834f48c2 change: rename .derived_signal() and .mapped_signal_setter() methods (#1637) 2023-09-04 08:41:41 -04:00
Greg Johnston
7be65a37c6 fix: versioned resources never decrement Suspense (closes #1640) (#1641) 2023-09-03 20:21:16 -04:00
Banzobotic
3b5e2d86fb docs: clean up messy spacing left over from cx replacements (#1626) 2023-09-03 20:21:05 -04:00
jquesada2016
716b9fb50b feat: add .into_X_boxed() for classes, properties, and styles as for attributes 2023-09-03 20:18:49 -04:00
jquesada2016
006ca13797 chore: hide get_property (#1638) 2023-09-03 20:15:15 -04:00
Village
6e008343c8 feat: add component generics (#1636) 2023-09-03 20:09:50 -04:00
Greg Johnston
2ca24883ac fix: memoize Suspense readiness to avoid rerendering children/fallback (#1642) 2023-09-03 20:07:20 -04:00
Village
4a43983f4e feat: implement spreading attributes onto elements (#1619) 2023-09-01 20:52:15 -04:00
IcosaHedron
d9e83121c1 feat: add reload websocket configuration and enable env configuration (#1613) 2023-09-01 20:51:46 -04:00
Antonin Peronnet
f5b4b97c9b feat: Callback types to make it easier to accept (optional) callback props (#1596) 2023-09-01 20:51:32 -04:00
Gareth
bcfa430a40 docs: fix incorrect variable name (#1623) 2023-09-01 07:39:41 -04:00
Lawrence Qupty
7c51815cf5 docs: remove extra space (#1622) 2023-09-01 07:39:05 -04:00
Dmitry Pytaylo
fee2fb953b docs: fix typo (#1618) 2023-09-01 07:37:52 -04:00
Sadra M
8ecb7f59c4 docs: update references to server binary in dockerfile (#1617) 2023-09-01 07:37:24 -04:00
martin frances
b85cb9fb3b docs: clarify how many times derived signals are called (#1614) 2023-09-01 07:36:15 -04:00
Joseph Cruz
a631c5ca1c doc(examples): report fantoccini use (#1616) 2023-09-01 07:35:29 -04:00
Greg Johnston
bee9bd8f67 0.5.0-beta2 2023-08-29 21:23:59 -04:00
Greg Johnston
8d3874f8a9 cargo fmt 2023-08-29 21:19:24 -04:00
Einherjar
bade16d227 docs: discuss unique paths for #[server] functions (#1610) 2023-08-29 20:49:31 -04:00
Jon Cahill
e0a132bde3 fix: don't try to parse as JSON the result from a server function redirect (#1604) 2023-08-29 20:42:19 -04:00
Daniel Oliveira
4d7e1f4d26 feat: improve server function client side error handling (#1597)
Handle all error codes 401-499 in addition to the
400 and 500-599 that were already handled.

In addition, handle them all in the same way
and improve the error message.
2023-08-29 20:40:03 -04:00
Maneren
700eee6604 fix(macro/params): clippy warning (#1612) 2023-08-29 20:31:54 -04:00
Joseph Cruz
694ed61e4c fix(ci): add new webkit dependency (#1607) (#1608) 2023-08-28 11:08:47 -04:00
Joseph Cruz
d7330097ba chore(examples): improve cucumber support #1598 (#1599)
* chore(examples): add cucumber runner

* chore(examples): clean cargo recursively
2023-08-28 11:08:22 -04:00
Greg Johnston
c65a3a6ca3 docs: add docs for builder syntax (#1603) 2023-08-28 11:08:07 -04:00
Danik Vitek
793c191619 feat: Oco (Owned Clones Once) smart pointer (#1480) 2023-08-26 11:43:51 -04:00
Greg Johnston
6c3e2fe53e feat: update to typed-builder 0.16 (closes #1455) (#1590) 2023-08-26 10:10:42 -04:00
Greg Johnston
08c419e3ee fix: broken test with untrack in tracing props (#1593) 2023-08-26 09:20:14 -04:00
Greg Johnston
736f4185b5 Merge pull request #1588 from leptos-rs/1457
Some resource and transition fixes
2023-08-26 07:34:21 -04:00
Greg Johnston
9cc0fc8c49 fix: adjust tracing properties 2023-08-26 07:24:52 -04:00
163 changed files with 4453 additions and 2290 deletions

View File

@@ -24,7 +24,7 @@ jobs:
steps:
# Setup environment
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Rust
uses: actions-rs/toolchain@v1
@@ -95,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 }}

2
.gitignore vendored
View File

@@ -9,3 +9,5 @@ Cargo.lock
.idea
.direnv
.envrc
.vscode

View File

@@ -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
View File

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

View File

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

View File

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

View File

@@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,6 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
#[component]
pub fn BusyButton() -> impl IntoView {
view! {
<button on:click=move |_| {
spawn_local(async {
add_todo("So much to do!".to_string()).await;
@@ -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 well 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).)
- Youll 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 well 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 didnt 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 its very important to remember. **Server functions are not magic; theyre syntax sugar for defining a public API.** The _body_ of a server function is never made public; its just part of your server binary. But the server function is a publicly accessible API endpoint, and its return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,98 @@
# No Macros: The View Builder Syntax
> If youre perfectly happy with the `view!` macro syntax described so far, youre 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 dont 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 youd rather not use the `view` macro, you can simply use that expanded syntax yourself. And its 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, its 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 thats 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 cant have additional builder methods applied to it without causing inconsistencies.
> 2. Using the builder syntax will result in less-than-optimal SSR performance. It wont be slow, by any means (and its worth running your own benchmarks in any case), just slower than the `view`-optimized version.

View File

@@ -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
'''

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = '''

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,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)]

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"]

View File

@@ -1,10 +1,11 @@
use crate::TextProp;
use std::rc::Rc;
/// A collection of additional HTML attributes to be applied to an element,
/// each of which may or may not be reactive.
#[derive(Default, Clone)]
#[derive(Clone)]
#[repr(transparent)]
pub struct AdditionalAttributes(pub(crate) Vec<(String, TextProp)>);
pub struct AdditionalAttributes(pub(crate) Rc<[(String, TextProp)]>);
impl<I, T, U> From<I> for AdditionalAttributes
where
@@ -22,6 +23,12 @@ where
}
}
impl Default for AdditionalAttributes {
fn default() -> Self {
Self([].into_iter().collect())
}
}
/// Iterator over additional HTML attributes.
#[repr(transparent)]
pub struct AdditionalAttributesIter<'a>(

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

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

View File

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

View File

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

View File

@@ -155,11 +155,15 @@ pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
pub use leptos_dom::{
self, create_node_ref, debug_warn, document, error, ev, helpers::*, html,
log, math, mount_to, mount_to_body, nonce, svg, warn, window, Attribute,
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
self, create_node_ref, document, ev, helpers::*, html, math, mount_to,
mount_to_body, nonce, svg, window, Attribute, Class, CollectView, Errors,
Fragment, HtmlElement, IntoAttribute, IntoClass, IntoProperty, IntoStyle,
IntoView, NodeRef, Property, View,
};
/// Utilities for simple isomorphic logging to the console or terminal.
pub mod logging {
pub use leptos_dom::{debug_warn, error, log, warn};
}
/// Types to make it easier to handle errors in your application.
pub mod error {
@@ -175,7 +179,6 @@ pub use leptos_server::{
ServerFnErrorErr,
};
pub use server_fn::{self, ServerFn as _};
pub use typed_builder;
#[cfg(all(target_arch = "wasm32", feature = "template_macro"))]
pub use {leptos_macro::template, wasm_bindgen, web_sys};
mod error_boundary;
@@ -195,20 +198,16 @@ pub use text_prop::TextProp;
#[doc(hidden)]
pub use tracing;
pub use transition::*;
#[doc(hidden)]
pub use typed_builder;
#[doc(hidden)]
pub use typed_builder::Optional;
#[doc(hidden)]
pub use typed_builder_macro;
mod children;
pub use children::*;
extern crate self as leptos;
/// The most common type for the `children` property on components,
/// which can only be called once.
pub type Children = Box<dyn FnOnce() -> Fragment>;
/// A type for the `children` property on components that can be called
/// more than once.
pub type ChildrenFn = Box<dyn Fn() -> Fragment>;
/// A type for the `children` property on components that can be called
/// more than once, but may mutate the children.
pub type ChildrenFnMut = Box<dyn FnMut() -> Fragment>;
/// A type for taking anything that implements [`IntoAttribute`].
///
/// ```rust

View File

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

View File

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

View File

@@ -1,14 +1,15 @@
use leptos_reactive::Oco;
use std::{fmt::Debug, rc::Rc};
/// Describes a value that is either a static or a reactive string, i.e.,
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
#[derive(Clone)]
pub struct TextProp(Rc<dyn Fn() -> String>);
pub struct TextProp(Rc<dyn Fn() -> Oco<'static, str>>);
impl TextProp {
/// Accesses the current value of the property.
#[inline(always)]
pub fn get(&self) -> String {
pub fn get(&self) -> Oco<'static, str> {
(self.0)()
}
}
@@ -21,23 +22,38 @@ impl Debug for TextProp {
impl From<String> for TextProp {
fn from(s: String) -> Self {
let s: Oco<'_, str> = Oco::Counted(Rc::from(s));
TextProp(Rc::new(move || s.clone()))
}
}
impl From<&str> for TextProp {
fn from(s: &str) -> Self {
let s = s.to_string();
impl From<&'static str> for TextProp {
fn from(s: &'static str) -> Self {
let s: Oco<'_, str> = s.into();
TextProp(Rc::new(move || s.clone()))
}
}
impl<F> From<F> for TextProp
impl From<Rc<str>> for TextProp {
fn from(s: Rc<str>) -> Self {
let s: Oco<'_, str> = s.into();
TextProp(Rc::new(move || s.clone()))
}
}
impl From<Oco<'static, str>> for TextProp {
fn from(s: Oco<'static, str>) -> Self {
TextProp(Rc::new(move || s.clone()))
}
}
impl<F, S> From<F> for TextProp
where
F: Fn() -> String + 'static,
F: Fn() -> S + 'static,
S: Into<Oco<'static, str>>,
{
#[inline(always)]
fn from(s: F) -> Self {
TextProp(Rc::new(s))
TextProp(Rc::new(move || s().into()))
}
}

View File

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

View File

@@ -13,7 +13,7 @@ config = "0.13.3"
regex = "1.7.0"
serde = { version = "1.0.151", features = ["derive"] }
thiserror = "1.0.38"
typed-builder = "0.14"
typed-builder = "0.16"
[dev-dependencies]
tokio = { version = "1", features = ["rt", "macros"] }

View File

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

View File

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

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

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

View File

@@ -14,12 +14,12 @@ pub use dyn_child::*;
pub use each::*;
pub use errors::*;
pub use fragment::*;
use leptos_reactive::untrack_with_diagnostics;
use leptos_reactive::{untrack_with_diagnostics, Oco};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use once_cell::unsync::OnceCell;
use std::fmt;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::rc::Rc;
use std::{borrow::Cow, fmt};
pub use unit::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::JsCast;
@@ -55,7 +55,7 @@ pub struct ComponentRepr {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
mounted: Rc<OnceCell<()>>,
#[cfg(any(debug_assertions, feature = "ssr"))]
pub(crate) name: Cow<'static, str>,
pub(crate) name: Oco<'static, str>,
#[cfg(debug_assertions)]
_opening: Comment,
/// The children of the component.
@@ -163,24 +163,24 @@ impl IntoView for ComponentRepr {
impl ComponentRepr {
/// Creates a new [`Component`].
#[inline(always)]
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
Self::new_with_id_concrete(name.into(), HydrationCtx::id())
}
/// Creates a new [`Component`] with the given hydration ID.
#[inline(always)]
pub fn new_with_id(
name: impl Into<Cow<'static, str>>,
name: impl Into<Oco<'static, str>>,
id: HydrationKey,
) -> Self {
Self::new_with_id_concrete(name.into(), id)
}
fn new_with_id_concrete(name: Cow<'static, str>, id: HydrationKey) -> Self {
fn new_with_id_concrete(name: Oco<'static, str>, id: HydrationKey) -> Self {
let markers = (
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
Comment::new(format!("</{name}>"), &id, true),
#[cfg(debug_assertions)]
Comment::new(Cow::Owned(format!("<{name}>")), &id, false),
Comment::new(format!("<{name}>"), &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -236,7 +236,7 @@ where
V: IntoView,
{
id: HydrationKey,
name: Cow<'static, str>,
name: Oco<'static, str>,
children_fn: F,
}
@@ -246,7 +246,7 @@ where
V: IntoView,
{
/// Creates a new component.
pub fn new(name: impl Into<Cow<'static, str>>, f: F) -> Self {
pub fn new(name: impl Into<Oco<'static, str>>, f: F) -> Self {
Self {
id: HydrationCtx::id(),
name: name.into(),

View File

@@ -3,7 +3,7 @@ use crate::{
Comment, IntoView, View,
};
use cfg_if::cfg_if;
use std::{borrow::Cow, cell::RefCell, fmt, ops::Deref, rc::Rc};
use std::{cell::RefCell, fmt, ops::Deref, rc::Rc};
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use crate::{mount_child, prepare_to_move, unmount_child, MountKind, Mountable, Text};
@@ -83,9 +83,9 @@ impl Mountable for DynChildRepr {
impl DynChildRepr {
fn new_with_id(id: HydrationKey) -> Self {
let markers = (
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
Comment::new("</DynChild>", &id, true),
#[cfg(debug_assertions)]
Comment::new(Cow::Borrowed("<DynChild>"), &id, false),
Comment::new("<DynChild>", &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]

View File

@@ -2,7 +2,7 @@
use crate::hydration::HydrationKey;
use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View};
use leptos_reactive::{as_child_of_current_owner, Disposer};
use std::{borrow::Cow, cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc};
use std::{cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use web::*;
@@ -79,9 +79,9 @@ impl Default for EachRepr {
let id = HydrationCtx::id();
let markers = (
Comment::new(Cow::Borrowed("</Each>"), &id, true),
Comment::new("</Each>", &id, true),
#[cfg(debug_assertions)]
Comment::new(Cow::Borrowed("<Each>"), &id, false),
Comment::new("<Each>", &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -224,13 +224,13 @@ impl EachItem {
let markers = (
if needs_closing {
Some(Comment::new(Cow::Borrowed("</EachItem>"), &id, true))
Some(Comment::new("</EachItem>", &id, true))
} else {
None
},
#[cfg(debug_assertions)]
if needs_closing {
Some(Comment::new(Cow::Borrowed("<EachItem>"), &id, false))
Some(Comment::new("<EachItem>", &id, false))
} else {
None
},

View File

@@ -1,6 +1,7 @@
pub mod typed;
use std::{borrow::Cow, cell::RefCell, collections::HashSet};
use leptos_reactive::Oco;
use std::{cell::RefCell, collections::HashSet};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::{
convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue,
@@ -8,7 +9,7 @@ use wasm_bindgen::{
};
thread_local! {
pub(crate) static GLOBAL_EVENTS: RefCell<HashSet<Cow<'static, str>>> = RefCell::new(HashSet::new());
pub(crate) static GLOBAL_EVENTS: RefCell<HashSet<Oco<'static, str>>> = RefCell::new(HashSet::new());
}
// Used in template macro
@@ -47,8 +48,8 @@ pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub fn add_event_listener<E>(
target: &web_sys::Element,
key: Cow<'static, str>,
event_name: Cow<'static, str>,
key: Oco<'static, str>,
event_name: Oco<'static, str>,
#[cfg(debug_assertions)] mut cb: Box<dyn FnMut(E)>,
#[cfg(not(debug_assertions))] cb: Box<dyn FnMut(E)>,
options: &Option<web_sys::AddEventListenerOptions>,
@@ -115,7 +116,7 @@ pub(crate) fn add_event_listener_undelegated<E>(
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn add_delegated_event_listener(
key: &str,
event_name: Cow<'static, str>,
event_name: Oco<'static, str>,
options: &Option<web_sys::AddEventListenerOptions>,
) {
GLOBAL_EVENTS.with(|global_events| {

View File

@@ -1,6 +1,7 @@
//! Types for all DOM events.
use std::{borrow::Cow, marker::PhantomData};
use leptos_reactive::Oco;
use std::marker::PhantomData;
use wasm_bindgen::convert::FromWasmAbi;
/// A trait for converting types into [web_sys events](web_sys).
@@ -16,10 +17,10 @@ pub trait EventDescriptor: Clone {
const BUBBLES: bool;
/// The name of the event, such as `click` or `mouseover`.
fn name(&self) -> Cow<'static, str>;
fn name(&self) -> Oco<'static, str>;
/// The key used for event delegation.
fn event_delegation_key(&self) -> Cow<'static, str>;
fn event_delegation_key(&self) -> Oco<'static, str>;
/// Return the options for this type. This is only used when you create a [`Custom`] event
/// handler.
@@ -39,12 +40,12 @@ impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
type EventType = Ev::EventType;
#[inline(always)]
fn name(&self) -> Cow<'static, str> {
fn name(&self) -> Oco<'static, str> {
self.0.name()
}
#[inline(always)]
fn event_delegation_key(&self) -> Cow<'static, str> {
fn event_delegation_key(&self) -> Oco<'static, str> {
self.0.event_delegation_key()
}
@@ -54,7 +55,7 @@ impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
/// A custom event.
#[derive(Debug)]
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
name: Cow<'static, str>,
name: Oco<'static, str>,
options: Option<web_sys::AddEventListenerOptions>,
_event_type: PhantomData<E>,
}
@@ -72,11 +73,11 @@ impl<E: FromWasmAbi> Clone for Custom<E> {
impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
type EventType = E;
fn name(&self) -> Cow<'static, str> {
fn name(&self) -> Oco<'static, str> {
self.name.clone()
}
fn event_delegation_key(&self) -> Cow<'static, str> {
fn event_delegation_key(&self) -> Oco<'static, str> {
format!("$$${}", self.name).into()
}
@@ -92,7 +93,7 @@ impl<E: FromWasmAbi> Custom<E> {
/// Creates a custom event type that can be used within
/// [`HtmlElement::on`](crate::HtmlElement::on), for events
/// which are not covered in the [`ev`](crate::ev) module.
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
Self {
name: name.into(),
options: None,
@@ -299,12 +300,12 @@ macro_rules! generate_event_types {
type EventType = web_sys::$web_event;
#[inline(always)]
fn name(&self) -> Cow<'static, str> {
fn name(&self) -> Oco<'static, str> {
stringify!([< $($event)+ >]).into()
}
#[inline(always)]
fn event_delegation_key(&self) -> Cow<'static, str> {
fn event_delegation_key(&self) -> Oco<'static, str> {
concat!("$$$", stringify!([< $($event)+ >])).into()
}

View File

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

View File

@@ -63,15 +63,18 @@ cfg_if! {
use crate::{
ev::EventDescriptor,
hydration::HydrationCtx,
macro_helpers::{IntoAttribute, IntoClass, IntoProperty, IntoStyle},
macro_helpers::{
Attribute, IntoAttribute, IntoClass, IntoProperty, IntoStyle,
},
Element, Fragment, IntoView, NodeRef, Text, View,
};
use std::{borrow::Cow, fmt};
use leptos_reactive::Oco;
use std::fmt;
/// Trait which allows creating an element tag.
pub trait ElementDescriptor: ElementDescriptorBounds {
/// The name of the element, i.e., `div`, `p`, `custom-element`.
fn name(&self) -> Cow<'static, str>;
fn name(&self) -> Oco<'static, str>;
/// Determines if the tag is void, i.e., `<input>` and `<br>`.
#[inline(always)]
@@ -126,7 +129,7 @@ where
/// Represents potentially any element.
#[derive(Clone, Debug)]
pub struct AnyElement {
pub(crate) name: Cow<'static, str>,
pub(crate) name: Oco<'static, str>,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) element: web_sys::HtmlElement,
pub(crate) is_void: bool,
@@ -159,7 +162,7 @@ impl std::convert::AsRef<web_sys::HtmlElement> for AnyElement {
}
impl ElementDescriptor for AnyElement {
fn name(&self) -> Cow<'static, str> {
fn name(&self) -> Oco<'static, str> {
self.name.clone()
}
@@ -178,7 +181,7 @@ impl ElementDescriptor for AnyElement {
/// Represents a custom HTML element, such as `<my-element>`.
#[derive(Clone, Debug)]
pub struct Custom {
name: Cow<'static, str>,
name: Oco<'static, str>,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
element: web_sys::HtmlElement,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
@@ -187,7 +190,7 @@ pub struct Custom {
impl Custom {
/// Creates a new custom element with the given tag name.
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
let name = name.into();
let id = HydrationCtx::id();
@@ -266,7 +269,7 @@ impl std::convert::AsRef<web_sys::HtmlElement> for Custom {
}
impl ElementDescriptor for Custom {
fn name(&self) -> Cow<'static, str> {
fn name(&self) -> Oco<'static, str> {
self.name.clone()
}
@@ -294,12 +297,12 @@ cfg_if! {
#[derive(educe::Educe, Clone)]
#[educe(Debug)]
pub struct HtmlElement<El: ElementDescriptor> {
pub(crate) element: El,
pub(crate) attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
#[educe(Debug(ignore))]
pub(crate) children: ElementChildren,
#[cfg(debug_assertions)]
pub(crate) view_marker: Option<String>
pub(crate) element: El,
pub(crate) attrs: SmallVec<[(Oco<'static, str>, Oco<'static, str>); 4]>,
#[educe(Debug(ignore))]
pub(crate) children: ElementChildren,
#[cfg(debug_assertions)]
pub(crate) view_marker: Option<String>
}
#[derive(Clone, educe::Educe, PartialEq, Eq)]
@@ -308,14 +311,14 @@ cfg_if! {
#[educe(Default)]
Empty,
Children(Vec<View>),
InnerHtml(Cow<'static, str>),
InnerHtml(Oco<'static, str>),
Chunks(Vec<StringOrView>)
}
#[doc(hidden)]
#[derive(Clone)]
pub enum StringOrView {
String(Cow<'static, str>),
String(Oco<'static, str>),
View(std::rc::Rc<dyn Fn() -> View>)
}
@@ -445,7 +448,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
/// Adds an `id` to the element.
#[track_caller]
#[inline(always)]
pub fn id(self, id: impl Into<Cow<'static, str>>) -> Self {
pub fn id(self, id: impl Into<Oco<'static, str>>) -> Self {
let id = id.into();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -575,7 +578,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg_attr(all(target_arch = "wasm32", feature = "web"), inline(always))]
pub fn attr(
self,
name: impl Into<Cow<'static, str>>,
name: impl Into<Oco<'static, str>>,
attr: impl IntoAttribute,
) -> Self {
let name = name.into();
@@ -592,8 +595,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
use crate::macro_helpers::Attribute;
let mut this = self;
let mut attr = attr.into_attribute();
@@ -621,6 +622,18 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
}
/// Adds multiple attributes to the element
#[track_caller]
pub fn attrs(
mut self,
attrs: impl std::iter::IntoIterator<Item = (&'static str, Attribute)>,
) -> Self {
for (name, value) in attrs {
self = self.attr(name, value);
}
self
}
/// Adds a class to an element.
///
/// **Note**: In the builder syntax, this will be overwritten by the `class`
@@ -634,7 +647,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[track_caller]
pub fn class(
self,
name: impl Into<Cow<'static, str>>,
name: impl Into<Oco<'static, str>>,
class: impl IntoClass,
) -> Self {
let name = name.into();
@@ -686,7 +699,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
/// Adds a list of classes separated by ASCII whitespace to an element.
#[track_caller]
#[inline(always)]
pub fn classes(self, classes: impl Into<Cow<'static, str>>) -> Self {
pub fn classes(self, classes: impl Into<Oco<'static, str>>) -> Self {
self.classes_inner(&classes.into())
}
@@ -698,7 +711,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
) -> Self
where
I: IntoIterator<Item = C>,
C: Into<Cow<'static, str>>,
C: Into<Oco<'static, str>>,
{
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
@@ -708,12 +721,12 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
leptos_reactive::create_effect(
move |prev_classes: Option<
SmallVec<[Cow<'static, str>; 4]>,
SmallVec<[Oco<'static, str>; 4]>,
>| {
let classes = classes_signal()
.into_iter()
.map(Into::into)
.collect::<SmallVec<[Cow<'static, str>; 4]>>(
.collect::<SmallVec<[Oco<'static, str>; 4]>>(
);
let new_classes = classes
@@ -797,7 +810,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[track_caller]
pub fn style(
self,
name: impl Into<Cow<'static, str>>,
name: impl Into<Oco<'static, str>>,
style: impl IntoStyle,
) -> Self {
let name = name.into();
@@ -856,7 +869,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[track_caller]
pub fn prop(
self,
name: impl Into<Cow<'static, str>>,
name: impl Into<Oco<'static, str>>,
value: impl IntoProperty,
) -> Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -1016,7 +1029,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
/// sanitize the input to avoid a cross-site scripting (XSS)
/// vulnerability.
#[inline(always)]
pub fn inner_html(self, html: impl Into<Cow<'static, str>>) -> Self {
pub fn inner_html(self, html: impl Into<Oco<'static, str>>) -> Self {
let html = html.into();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -1103,7 +1116,7 @@ pub fn custom<El: ElementDescriptor>(el: El) -> HtmlElement<Custom> {
/// Creates a text node.
#[inline(always)]
pub fn text(text: impl Into<Cow<'static, str>>) -> Text {
pub fn text(text: impl Into<Oco<'static, str>>) -> Text {
Text::new(text.into())
}
@@ -1190,7 +1203,7 @@ macro_rules! generate_html_tags {
impl ElementDescriptor for [<$tag:camel $($trailing_)?>] {
#[inline(always)]
fn name(&self) -> Cow<'static, str> {
fn name(&self) -> Oco<'static, str> {
stringify!($tag).into()
}

View File

@@ -9,12 +9,14 @@
#[cfg_attr(any(debug_assertions, feature = "ssr"), macro_use)]
pub extern crate tracing;
pub mod callback;
mod components;
mod events;
pub mod helpers;
pub mod html;
mod hydration;
mod logging;
/// Utilities for simple isomorphic logging to the console or terminal.
pub mod logging;
mod macro_helpers;
pub mod math;
mod node_ref;
@@ -24,6 +26,7 @@ pub mod ssr;
pub mod ssr_in_order;
pub mod svg;
mod transparent;
pub use callback::*;
use cfg_if::cfg_if;
pub use components::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -34,11 +37,11 @@ pub use events::{typed as ev, typed::EventHandler};
pub use html::HtmlElement;
use html::{AnyElement, ElementDescriptor};
pub use hydration::{HydrationCtx, HydrationKey};
use leptos_reactive::Oco;
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
pub use logging::*;
pub use macro_helpers::*;
pub use node_ref::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -240,7 +243,7 @@ cfg_if! {
pub struct Element {
#[doc(hidden)]
#[cfg(debug_assertions)]
pub name: Cow<'static, str>,
pub name: Oco<'static, str>,
#[doc(hidden)]
pub element: web_sys::HtmlElement,
#[cfg(debug_assertions)]
@@ -261,9 +264,9 @@ cfg_if! {
/// HTML element.
#[derive(Clone, PartialEq, Eq)]
pub struct Element {
name: Cow<'static, str>,
name: Oco<'static, str>,
is_void: bool,
attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
attrs: SmallVec<[(Oco<'static, str>, Oco<'static, str>); 4]>,
children: ElementChildren,
id: HydrationKey,
#[cfg(debug_assertions)]
@@ -396,13 +399,13 @@ impl Element {
struct Comment {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
node: web_sys::Node,
content: Cow<'static, str>,
content: Oco<'static, str>,
}
impl Comment {
#[inline]
fn new(
content: impl Into<Cow<'static, str>>,
content: impl Into<Oco<'static, str>>,
id: &HydrationKey,
closing: bool,
) -> Self {
@@ -410,7 +413,7 @@ impl Comment {
}
fn new_inner(
content: Cow<'static, str>,
content: Oco<'static, str>,
id: &HydrationKey,
closing: bool,
) -> Self {
@@ -466,12 +469,13 @@ pub struct Text {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
node: web_sys::Node,
/// The current contents of the text node.
pub content: Cow<'static, str>,
pub content: Oco<'static, str>,
}
impl fmt::Debug for Text {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "\"{}\"", self.content)
fmt::Debug::fmt(&self.content, f)
}
}
@@ -484,7 +488,7 @@ impl IntoView for Text {
impl Text {
/// Creates a new [`Text`].
pub fn new(content: Cow<'static, str>) -> Self {
pub fn new(content: Oco<'static, str>) -> Self {
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
node: crate::document()
@@ -849,7 +853,7 @@ where
N: IntoView,
{
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_warn(
crate::logging::console_warn(
"You have both `csr` and `ssr` or `hydrate` and `ssr` enabled as \
features, which may cause issues like <Suspense/>` failing to work \
silently.",

View File

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

View File

@@ -1,3 +1,4 @@
use leptos_reactive::Oco;
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
@@ -14,11 +15,11 @@ use wasm_bindgen::UnwrapThrowExt;
#[derive(Clone)]
pub enum Attribute {
/// A plain string value.
String(Cow<'static, str>),
String(Oco<'static, str>),
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
Fn(Rc<dyn Fn() -> Attribute>),
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
Option(Option<Cow<'static, str>>),
Option(Option<Oco<'static, str>>),
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
Bool(bool),
}
@@ -29,7 +30,7 @@ impl Attribute {
pub fn as_value_string(
&self,
attr_name: &'static str,
) -> Cow<'static, str> {
) -> Oco<'static, str> {
match self {
Attribute::String(value) => {
format!("{attr_name}=\"{value}\"").into()
@@ -46,14 +47,14 @@ impl Attribute {
.map(|value| format!("{attr_name}=\"{value}\"").into())
.unwrap_or_default(),
Attribute::Bool(include) => {
Cow::Borrowed(if *include { attr_name } else { "" })
Oco::Borrowed(if *include { attr_name } else { "" })
}
}
}
/// Converts the attribute to its HTML value at that moment, not including
/// the attribute name, so it can be rendered on the server.
pub fn as_nameless_value_string(&self) -> Option<Cow<'static, str>> {
pub fn as_nameless_value_string(&self) -> Option<Oco<'static, str>> {
match self {
Attribute::String(value) => Some(value.clone()),
Attribute::Fn(f) => {
@@ -104,6 +105,7 @@ impl std::fmt::Debug for Attribute {
pub trait IntoAttribute {
/// Converts the object into an [`Attribute`].
fn into_attribute(self) -> Attribute;
/// Helper function for dealing with `Box<dyn IntoAttribute>`.
fn into_attribute_boxed(self: Box<Self>) -> Attribute;
}
@@ -148,7 +150,7 @@ impl IntoAttribute for Option<Attribute> {
impl IntoAttribute for String {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(Cow::Owned(self))
Attribute::String(Oco::Owned(self))
}
impl_into_attr_boxed! {}
@@ -157,13 +159,22 @@ impl IntoAttribute for String {
impl IntoAttribute for &'static str {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(Cow::Borrowed(self))
Attribute::String(Oco::Borrowed(self))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Cow<'static, str> {
impl IntoAttribute for Rc<str> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(Oco::Counted(self))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Oco<'static, str> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(self)
@@ -184,7 +195,7 @@ impl IntoAttribute for bool {
impl IntoAttribute for Option<String> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::Option(self.map(Cow::Owned))
Attribute::Option(self.map(Oco::Owned))
}
impl_into_attr_boxed! {}
@@ -193,13 +204,31 @@ impl IntoAttribute for Option<String> {
impl IntoAttribute for Option<&'static str> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::Option(self.map(Cow::Borrowed))
Attribute::Option(self.map(Oco::Borrowed))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Option<Rc<str>> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::Option(self.map(Oco::Counted))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Option<Cow<'static, str>> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::Option(self.map(Oco::from))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Option<Oco<'static, str>> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::Option(self)
@@ -331,7 +360,7 @@ attr_signal_type_optional!(MaybeProp<T>);
#[inline(never)]
pub fn attribute_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
name: Oco<'static, str>,
value: Attribute,
) {
use leptos_reactive::create_render_effect;

View File

@@ -21,6 +21,9 @@ pub enum Class {
pub trait IntoClass {
/// Converts the object into a [`Class`].
fn into_class(self) -> Class;
/// Helper function for dealing with `Box<dyn IntoClass>`.
fn into_class_boxed(self: Box<Self>) -> Class;
}
impl IntoClass for bool {
@@ -28,6 +31,10 @@ impl IntoClass for bool {
fn into_class(self) -> Class {
Class::Value(self)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
impl<T> IntoClass for T
@@ -39,6 +46,10 @@ where
let modified_fn = Box::new(self);
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
impl Class {
@@ -65,14 +76,14 @@ impl Class {
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;
use leptos_reactive::Oco;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
pub fn class_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
name: Oco<'static, str>,
value: Class,
) {
use leptos_reactive::create_render_effect;
@@ -128,6 +139,10 @@ macro_rules! class_signal_type {
let modified_fn = Box::new(move || self.get());
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
};
}
@@ -141,6 +156,10 @@ macro_rules! class_signal_type_optional {
let modified_fn = Box::new(move || self.get().unwrap_or(false));
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
};
}

View File

@@ -15,7 +15,7 @@ use wasm_bindgen::UnwrapThrowExt;
pub enum Property {
/// A static JavaScript value.
Value(JsValue),
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
/// A (presumably reactive) function, which will be run inside an effect to update the property.
Fn(Box<dyn Fn() -> JsValue>),
}
@@ -25,6 +25,9 @@ pub enum Property {
pub trait IntoProperty {
/// Converts the object into a [`Property`].
fn into_property(self) -> Property;
/// Helper function for dealing with `Box<dyn IntoProperty>`.
fn into_property_boxed(self: Box<Self>) -> Property;
}
impl<T, U> IntoProperty for T
@@ -36,6 +39,10 @@ where
let modified_fn = Box::new(move || self().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
macro_rules! prop_type {
@@ -45,6 +52,10 @@ macro_rules! prop_type {
fn into_property(self) -> Property {
Property::Value(self.into())
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
impl IntoProperty for Option<$prop_type> {
@@ -52,6 +63,10 @@ macro_rules! prop_type {
fn into_property(self) -> Property {
Property::Value(self.into())
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}
@@ -67,6 +82,10 @@ macro_rules! prop_signal_type {
let modified_fn = Box::new(move || self.get().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}
@@ -83,6 +102,10 @@ macro_rules! prop_signal_type_optional {
let modified_fn = Box::new(move || self.get().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}
@@ -115,13 +138,13 @@ prop_signal_type!(MaybeSignal<T>);
prop_signal_type_optional!(MaybeProp<T>);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;
use leptos_reactive::Oco;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn property_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
name: Oco<'static, str>,
value: Property,
) {
use leptos_reactive::create_render_effect;

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