Compare commits

..

14 Commits

Author SHA1 Message Date
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
52 changed files with 1287 additions and 318 deletions

View File

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

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

@@ -70,6 +70,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 +117,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

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

@@ -53,6 +53,8 @@ echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
workspace = false
description = "report ci test runners for each example - OPTION: [all]"
script = '''
set -emu
BOLD="\e[1m"
GREEN="\e[0;32m"
ITALIC="\e[3m"
@@ -77,36 +79,54 @@ for path in $makefile_paths; do
test_runner=
test_count=$(grep -rl -E "#\[(test|rstest)\]" | wc -l)
test_count=$(grep -rl -E "#\[test\]" | wc -l)
if [ $test_count -gt 0 ]; then
test_runner="-C"
test_runner="T"
fi
while read -r line; do
case $line in
*"cucumber"*)
test_runner=$test_runner"C"
;;
*"rstest"*)
test_runner=$test_runner"R"
;;
esac
done <"./Cargo.toml"
while read -r line; do
case $line in
*"wasm-test.toml"*)
test_runner=$test_runner"-W"
test_runner=$test_runner"W"
;;
*"playwright-test.toml"*)
test_runner=$test_runner"-P"
test_runner=$test_runner"P"
;;
*"cargo-leptos-test.toml"*)
test_runner=$test_runner"-L"
test_runner=$test_runner"L"
;;
esac
done <"./Makefile.toml"
if [ ! -z "$1" ]; then
runners=$(echo ${test_runner} | grep -o . | sort | tr -d "\n")
if [ ! -z ${1+x} ]; then
# Show all examples
echo "$path ${BOLD}${test_runner}${RESET}"
elif [ ! -z $test_runner ]; then
echo "$path ${BOLD}${runners}${RESET}"
elif [ ! -z $runners ]; then
# Filter out examples that do not run tests in `ci`
echo "$path ${BOLD}${test_runner}${RESET}"
echo "$path ${BOLD}${runners}${RESET}"
fi
cd ${start_path}
done
echo
echo "${ITALIC}Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, W = WASM Test${RESET}"
echo "${ITALIC}Runners: C = Cucumber, L = Cargo Leptos, P = Playwright, R = RS Test, T = Cargo, W = WASM${RESET}"
echo
'''
# ALIASES
[tasks.tr]
alias = "test-runner-report"

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 target | xargs rm -rf
'''
[tasks.clean-node_modules]
script = '''

View File

@@ -5,7 +5,6 @@ use leptos::{ev, html::*, *};
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
let (count, set_count) = create_signal(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()
@@ -25,20 +24,13 @@ pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
set_count.update(|count| 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())
})
.child("+1"),
)
))
}
#[derive(Debug, Clone)]

View File

@@ -16,7 +16,8 @@ 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 }

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

View File

@@ -175,7 +175,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,6 +194,12 @@ 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;
extern crate self as leptos;
/// The most common type for the `children` property on components,

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

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

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

@@ -66,12 +66,13 @@ use crate::{
macro_helpers::{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 +127,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 +160,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 +179,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 +188,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 +267,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 +295,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 +309,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 +446,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 +576,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();
@@ -634,7 +635,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 +687,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 +699,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 +709,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 +798,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 +857,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 +1017,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 +1104,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 +1191,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

@@ -34,6 +34,7 @@ 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,
@@ -240,7 +241,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 +262,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 +397,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 +411,7 @@ impl Comment {
}
fn new_inner(
content: Cow<'static, str>,
content: Oco<'static, str>,
id: &HydrationKey,
closing: bool,
) -> Self {
@@ -466,12 +467,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 +486,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()

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) => {
@@ -148,7 +149,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 +158,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 +194,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 +203,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 +359,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

@@ -65,14 +65,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;

View File

@@ -115,13 +115,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;

View File

@@ -1,3 +1,4 @@
use leptos_reactive::Oco;
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
@@ -8,9 +9,9 @@ use std::{borrow::Cow, rc::Rc};
#[derive(Clone)]
pub enum Style {
/// A plain string value.
Value(Cow<'static, str>),
Value(Oco<'static, str>),
/// An optional string value, which sets the property to the value if `Some` and removes the property if `None`.
Option(Option<Cow<'static, str>>),
Option(Option<Oco<'static, str>>),
/// A (presumably reactive) function, which will be run inside an effect to update the style.
Fn(Rc<dyn Fn() -> Style>),
}
@@ -45,28 +46,70 @@ pub trait IntoStyle {
impl IntoStyle for &'static str {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(self.into())
Style::Value(Oco::Borrowed(self))
}
}
impl IntoStyle for String {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(Oco::Owned(self))
}
}
impl IntoStyle for Rc<str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(Oco::Counted(self))
}
}
impl IntoStyle for Cow<'static, str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(self.into())
}
}
impl IntoStyle for Oco<'static, str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(self)
}
}
impl IntoStyle for Option<&'static str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Cow::Borrowed))
Style::Option(self.map(Oco::Borrowed))
}
}
impl IntoStyle for Option<String> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Cow::Owned))
Style::Option(self.map(Oco::Owned))
}
}
impl IntoStyle for Option<Rc<str>> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Oco::Counted))
}
}
impl IntoStyle for Option<Cow<'static, str>> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Oco::from))
}
}
impl IntoStyle for Option<Oco<'static, str>> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self)
}
}
@@ -87,7 +130,7 @@ impl Style {
pub fn as_value_string(
&self,
style_name: &'static str,
) -> Option<Cow<'static, str>> {
) -> Option<Oco<'static, str>> {
match self {
Style::Value(value) => {
Some(format!("{style_name}: {value};").into())
@@ -111,10 +154,11 @@ impl Style {
#[inline(never)]
pub fn style_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
name: Oco<'static, str>,
value: Style,
) {
use leptos_reactive::create_render_effect;
use std::ops::Deref;
use wasm_bindgen::JsCast;
let el = el.unchecked_ref::<web_sys::HtmlElement>();
@@ -132,16 +176,16 @@ pub fn style_helper(
_ => unreachable!(),
};
if old.as_ref() != Some(&new) {
style_expression(&style_list, &name, new.as_ref(), true)
style_expression(&style_list, &name, new.as_deref(), true)
}
new
});
}
Style::Value(value) => {
style_expression(&style_list, &name, Some(&value), false)
style_expression(&style_list, &name, Some(value.deref()), false)
}
Style::Option(value) => {
style_expression(&style_list, &name, value.as_ref(), false)
style_expression(&style_list, &name, value.as_deref(), false)
}
};
}
@@ -151,7 +195,7 @@ pub fn style_helper(
pub(crate) fn style_expression(
style_list: &web_sys::CssStyleDeclaration,
style_name: &str,
value: Option<&Cow<'static, str>>,
value: Option<&str>,
force: bool,
) {
use crate::HydrationCtx;
@@ -160,7 +204,7 @@ pub(crate) fn style_expression(
let style_name = wasm_bindgen::intern(style_name);
if let Some(value) = value {
if let Err(e) = style_list.set_property(style_name, &value) {
if let Err(e) = style_list.set_property(style_name, value) {
crate::error!("[HtmlElement::style()] {e:?}");
}
} else {

View File

@@ -5,7 +5,7 @@ use wasm_bindgen::UnwrapThrowExt;
macro_rules! tracing_props {
() => {
::leptos::leptos_dom::tracing::span!(
::leptos::leptos_dom::tracing::Level::DEBUG,
::leptos::leptos_dom::tracing::Level::TRACE,
"leptos_dom::tracing_props",
props = String::from("[]")
);
@@ -24,7 +24,7 @@ macro_rules! tracing_props {
props.pop();
props.push(']');
::leptos::leptos_dom::tracing::span!(
::leptos::leptos_dom::tracing::Level::DEBUG,
::leptos::leptos_dom::tracing::Level::TRACE,
"leptos_dom::tracing_props",
props
);
@@ -48,10 +48,21 @@ impl<T: serde::Serialize> SerializeMatch for &Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
serde_json::to_string(self.value.get().unwrap_throw()).map_or_else(
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
)
// suppresses warnings when serializing signals into props
#[cfg(debug_assertions)]
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
let value = serde_json::to_string(self.value.get().unwrap_throw())
.map_or_else(
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
);
#[cfg(debug_assertions)]
leptos_reactive::SpecialNonReactiveZone::exit(prev);
value
}
}
@@ -63,9 +74,7 @@ impl<T> DefaultMatch for Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
format!(
r#"{{"name": "{name}", "error": "The trait `serde::Serialize` is not implemented"}}"#
)
format!(r#"{{"name": "{name}", "value": "[unserializable value]"}}"#)
}
}
@@ -147,6 +156,8 @@ fn match_serialize() {
#[test]
fn match_no_serialize() {
#![allow(clippy::needless_borrow)]
struct CustomStruct {
field: &'static str,
}
@@ -159,7 +170,7 @@ fn match_no_serialize() {
.spez();
assert_eq!(
prop,
r#"{"name": "test", "error": "The trait `serde::Serialize` is not implemented"}"#
r#"{"name": "test", "value": "[unserializable value]"}"#
);
// Verification of ownership
assert_eq!(test.field, "field");

View File

@@ -3,7 +3,7 @@
use super::{ElementDescriptor, HtmlElement};
use crate::HydrationCtx;
use cfg_if::cfg_if;
use std::borrow::Cow;
use leptos_reactive::Oco;
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use once_cell::unsync::Lazy as LazyCell;
@@ -145,7 +145,7 @@ macro_rules! generate_math_tags {
}
impl ElementDescriptor for [<$tag:camel $($second:camel $($third:camel)?)?>] {
fn name(&self) -> Cow<'static, str> {
fn name(&self) -> Oco<'static, str> {
stringify!($tag).into()
}

View File

@@ -9,8 +9,8 @@ use crate::{
use cfg_if::cfg_if;
use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
use itertools::Itertools;
use leptos_reactive::*;
use std::{borrow::Cow, pin::Pin};
use leptos_reactive::{Oco, *};
use std::pin::Pin;
type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
@@ -30,7 +30,7 @@ type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
any(debug_assertions, feature = "ssr"),
instrument(level = "info", skip_all,)
)]
pub fn render_to_string<F, N>(f: F) -> String
pub fn render_to_string<F, N>(f: F) -> Oco<'static, str>
where
F: FnOnce() -> N + 'static,
N: IntoView,
@@ -42,7 +42,7 @@ where
runtime.dispose();
html.into()
html
}
/// Renders a function to a stream of HTML strings.
@@ -87,7 +87,7 @@ pub fn render_to_stream(
)]
pub fn render_to_stream_with_prefix(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
) -> impl Stream<Item = String> {
let (stream, runtime) =
render_to_stream_with_prefix_undisposed(view, prefix);
@@ -116,7 +116,7 @@ pub fn render_to_stream_with_prefix(
)]
pub fn render_to_stream_with_prefix_undisposed(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
) -> (impl Stream<Item = String>, RuntimeId) {
render_to_stream_with_prefix_undisposed_with_context(view, prefix, || {})
}
@@ -142,7 +142,7 @@ pub fn render_to_stream_with_prefix_undisposed(
)]
pub fn render_to_stream_with_prefix_undisposed_with_context(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
additional_context: impl FnOnce() + 'static,
) -> (impl Stream<Item = String>, RuntimeId) {
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
@@ -179,7 +179,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
)]
pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
additional_context: impl FnOnce() + 'static,
replace_blocks: bool,
) -> (impl Stream<Item = String>, RuntimeId) {
@@ -363,7 +363,7 @@ impl View {
any(debug_assertions, feature = "ssr"),
instrument(level = "info", skip_all,)
)]
pub fn render_to_string(self) -> Cow<'static, str> {
pub fn render_to_string(self) -> Oco<'static, str> {
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_error(
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
@@ -381,7 +381,7 @@ impl View {
pub(crate) fn render_to_string_helper(
self,
dont_escape_text: bool,
) -> Cow<'static, str> {
) -> Oco<'static, str> {
match self {
View::Text(node) => {
if dont_escape_text {
@@ -450,7 +450,7 @@ impl View {
)
.into()
})
as Box<dyn FnOnce() -> Cow<'static, str>>,
as Box<dyn FnOnce() -> Oco<'static, str>>,
),
CoreComponent::DynChild(node) => {
let child = node.child.take();
@@ -500,7 +500,7 @@ impl View {
"".into()
}
})
as Box<dyn FnOnce() -> Cow<'static, str>>,
as Box<dyn FnOnce() -> Oco<'static, str>>,
)
}
CoreComponent::Each(node) => {
@@ -554,7 +554,7 @@ impl View {
.join("")
.into()
})
as Box<dyn FnOnce() -> Cow<'static, str>>,
as Box<dyn FnOnce() -> Oco<'static, str>>,
)
}
};
@@ -598,15 +598,15 @@ impl View {
.join("")
.into()
} else {
let tag_name = el.name;
let tag_name: Oco<'_, str> = el.name;
let mut inner_html = None;
let mut inner_html: Option<Oco<'_, str>> = None;
let attrs = el
.attrs
.into_iter()
.filter_map(
|(name, value)| -> Option<Cow<'static, str>> {
|(name, value)| -> Option<Oco<'static, str>> {
if value.is_empty() {
Some(format!(" {name}").into())
} else if name == "inner_html" {
@@ -615,9 +615,9 @@ impl View {
} else {
Some(
format!(
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
.into(),
)
}
@@ -729,9 +729,9 @@ pub(crate) fn render_serializers(
}
#[doc(hidden)]
pub fn escape_attr<T>(value: &T) -> Cow<'_, str>
pub fn escape_attr<T>(value: &T) -> Oco<'_, str>
where
T: AsRef<str>,
{
html_escape::encode_double_quoted_attribute(value)
html_escape::encode_double_quoted_attribute(value).into()
}

View File

@@ -12,7 +12,7 @@ use cfg_if::cfg_if;
use futures::{channel::mpsc::UnboundedSender, Stream, StreamExt};
use itertools::Itertools;
use leptos_reactive::{
create_runtime, suspense::StreamChunk, RuntimeId, SharedContext,
create_runtime, suspense::StreamChunk, Oco, RuntimeId, SharedContext,
};
use std::{borrow::Cow, collections::VecDeque};
@@ -59,7 +59,7 @@ pub fn render_to_stream_in_order(
#[tracing::instrument(level = "trace", skip_all)]
pub fn render_to_stream_in_order_with_prefix(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
) -> impl Stream<Item = String> {
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_error(
@@ -89,7 +89,7 @@ pub fn render_to_stream_in_order_with_prefix(
#[tracing::instrument(level = "trace", skip_all)]
pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
additional_context: impl FnOnce() + 'static,
) -> (impl Stream<Item = String>, RuntimeId) {
HydrationCtx::reset_id();
@@ -287,12 +287,11 @@ impl View {
StringOrView::String(string) => {
chunks.push_back(StreamChunk::Sync(string))
}
StringOrView::View(view) => {
view().into_stream_chunks_helper(
StringOrView::View(view) => view()
.into_stream_chunks_helper(
chunks,
is_script_or_style,
);
}
),
}
}
} else {
@@ -313,9 +312,9 @@ impl View {
} else {
Some(
format!(
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
.into(),
)
}
@@ -350,7 +349,7 @@ impl View {
}
}
ElementChildren::InnerHtml(inner_html) => {
chunks.push_back(StreamChunk::Sync(inner_html));
chunks.push_back(StreamChunk::Sync(inner_html))
}
// handled above
ElementChildren::Chunks(_) => unreachable!(),

View File

@@ -4,9 +4,9 @@
use super::{html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG, HydrationKey};
use super::{ElementDescriptor, HtmlElement};
use crate::HydrationCtx;
use leptos_reactive::Oco;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use once_cell::unsync::Lazy as LazyCell;
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::JsCast;
@@ -142,7 +142,7 @@ macro_rules! generate_svg_tags {
}
impl ElementDescriptor for [<$tag:camel $($second:camel $($third:camel)?)?>] {
fn name(&self) -> Cow<'static, str> {
fn name(&self) -> Oco<'static, str> {
stringify!($tag).into()
}

View File

@@ -30,7 +30,7 @@ tracing = "0.1.37"
[dev-dependencies]
log = "0.4"
typed-builder = "0.14"
typed-builder = "0.16"
trybuild = "1"
leptos = { path = "../leptos" }
insta = "1.29"

View File

@@ -252,8 +252,9 @@ impl ToTokens for Model {
#[doc = ""]
#docs
#component_fn_prop_docs
#[derive(::leptos::typed_builder::TypedBuilder)]
#[builder(doc)]
#[derive(::leptos::typed_builder_macro::TypedBuilder)]
//#[builder(doc)]
#[builder(crate_module_path=::leptos::typed_builder)]
#vis struct #props_name #impl_generics #where_clause {
#prop_builder_fields
}
@@ -554,7 +555,11 @@ impl ToTokens for TypedBuilderOpts {
quote! {}
};
let output = quote! { #[builder(#default #setter)] };
let output = if !default.is_empty() || !setter.is_empty() {
quote! { #[builder(#default #setter)] }
} else {
quote! {}
};
tokens.append_all(output);
}

View File

@@ -818,6 +818,10 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// - Your server must be ready to handle the server functions at the API prefix you list. The easiest way to do this
/// is to use the `handle_server_fns` function from [`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/fn.handle_server_fns.html)
/// or [`leptos_axum`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.handle_server_fns.html).
/// - **Server functions must have unique paths**. Unique paths are automatically generated for each
/// server function. If you choose to specify a path in the fourth argument, you must ensure that these
/// are unique. You cannot define two server functions with the same URL prefix and endpoint path,
/// even if they have different URL encodings, e.g. a POST method at `/api/foo` and a GET method at `/api/foo`.
///
/// ## Server Function Encodings
///

View File

@@ -19,7 +19,7 @@ pub fn impl_params(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let span = field.span();
quote_spanned! {
span => #ident: <#ty>::into_param(map.get(#field_name_string).map(|n| n.as_str()), #field_name_string)?
span => #ident: <#ty>::into_param(map.get(#field_name_string).map(::std::string::String::as_str), #field_name_string)?
}
})
.collect()

View File

@@ -79,8 +79,8 @@ impl ToTokens for Model {
#[doc = ""]
#docs
#prop_docs
#[derive(::leptos::typed_builder::TypedBuilder)]
#[builder(doc)]
#[derive(::leptos::typed_builder_macro::TypedBuilder)]
#[builder(doc, crate_module_path=::leptos::typed_builder)]
#vis struct #name #generics #where_clause {
#prop_builder_fields
}
@@ -191,7 +191,11 @@ impl ToTokens for TypedBuilderOpts {
quote! {}
};
let output = quote! { #[builder(#default #setter)] };
let output = if !default.is_empty() || !setter.is_empty() {
quote! { #[builder(#default #setter)] }
} else {
quote! {}
};
tokens.append_all(output);
}

View File

@@ -85,6 +85,7 @@ mod effect;
mod hydration;
mod memo;
mod node;
pub mod oco;
mod resource;
mod runtime;
mod selector;
@@ -107,6 +108,7 @@ pub use effect::*;
pub use hydration::{FragmentData, SharedContext};
pub use memo::*;
pub use node::Disposer;
pub use oco::*;
pub use resource::*;
use runtime::*;
pub use runtime::{

681
leptos_reactive/src/oco.rs Normal file
View File

@@ -0,0 +1,681 @@
//! This module contains the `Oco` (Owned Clones Once) smart pointer,
//! which is used to store immutable references to values.
//! This is useful for storing, for example, strings.
use std::{
borrow::{Borrow, Cow},
ffi::{CStr, OsStr},
fmt,
hash::Hash,
ops::{Add, Deref},
path::Path,
rc::Rc,
};
/// "Owned Clones Once" - a smart pointer that can be either a reference,
/// an owned value, or a reference counted pointer. This is useful for
/// storing immutable values, such as strings, in a way that is cheap to
/// clone and pass around.
///
/// The `Clone` implementation is amortized `O(1)`. Cloning the [`Oco::Borrowed`]
/// variant simply copies the references (`O(1)`). Cloning the [`Oco::Counted`]
/// variant increments a reference count (`O(1)`). Cloning the [`Oco::Owned`]
/// variant upgrades it to [`Oco::Counted`], which requires an `O(n)` clone of the
/// data, but all subsequent clones will be `O(1)`.
pub enum Oco<'a, T: ?Sized + ToOwned + 'a> {
/// A static reference to a value.
Borrowed(&'a T),
/// A reference counted pointer to a value.
Counted(Rc<T>),
/// An owned value.
Owned(<T as ToOwned>::Owned),
}
impl<T: ?Sized + ToOwned> Oco<'_, T> {
/// Converts the value into an owned value.
pub fn into_owned(self) -> <T as ToOwned>::Owned {
match self {
Oco::Borrowed(v) => v.to_owned(),
Oco::Counted(v) => v.as_ref().to_owned(),
Oco::Owned(v) => v,
}
}
/// Checks if the value is [`Oco::Borrowed`].
/// # Examples
/// ```
/// # use std::rc::Rc;
/// # use leptos_reactive::oco::Oco;
/// assert!(Oco::<str>::Borrowed("Hello").is_borrowed());
/// assert!(!Oco::<str>::Counted(Rc::from("Hello")).is_borrowed());
/// assert!(!Oco::<str>::Owned("Hello".to_string()).is_borrowed());
/// ```
pub const fn is_borrowed(&self) -> bool {
matches!(self, Oco::Borrowed(_))
}
/// Checks if the value is [`Oco::Counted`].
/// # Examples
/// ```
/// # use std::rc::Rc;
/// # use leptos_reactive::oco::Oco;
/// assert!(Oco::<str>::Counted(Rc::from("Hello")).is_counted());
/// assert!(!Oco::<str>::Borrowed("Hello").is_counted());
/// assert!(!Oco::<str>::Owned("Hello".to_string()).is_counted());
/// ```
pub const fn is_counted(&self) -> bool {
matches!(self, Oco::Counted(_))
}
/// Checks if the value is [`Oco::Owned`].
/// # Examples
/// ```
/// # use std::rc::Rc;
/// # use leptos_reactive::oco::Oco;
/// assert!(Oco::<str>::Owned("Hello".to_string()).is_owned());
/// assert!(!Oco::<str>::Borrowed("Hello").is_owned());
/// assert!(!Oco::<str>::Counted(Rc::from("Hello")).is_owned());
/// ```
pub const fn is_owned(&self) -> bool {
matches!(self, Oco::Owned(_))
}
}
impl<T: ?Sized + ToOwned> Deref for Oco<'_, T> {
type Target = T;
fn deref(&self) -> &T {
match self {
Oco::Borrowed(v) => v,
Oco::Owned(v) => v.borrow(),
Oco::Counted(v) => v,
}
}
}
impl<T: ?Sized + ToOwned> Borrow<T> for Oco<'_, T> {
#[inline(always)]
fn borrow(&self) -> &T {
self.deref()
}
}
impl<T: ?Sized + ToOwned> AsRef<T> for Oco<'_, T> {
#[inline(always)]
fn as_ref(&self) -> &T {
self.deref()
}
}
impl AsRef<Path> for Oco<'_, str> {
#[inline(always)]
fn as_ref(&self) -> &Path {
self.as_str().as_ref()
}
}
impl AsRef<Path> for Oco<'_, OsStr> {
#[inline(always)]
fn as_ref(&self) -> &Path {
self.as_os_str().as_ref()
}
}
// --------------------------------------
// pub fn as_{slice}(&self) -> &{slice}
// --------------------------------------
impl Oco<'_, str> {
/// Returns a `&str` slice of this [`Oco`].
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// let oco = Oco::<str>::Borrowed("Hello");
/// let s: &str = oco.as_str();
/// assert_eq!(s, "Hello");
/// ```
#[inline(always)]
pub fn as_str(&self) -> &str {
self
}
}
impl Oco<'_, CStr> {
/// Returns a `&CStr` slice of this [`Oco`].
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// use std::ffi::CStr;
///
/// let oco =
/// Oco::<CStr>::Borrowed(CStr::from_bytes_with_nul(b"Hello\0").unwrap());
/// let s: &CStr = oco.as_c_str();
/// assert_eq!(s, CStr::from_bytes_with_nul(b"Hello\0").unwrap());
/// ```
#[inline(always)]
pub fn as_c_str(&self) -> &CStr {
self
}
}
impl Oco<'_, OsStr> {
/// Returns a `&OsStr` slice of this [`Oco`].
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// use std::ffi::OsStr;
///
/// let oco = Oco::<OsStr>::Borrowed(OsStr::new("Hello"));
/// let s: &OsStr = oco.as_os_str();
/// assert_eq!(s, OsStr::new("Hello"));
/// ```
#[inline(always)]
pub fn as_os_str(&self) -> &OsStr {
self
}
}
impl Oco<'_, Path> {
/// Returns a `&Path` slice of this [`Oco`].
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// use std::path::Path;
///
/// let oco = Oco::<Path>::Borrowed(Path::new("Hello"));
/// let s: &Path = oco.as_path();
/// assert_eq!(s, Path::new("Hello"));
/// ```
#[inline(always)]
pub fn as_path(&self) -> &Path {
self
}
}
impl<T> Oco<'_, [T]>
where
[T]: ToOwned,
{
/// Returns a `&[T]` slice of this [`Oco`].
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// let oco = Oco::<[u8]>::Borrowed(b"Hello");
/// let s: &[u8] = oco.as_slice();
/// assert_eq!(s, b"Hello");
/// ```
#[inline(always)]
pub fn as_slice(&self) -> &[T] {
self
}
}
// ------------------------------------------------------------------------------------------------------
// Cloning (has to be implemented manually because of the `Rc<T>: From<&<T as ToOwned>::Owned>` bound)
// ------------------------------------------------------------------------------------------------------
impl Clone for Oco<'_, str> {
/// Returns a new [`Oco`] with the same value as this one.
/// If the value is [`Oco::Owned`], this will convert it into
/// [`Oco::Counted`], so that the next clone will be O(1).
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// let oco = Oco::<str>::Owned("Hello".to_string());
/// let oco2 = oco.clone();
/// assert_eq!(oco, oco2);
/// assert!(oco2.is_counted());
/// ```
fn clone(&self) -> Self {
match self {
Oco::Borrowed(v) => Oco::Borrowed(v),
Oco::Counted(v) => Oco::Counted(v.clone()),
Oco::Owned(v) => Oco::Counted(Rc::<str>::from(v.as_str())),
}
}
}
impl Clone for Oco<'_, CStr> {
/// Returns a new [`Oco`] with the same value as this one.
/// If the value is [`Oco::Owned`], this will convert it into
/// [`Oco::Counted`], so that the next clone will be O(1).
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// use std::ffi::CStr;
///
/// let oco = Oco::<CStr>::Owned(
/// CStr::from_bytes_with_nul(b"Hello\0").unwrap().to_owned(),
/// );
/// let oco2 = oco.clone();
/// assert_eq!(oco, oco2);
/// assert!(oco2.is_counted());
/// ```
fn clone(&self) -> Self {
match self {
Oco::Borrowed(v) => Oco::Borrowed(v),
Oco::Counted(v) => Oco::Counted(v.clone()),
Oco::Owned(v) => Oco::Counted(Rc::<CStr>::from(v.as_c_str())),
}
}
}
impl Clone for Oco<'_, OsStr> {
/// Returns a new [`Oco`] with the same value as this one.
/// If the value is [`Oco::Owned`], this will convert it into
/// [`Oco::Counted`], so that the next clone will be O(1).
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// use std::ffi::OsStr;
///
/// let oco = Oco::<OsStr>::Owned(OsStr::new("Hello").to_owned());
/// let oco2 = oco.clone();
/// assert_eq!(oco, oco2);
/// assert!(oco2.is_counted());
/// ```
fn clone(&self) -> Self {
match self {
Oco::Borrowed(v) => Oco::Borrowed(v),
Oco::Counted(v) => Oco::Counted(v.clone()),
Oco::Owned(v) => Oco::Counted(Rc::<OsStr>::from(v.as_os_str())),
}
}
}
impl Clone for Oco<'_, Path> {
/// Returns a new [`Oco`] with the same value as this one.
/// If the value is [`Oco::Owned`], this will convert it into
/// [`Oco::Counted`], so that the next clone will be O(1).
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// use std::path::Path;
///
/// let oco = Oco::<Path>::Owned(Path::new("Hello").to_owned());
/// let oco2 = oco.clone();
/// assert_eq!(oco, oco2);
/// assert!(oco2.is_counted());
/// ```
fn clone(&self) -> Self {
match self {
Oco::Borrowed(v) => Oco::Borrowed(v),
Oco::Counted(v) => Oco::Counted(v.clone()),
Oco::Owned(v) => Oco::Counted(Rc::<Path>::from(v.as_path())),
}
}
}
impl<T: Clone> Clone for Oco<'_, [T]>
where
[T]: ToOwned<Owned = Vec<T>>,
{
/// Returns a new [`Oco`] with the same value as this one.
/// If the value is [`Oco::Owned`], this will convert it into
/// [`Oco::Counted`], so that the next clone will be O(1).
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// let oco = Oco::<[i32]>::Owned(vec![1, 2, 3]);
/// let oco2 = oco.clone();
/// assert_eq!(oco, oco2);
/// assert!(oco2.is_counted());
/// ```
fn clone(&self) -> Self {
match self {
Oco::Borrowed(v) => Oco::Borrowed(v),
Oco::Counted(v) => Oco::Counted(v.clone()),
Oco::Owned(v) => Oco::Counted(Rc::<[T]>::from(v.as_slice())),
}
}
}
impl<T: ?Sized> Default for Oco<'_, T>
where
T: ToOwned,
T::Owned: Default,
{
fn default() -> Self {
Oco::Owned(T::Owned::default())
}
}
impl<'a, 'b, A: ?Sized, B: ?Sized> PartialEq<Oco<'b, B>> for Oco<'a, A>
where
A: PartialEq<B>,
A: ToOwned,
B: ToOwned,
{
fn eq(&self, other: &Oco<'b, B>) -> bool {
**self == **other
}
}
impl<T: ?Sized + ToOwned + Eq> Eq for Oco<'_, T> {}
impl<'a, 'b, A: ?Sized, B: ?Sized> PartialOrd<Oco<'b, B>> for Oco<'a, A>
where
A: PartialOrd<B>,
A: ToOwned,
B: ToOwned,
{
fn partial_cmp(&self, other: &Oco<'b, B>) -> Option<std::cmp::Ordering> {
(**self).partial_cmp(&**other)
}
}
impl<T: ?Sized + Ord> Ord for Oco<'_, T>
where
T: ToOwned,
{
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(**self).cmp(&**other)
}
}
impl<T: ?Sized + Hash> Hash for Oco<'_, T>
where
T: ToOwned,
{
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(**self).hash(state)
}
}
impl<T: ?Sized + fmt::Debug> fmt::Debug for Oco<'_, T>
where
T: ToOwned,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(**self).fmt(f)
}
}
impl<T: ?Sized + fmt::Display> fmt::Display for Oco<'_, T>
where
T: ToOwned,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(**self).fmt(f)
}
}
impl<'a, T: ?Sized> From<&'a T> for Oco<'a, T>
where
T: ToOwned,
{
fn from(v: &'a T) -> Self {
Oco::Borrowed(v)
}
}
impl<'a, T: ?Sized> From<Cow<'a, T>> for Oco<'a, T>
where
T: ToOwned,
{
fn from(v: Cow<'a, T>) -> Self {
match v {
Cow::Borrowed(v) => Oco::Borrowed(v),
Cow::Owned(v) => Oco::Owned(v),
}
}
}
impl<'a, T: ?Sized> From<Oco<'a, T>> for Cow<'a, T>
where
T: ToOwned,
{
fn from(value: Oco<'a, T>) -> Self {
match value {
Oco::Borrowed(v) => Cow::Borrowed(v),
Oco::Owned(v) => Cow::Owned(v),
Oco::Counted(v) => Cow::Owned(v.as_ref().to_owned()),
}
}
}
impl<T: ?Sized> From<Rc<T>> for Oco<'_, T>
where
T: ToOwned,
{
fn from(v: Rc<T>) -> Self {
Oco::Counted(v)
}
}
impl<T: ?Sized> From<Box<T>> for Oco<'_, T>
where
T: ToOwned,
{
fn from(v: Box<T>) -> Self {
Oco::Counted(v.into())
}
}
impl From<String> for Oco<'_, str> {
fn from(v: String) -> Self {
Oco::Owned(v)
}
}
impl From<Oco<'_, str>> for String {
fn from(v: Oco<'_, str>) -> Self {
match v {
Oco::Borrowed(v) => v.to_owned(),
Oco::Counted(v) => v.as_ref().to_owned(),
Oco::Owned(v) => v,
}
}
}
impl<T> From<Vec<T>> for Oco<'_, [T]>
where
[T]: ToOwned<Owned = Vec<T>>,
{
fn from(v: Vec<T>) -> Self {
Oco::Owned(v)
}
}
impl<'a, T, const N: usize> From<&'a [T; N]> for Oco<'a, [T]>
where
[T]: ToOwned,
{
fn from(v: &'a [T; N]) -> Self {
Oco::Borrowed(v)
}
}
impl<'a> From<Oco<'a, str>> for Oco<'a, [u8]> {
fn from(v: Oco<'a, str>) -> Self {
match v {
Oco::Borrowed(v) => Oco::Borrowed(v.as_bytes()),
Oco::Owned(v) => Oco::Owned(v.into_bytes()),
Oco::Counted(v) => Oco::Counted(v.into()),
}
}
}
/// Error returned from [`Oco::try_from`] for unsuccessful
/// conversion from `Oco<'_, [u8]>` to `Oco<'_, str>`.
#[derive(Debug, Clone, thiserror::Error)]
#[error("invalid utf-8 sequence: {_0}")]
pub enum FromUtf8Error {
/// Error for conversion of [`Oco::Borrowed`] and [`Oco::Counted`] variants
/// (`&[u8]` to `&str`).
#[error("{_0}")]
StrFromBytes(
#[source]
#[from]
std::str::Utf8Error,
),
/// Error for conversion of [`Oco::Owned`] variant (`Vec<u8>` to `String`).
#[error("{_0}")]
StringFromBytes(
#[source]
#[from]
std::string::FromUtf8Error,
),
}
macro_rules! impl_slice_eq {
([$($g:tt)*] $((where $($w:tt)+))?, $lhs:ty, $rhs: ty) => {
impl<$($g)*> PartialEq<$rhs> for $lhs
$(where
$($w)*)?
{
#[inline]
fn eq(&self, other: &$rhs) -> bool {
PartialEq::eq(&self[..], &other[..])
}
}
impl<$($g)*> PartialEq<$lhs> for $rhs
$(where
$($w)*)?
{
#[inline]
fn eq(&self, other: &$lhs) -> bool {
PartialEq::eq(&self[..], &other[..])
}
}
};
}
impl_slice_eq!([], Oco<'_, str>, str);
impl_slice_eq!(['a, 'b], Oco<'a, str>, &'b str);
impl_slice_eq!([], Oco<'_, str>, String);
impl_slice_eq!(['a, 'b], Oco<'a, str>, Cow<'b, str>);
impl_slice_eq!([T: PartialEq] (where [T]: ToOwned), Oco<'_, [T]>, [T]);
impl_slice_eq!(['a, 'b, T: PartialEq] (where [T]: ToOwned), Oco<'a, [T]>, &'b [T]);
impl_slice_eq!([T: PartialEq] (where [T]: ToOwned), Oco<'_, [T]>, Vec<T>);
impl_slice_eq!(['a, 'b, T: PartialEq] (where [T]: ToOwned), Oco<'a, [T]>, Cow<'b, [T]>);
impl<'a, 'b> Add<&'b str> for Oco<'a, str> {
type Output = Oco<'static, str>;
fn add(self, rhs: &'b str) -> Self::Output {
Oco::Owned(String::from(self) + rhs)
}
}
impl<'a, 'b> Add<Cow<'b, str>> for Oco<'a, str> {
type Output = Oco<'static, str>;
fn add(self, rhs: Cow<'b, str>) -> Self::Output {
Oco::Owned(String::from(self) + rhs.as_ref())
}
}
impl<'a, 'b> Add<Oco<'b, str>> for Oco<'a, str> {
type Output = Oco<'static, str>;
fn add(self, rhs: Oco<'b, str>) -> Self::Output {
Oco::Owned(String::from(self) + rhs.as_ref())
}
}
impl<'a> FromIterator<Oco<'a, str>> for String {
fn from_iter<T: IntoIterator<Item = Oco<'a, str>>>(iter: T) -> Self {
iter.into_iter().fold(String::new(), |mut acc, item| {
acc.push_str(item.as_ref());
acc
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_fmt_should_display_quotes_for_strings() {
let s: Oco<str> = Oco::Borrowed("hello");
assert_eq!(format!("{:?}", s), "\"hello\"");
let s: Oco<str> = Oco::Counted(Rc::from("hello"));
assert_eq!(format!("{:?}", s), "\"hello\"");
}
#[test]
fn partial_eq_should_compare_str_to_str() {
let s: Oco<str> = Oco::Borrowed("hello");
assert_eq!(s, "hello");
assert_eq!("hello", s);
assert_eq!(s, String::from("hello"));
assert_eq!(String::from("hello"), s);
assert_eq!(s, Cow::from("hello"));
assert_eq!(Cow::from("hello"), s);
}
#[test]
fn partial_eq_should_compare_slice_to_slice() {
let s: Oco<[i32]> = Oco::Borrowed([1, 2, 3].as_slice());
assert_eq!(s, [1, 2, 3].as_slice());
assert_eq!([1, 2, 3].as_slice(), s);
assert_eq!(s, vec![1, 2, 3]);
assert_eq!(vec![1, 2, 3], s);
assert_eq!(s, Cow::<'_, [i32]>::Borrowed(&[1, 2, 3]));
assert_eq!(Cow::<'_, [i32]>::Borrowed(&[1, 2, 3]), s);
}
#[test]
fn add_should_concatenate_strings() {
let s: Oco<str> = Oco::Borrowed("hello");
assert_eq!(s.clone() + " world", "hello world");
assert_eq!(s.clone() + Cow::from(" world"), "hello world");
assert_eq!(s + Oco::from(" world"), "hello world");
}
#[test]
fn as_str_should_return_a_str() {
let s: Oco<str> = Oco::Borrowed("hello");
assert_eq!(s.as_str(), "hello");
let s: Oco<str> = Oco::Counted(Rc::from("hello"));
assert_eq!(s.as_str(), "hello");
}
#[test]
fn as_slice_should_return_a_slice() {
let s: Oco<[i32]> = Oco::Borrowed([1, 2, 3].as_slice());
assert_eq!(s.as_slice(), [1, 2, 3].as_slice());
let s: Oco<[i32]> = Oco::Counted(Rc::from([1, 2, 3]));
assert_eq!(s.as_slice(), [1, 2, 3].as_slice());
}
#[test]
fn default_for_str_should_return_an_empty_string() {
let s: Oco<str> = Default::default();
assert!(s.is_empty());
}
#[test]
fn default_for_slice_should_return_an_empty_slice() {
let s: Oco<[i32]> = Default::default();
assert!(s.is_empty());
}
#[test]
fn default_for_any_option_should_return_none() {
let s: Oco<Option<i32>> = Default::default();
assert!(s.is_none());
}
#[test]
fn cloned_owned_string_should_become_counted_str() {
let s: Oco<str> = Oco::Owned(String::from("hello"));
assert!(s.clone().is_counted());
}
#[test]
fn cloned_borrowed_str_should_remain_borrowed_str() {
let s: Oco<str> = Oco::Borrowed("hello");
assert!(s.clone().is_borrowed());
}
#[test]
fn cloned_counted_str_should_remain_counted_str() {
let s: Oco<str> = Oco::Counted(Rc::from("hello"));
assert!(s.clone().is_counted());
}
}

View File

@@ -1,14 +1,12 @@
//! Types that handle asynchronous data loading via `<Suspense/>`.
use crate::{
create_isomorphic_effect, create_rw_signal, create_signal, queue_microtask,
signal::SignalGet, store_value, ReadSignal, RwSignal, SignalSet,
SignalUpdate, StoredValue, WriteSignal,
create_isomorphic_effect, create_rw_signal, create_signal, oco::Oco,
queue_microtask, signal::SignalGet, store_value, ReadSignal, RwSignal,
SignalSet, SignalUpdate, StoredValue, WriteSignal,
};
use futures::Future;
use std::{
borrow::Cow, cell::RefCell, collections::VecDeque, pin::Pin, rc::Rc,
};
use std::{cell::RefCell, collections::VecDeque, pin::Pin, rc::Rc};
/// Tracks [`Resource`](crate::Resource)s that are read under a suspense context,
/// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component.
@@ -172,7 +170,7 @@ impl Default for SuspenseContext {
/// Represents a chunk in a stream of HTML.
pub enum StreamChunk {
/// A chunk of synchronous HTML.
Sync(Cow<'static, str>),
Sync(Oco<'static, str>),
/// A future that resolves to be a list of additional chunks.
Async {
/// The HTML chunks this contains.

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.5.0"
version = "0.5.0-beta2"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -51,7 +51,6 @@ use leptos::{
*,
};
use std::{
borrow::Cow,
cell::{Cell, RefCell},
fmt::Debug,
rc::Rc,
@@ -100,7 +99,7 @@ pub struct MetaTagsContext {
els: Rc<
RefCell<
IndexMap<
Cow<'static, str>,
Oco<'static, str>,
(HtmlElement<AnyElement>, Option<web_sys::Element>),
>,
>,
@@ -130,7 +129,7 @@ impl MetaTagsContext {
pub fn register(
&self,
id: Cow<'static, str>,
id: Oco<'static, str>,
builder_el: HtmlElement<AnyElement>,
) {
cfg_if! {

View File

@@ -1,6 +1,5 @@
use crate::use_head;
use leptos::{nonce::use_nonce, *};
use std::borrow::Cow;
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
/// head, accepting any of the valid attributes for that tag.
@@ -28,62 +27,62 @@ use std::borrow::Cow;
pub fn Link(
/// The [`id`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-id) attribute.
#[prop(optional, into)]
id: Option<Cow<'static, str>>,
id: Option<Oco<'static, str>>,
/// The [`as`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-as) attribute.
#[prop(optional, into)]
as_: Option<Cow<'static, str>>,
as_: Option<Oco<'static, str>>,
/// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-crossorigin) attribute.
#[prop(optional, into)]
crossorigin: Option<Cow<'static, str>>,
crossorigin: Option<Oco<'static, str>>,
/// The [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-disabled) attribute.
#[prop(optional, into)]
disabled: Option<bool>,
/// The [`fetchpriority`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-fetchpriority) attribute.
#[prop(optional, into)]
fetchpriority: Option<Cow<'static, str>>,
fetchpriority: Option<Oco<'static, str>>,
/// The [`href`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-href) attribute.
#[prop(optional, into)]
href: Option<Cow<'static, str>>,
href: Option<Oco<'static, str>>,
/// The [`hreflang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-hreflang) attribute.
#[prop(optional, into)]
hreflang: Option<Cow<'static, str>>,
hreflang: Option<Oco<'static, str>>,
/// The [`imagesizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesizes) attribute.
#[prop(optional, into)]
imagesizes: Option<Cow<'static, str>>,
imagesizes: Option<Oco<'static, str>>,
/// The [`imagesrcset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesrcset) attribute.
#[prop(optional, into)]
imagesrcset: Option<Cow<'static, str>>,
imagesrcset: Option<Oco<'static, str>>,
/// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-integrity) attribute.
#[prop(optional, into)]
integrity: Option<Cow<'static, str>>,
integrity: Option<Oco<'static, str>>,
/// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-media) attribute.
#[prop(optional, into)]
media: Option<Cow<'static, str>>,
media: Option<Oco<'static, str>>,
/// The [`prefetch`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-prefetch) attribute.
#[prop(optional, into)]
prefetch: Option<Cow<'static, str>>,
prefetch: Option<Oco<'static, str>>,
/// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-referrerpolicy) attribute.
#[prop(optional, into)]
referrerpolicy: Option<Cow<'static, str>>,
referrerpolicy: Option<Oco<'static, str>>,
/// The [`rel`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-rel) attribute.
#[prop(optional, into)]
rel: Option<Cow<'static, str>>,
rel: Option<Oco<'static, str>>,
/// The [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes) attribute.
#[prop(optional, into)]
sizes: Option<Cow<'static, str>>,
sizes: Option<Oco<'static, str>>,
/// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-title) attribute.
#[prop(optional, into)]
title: Option<Cow<'static, str>>,
title: Option<Oco<'static, str>>,
/// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-type) attribute.
#[prop(optional, into)]
type_: Option<Cow<'static, str>>,
type_: Option<Oco<'static, str>>,
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-blocking) attribute.
#[prop(optional, into)]
blocking: Option<Cow<'static, str>>,
blocking: Option<Oco<'static, str>>,
) -> impl IntoView {
let meta = use_head();
let next_id = meta.tags.get_next_id();
let id: Cow<'static, str> =
let id: Oco<'static, str> =
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
let builder_el = leptos::leptos_dom::html::as_meta_tag({

View File

@@ -1,6 +1,5 @@
use crate::use_head;
use leptos::{nonce::use_nonce, *};
use std::borrow::Cow;
/// Injects an [HTMLScriptElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement) into the document
/// head, accepting any of the valid attributes for that tag.
@@ -25,47 +24,47 @@ use std::borrow::Cow;
pub fn Script(
/// An ID for the `<script>` tag.
#[prop(optional, into)]
id: Option<Cow<'static, str>>,
id: Option<Oco<'static, str>>,
/// The [`async`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-async) attribute.
#[prop(optional, into)]
async_: Option<Cow<'static, str>>,
async_: Option<Oco<'static, str>>,
/// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-crossorigin) attribute.
#[prop(optional, into)]
crossorigin: Option<Cow<'static, str>>,
crossorigin: Option<Oco<'static, str>>,
/// The [`defer`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer) attribute.
#[prop(optional, into)]
defer: Option<Cow<'static, str>>,
defer: Option<Oco<'static, str>>,
/// The [`fetchpriority `](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-fetchpriority ) attribute.
#[prop(optional, into)]
fetchpriority: Option<Cow<'static, str>>,
fetchpriority: Option<Oco<'static, str>>,
/// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-integrity) attribute.
#[prop(optional, into)]
integrity: Option<Cow<'static, str>>,
integrity: Option<Oco<'static, str>>,
/// The [`nomodule`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nomodule) attribute.
#[prop(optional, into)]
nomodule: Option<Cow<'static, str>>,
nomodule: Option<Oco<'static, str>>,
/// The [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nonce) attribute.
#[prop(optional, into)]
nonce: Option<Cow<'static, str>>,
nonce: Option<Oco<'static, str>>,
/// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-referrerpolicy) attribute.
#[prop(optional, into)]
referrerpolicy: Option<Cow<'static, str>>,
referrerpolicy: Option<Oco<'static, str>>,
/// The [`src`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-src) attribute.
#[prop(optional, into)]
src: Option<Cow<'static, str>>,
src: Option<Oco<'static, str>>,
/// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type) attribute.
#[prop(optional, into)]
type_: Option<Cow<'static, str>>,
type_: Option<Oco<'static, str>>,
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-blocking) attribute.
#[prop(optional, into)]
blocking: Option<Cow<'static, str>>,
blocking: Option<Oco<'static, str>>,
/// The content of the `<script>` tag.
#[prop(optional)]
children: Option<Box<dyn FnOnce() -> Fragment>>,
) -> impl IntoView {
let meta = use_head();
let next_id = meta.tags.get_next_id();
let id: Cow<'static, str> =
let id: Oco<'static, str> =
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
let builder_el = leptos::leptos_dom::html::as_meta_tag({

View File

@@ -1,6 +1,5 @@
use crate::use_head;
use leptos::{nonce::use_nonce, *};
use std::borrow::Cow;
/// Injects an [HTMLStyleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement) into the document
/// head, accepting any of the valid attributes for that tag.
@@ -25,26 +24,26 @@ use std::borrow::Cow;
pub fn Style(
/// An ID for the `<script>` tag.
#[prop(optional, into)]
id: Option<Cow<'static, str>>,
id: Option<Oco<'static, str>>,
/// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-media) attribute.
#[prop(optional, into)]
media: Option<Cow<'static, str>>,
media: Option<Oco<'static, str>>,
/// The [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-nonce) attribute.
#[prop(optional, into)]
nonce: Option<Cow<'static, str>>,
nonce: Option<Oco<'static, str>>,
/// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-title) attribute.
#[prop(optional, into)]
title: Option<Cow<'static, str>>,
title: Option<Oco<'static, str>>,
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-blocking) attribute.
#[prop(optional, into)]
blocking: Option<Cow<'static, str>>,
blocking: Option<Oco<'static, str>>,
/// The content of the `<style>` tag.
#[prop(optional)]
children: Option<Box<dyn FnOnce() -> Fragment>>,
) -> impl IntoView {
let meta = use_head();
let next_id = meta.tags.get_next_id();
let id: Cow<'static, str> =
let id: Oco<'static, str> =
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
let builder_el = leptos::leptos_dom::html::as_meta_tag({

View File

@@ -16,11 +16,11 @@ pub struct TitleContext {
impl TitleContext {
/// Converts the title into a string that can be used as the text content of a `<title>` tag.
pub fn as_string(&self) -> Option<String> {
let title = self.text.borrow().as_ref().map(|f| f.get());
pub fn as_string(&self) -> Option<Oco<'static, str>> {
let title = self.text.borrow().as_ref().map(TextProp::get);
title.map(|title| {
if let Some(formatter) = &*self.formatter.borrow() {
(formatter.0)(title)
(formatter.0)(title.into_owned()).into()
} else {
title
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.5.0"
version = "0.5.0-beta2"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -409,6 +409,13 @@ where
let on_response = Rc::new(move |resp: &web_sys::Response| {
let resp = resp.clone().expect("couldn't get Response");
// If the response was redirected then a JSON will not be available in the response, instead
// it will be an actual page, so we don't want to try to parse it.
if resp.redirected() {
return;
}
spawn_local(async move {
let body = JsFuture::from(
resp.text().expect("couldn't get .text() from Response"),

View File

@@ -24,6 +24,20 @@ impl ToHref for String {
}
}
impl ToHref for Cow<'_, str> {
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl ToHref for Oco<'_, str> {
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl<F> ToHref for F
where
F: Fn() -> String + 'static,
@@ -65,7 +79,7 @@ pub fn A<H>(
/// `[aria-current=page]` selector, you should prefer that, as it enables significant
/// SSR optimizations.
#[prop(optional, into)]
active_class: Option<Cow<'static, str>>,
active_class: Option<Oco<'static, str>>,
/// An object of any type that will be pushed to router state
#[prop(optional)]
state: Option<State>,
@@ -78,7 +92,7 @@ pub fn A<H>(
class: Option<AttributeValue>,
/// Sets the `id` attribute on the underlying `<a>` tag, making it easier to target.
#[prop(optional, into)]
id: Option<String>,
id: Option<Oco<'static, str>>,
/// The nodes or elements to be shown inside the link.
children: Children,
) -> impl IntoView
@@ -95,37 +109,35 @@ where
#[allow(unused)] state: Option<State>,
#[allow(unused)] replace: bool,
class: Option<AttributeValue>,
#[allow(unused)] active_class: Option<Cow<'static, str>>,
id: Option<String>,
#[allow(unused)] active_class: Option<Oco<'static, str>>,
id: Option<Oco<'static, str>>,
children: Children,
) -> View {
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
{
_ = state;
}
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
{
_ = replace;
}
let location = use_location();
let is_active = create_memo(move |_| match href.get() {
None => false,
Some(to) => {
let path = to
.split(['?', '#'])
.next()
.unwrap_or_default()
.to_lowercase();
let loc = location.pathname.get().to_lowercase();
if exact {
loc == path
} else {
loc.starts_with(&path)
}
}
let is_active = create_memo(move |_| {
href.with(|href| {
href.as_deref().is_some_and(|to| {
let path = to
.split(['?', '#'])
.next()
.unwrap_or_default()
.to_lowercase();
location.pathname.with(|loc| {
let loc = loc.to_lowercase();
if exact {
loc == path
} else {
loc.starts_with(&path)
}
})
})
})
});
#[cfg(feature = "ssr")]

View File

@@ -5,6 +5,7 @@ use crate::{
use leptos::{leptos_dom::Transparent, *};
use std::{
any::Any,
borrow::Cow,
cell::{Cell, RefCell},
rc::Rc,
};
@@ -309,7 +310,7 @@ impl RouteContext {
pub(crate) fn resolve_path_tracked(&self, to: &str) -> Option<String> {
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.get()))
.map(String::from)
.map(Cow::into_owned)
}
/// The nested child route, if any.

View File

@@ -544,6 +544,7 @@ pub(crate) fn create_branch(routes: &[RouteData], index: usize) -> Branch {
score: routes.last().unwrap().score() * 10000 - (index as i32),
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all,)
@@ -567,7 +568,7 @@ fn create_routes(route_def: &RouteDefinition, base: &str) -> Vec<RouteData> {
id: route_def.id,
matcher: Matcher::new_with_partial(&pattern, !is_leaf),
pattern,
original_path: original_path.to_string(),
original_path: original_path.into_owned(),
});
}
acc

View File

@@ -4,9 +4,9 @@ use crate::{
};
use leptos::{
create_memo, request_animation_frame, signal_prelude::*, use_context, Memo,
Oco,
};
use std::{borrow::Cow, rc::Rc, str::FromStr};
use std::{rc::Rc, str::FromStr};
/// Constructs a signal synchronized with a specific URL query parameter.
///
/// The function creates a bidirectional sync mechanism between the state encapsulated in a signal and a URL query parameter.
@@ -45,7 +45,7 @@ use std::{borrow::Cow, rc::Rc, str::FromStr};
/// ```
#[track_caller]
pub fn create_query_signal<T>(
key: impl Into<Cow<'static, str>>,
key: impl Into<Oco<'static, str>>,
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
where
T: FromStr + ToString + PartialEq,
@@ -163,7 +163,7 @@ pub fn use_resolved_path(
if path.starts_with('/') {
Some(path)
} else {
route.resolve_path_tracked(&path).map(String::from)
route.resolve_path_tracked(&path)
}
})
}

View File

@@ -2,7 +2,7 @@ use std::borrow::Cow;
#[doc(hidden)]
#[cfg(not(feature = "ssr"))]
pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
pub fn expand_optionals(pattern: &str) -> Vec<Cow<'_, str>> {
use js_sys::RegExp;
use once_cell::unsync::Lazy;
use wasm_bindgen::JsValue;
@@ -58,7 +58,7 @@ pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
#[doc(hidden)]
#[cfg(feature = "ssr")]
pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
pub fn expand_optionals(pattern: &str) -> Vec<Cow<'_, str>> {
use regex::Regex;
lazy_static::lazy_static! {

View File

@@ -555,14 +555,22 @@ where
let status = resp.status();
#[cfg(not(target_arch = "wasm32"))]
let status = status.as_u16();
if (500..=599).contains(&status) {
if (400..=599).contains(&status) {
let text = resp.text().await.unwrap_or_default();
#[cfg(target_arch = "wasm32")]
let status_text = resp.status_text();
#[cfg(not(target_arch = "wasm32"))]
let status_text = status.to_string();
return Err(serde_json::from_str(&text)
.unwrap_or(ServerFnError::ServerError(status_text)));
return Err(match serde_json::from_str(&text) {
Ok(e) => e,
Err(_) => {
#[cfg(target_arch = "wasm32")]
let status_text = resp.status_text();
#[cfg(not(target_arch = "wasm32"))]
let status_text = status.to_string();
ServerFnError::ServerError(if text.is_empty() {
format!("{} {}", status, status_text)
} else {
format!("{} {}: {}", status, status_text, text)
})
}
});
}
// Decoding the body of the request
@@ -582,12 +590,6 @@ where
#[cfg(not(target_arch = "wasm32"))]
let binary = binary.as_ref();
if status == 400 {
return Err(ServerFnError::ServerError(
"No server function was found at this URL.".to_string(),
));
}
ciborium::de::from_reader(binary)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
} else {
@@ -596,10 +598,6 @@ where
.await
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
if status == 400 {
return Err(ServerFnError::ServerError(text));
}
let mut deserializer = JSONDeserializer::from_str(&text);
T::deserialize(&mut deserializer)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))