mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 07:52:34 -05:00
Compare commits
25 Commits
v0.5.0-alp
...
1382-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ad3164f6f | ||
|
|
e0c9a9523a | ||
|
|
0726a3034d | ||
|
|
a88d047eff | ||
|
|
4001561987 | ||
|
|
2f860b37bd | ||
|
|
b86009b9d0 | ||
|
|
54733e1b34 | ||
|
|
56f01888b7 | ||
|
|
8320f16716 | ||
|
|
0b16e5992d | ||
|
|
248beb4a55 | ||
|
|
c9f608d030 | ||
|
|
f837d3e6a2 | ||
|
|
8847d5fc42 | ||
|
|
7819a6fac0 | ||
|
|
c199185808 | ||
|
|
e0b5738606 | ||
|
|
f3e3880a57 | ||
|
|
d44b90c16d | ||
|
|
cc32a3e863 | ||
|
|
5740c9b76b | ||
|
|
80fa6ad3eb | ||
|
|
7bc1ad2b4f | ||
|
|
82a2fe7cbe |
28
Cargo.toml
28
Cargo.toml
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.6"
|
||||
version = "0.4.8"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.4.6" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.6" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.6" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.6" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.6" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.6" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.6" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.6" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.6" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.6" }
|
||||
leptos_router = { path = "./router", version = "0.4.6" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.6" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.6" }
|
||||
leptos = { path = "./leptos", version = "0.4.8" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.8" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.8" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.8" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.8" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.8" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.8" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.8" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.8" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.8" }
|
||||
leptos_router = { path = "./router", version = "0.4.8" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.8" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.8" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -88,8 +88,6 @@ targets = ["wasm32-unknown-unknown"]
|
||||
|
||||
The `nightly` feature enables the function call syntax for accessing and setting signals, as opposed to `.get()` and `.set()`. This leads to a consistent mental model in which accessing a reactive value of any kind (a signal, memo, or derived signal) is always represented as a function call. This is only possible with nightly Rust and the `nightly` feature.
|
||||
|
||||
> Note: The `nightly` feature is present on the main branch version right now, but not in 0.3.x. For 0.3.x, nightly is the default and `stable` has a special feature.
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
|
||||
|
||||
@@ -26,7 +26,7 @@ cargo init leptos-tutorial
|
||||
cargo add leptos --features=csr,nightly
|
||||
```
|
||||
|
||||
Or you can leave off `nighly` if you're using stable Rust
|
||||
Or you can leave off `nightly` if you're using stable Rust
|
||||
```bash
|
||||
cargo add leptos --features=csr
|
||||
```
|
||||
|
||||
@@ -84,7 +84,7 @@ Because it offers the best blend of performance characteristics, Leptos defaults
|
||||
```rust
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
<Route path="" view=HomePage
|
||||
<Route path="" view=HomePage/>
|
||||
|
||||
// We'll load the posts with async rendering, so they can set
|
||||
// the title and metadata *after* loading the data
|
||||
|
||||
@@ -5,34 +5,34 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
|
||||
CARGO_MAKE_WORKSPACE_EMULATION = true
|
||||
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
"animated_show",
|
||||
"counter",
|
||||
"counter_isomorphic",
|
||||
"counters",
|
||||
"counters_stable",
|
||||
"counter_url_query,
|
||||
"counter_without_macros",
|
||||
"error_boundary",
|
||||
"errors_axum",
|
||||
"fetch",
|
||||
"hackernews",
|
||||
"hackernews_axum",
|
||||
"js-framework-benchmark",
|
||||
"leptos-tailwind-axum",
|
||||
"login_with_token_csr_only",
|
||||
"parent_child",
|
||||
"router",
|
||||
"session_auth_axum",
|
||||
"slots",
|
||||
"ssr_modes",
|
||||
"ssr_modes_axum",
|
||||
"tailwind",
|
||||
"tailwind_csr_trunk",
|
||||
"timer",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
"todo_app_sqlite_viz",
|
||||
"todomvc",
|
||||
"animated_show",
|
||||
"counter",
|
||||
"counter_isomorphic",
|
||||
"counters",
|
||||
"counters_stable",
|
||||
"counter_url_query",
|
||||
"counter_without_macros",
|
||||
"error_boundary",
|
||||
"errors_axum",
|
||||
"fetch",
|
||||
"hackernews",
|
||||
"hackernews_axum",
|
||||
"js-framework-benchmark",
|
||||
"leptos-tailwind-axum",
|
||||
"login_with_token_csr_only",
|
||||
"parent_child",
|
||||
"router",
|
||||
"session_auth_axum",
|
||||
"slots",
|
||||
"ssr_modes",
|
||||
"ssr_modes_axum",
|
||||
"tailwind",
|
||||
"tailwind_csr_trunk",
|
||||
"timer",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
"todo_app_sqlite_viz",
|
||||
"todomvc",
|
||||
]
|
||||
|
||||
[tasks.gen-members]
|
||||
@@ -48,9 +48,9 @@ jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
|
||||
'''
|
||||
|
||||
[tasks.test-info]
|
||||
[tasks.test-runner-report]
|
||||
workspace = false
|
||||
description = "report ci test runners for each example - Option [all]"
|
||||
description = "report ci test runners for each example - OPTION: [all]"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
@@ -59,63 +59,53 @@ YELLOW="\e[0;33m"
|
||||
RESET="\e[0m"
|
||||
|
||||
echo
|
||||
echo "${YELLOW}CI test runners by example...${RESET}"
|
||||
echo "${YELLOW}Test Runner Report${RESET}"
|
||||
echo "${ITALIC}Pass the option \"all\" to show all the examples${RESET}"
|
||||
echo
|
||||
|
||||
examples=$(ls |
|
||||
grep -v README.md |
|
||||
grep -v Makefile.toml |
|
||||
grep -v cargo-make |
|
||||
grep -v gtk |
|
||||
sort -u |
|
||||
awk '{print $0 ", "}')
|
||||
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' |
|
||||
sed 's%./%%' |
|
||||
sed 's%/Makefile.toml%%' |
|
||||
grep -v Makefile.toml |
|
||||
sort -u)
|
||||
|
||||
example_root_dir=$(pwd)
|
||||
start_path=$(pwd)
|
||||
|
||||
for example_dir in $examples
|
||||
do
|
||||
clean_name=$(echo $example_dir | sed 's%,%%')
|
||||
cd $clean_name
|
||||
|
||||
c_tests=$(grep -rl --fixed-strings "#[test]" | wc -l)
|
||||
rs_tests=$(grep -rl --fixed-strings "#[rstest]" | wc -l)
|
||||
w_configs=$(grep -rl "\/wasm-test.toml\"" | wc -l)
|
||||
pw_configs=$(grep -rl "\/playwright-test.toml\"" | wc -l)
|
||||
cl_configs=$(grep -rl "\/cargo-leptos-test.toml\"" | wc -l)
|
||||
for path in $makefile_paths; do
|
||||
cd $path
|
||||
|
||||
test_runner=
|
||||
|
||||
if [ $c_tests -gt 0 ]; then
|
||||
test_count=$(grep -rl -E "#\[(test|rstest)\]" | wc -l)
|
||||
if [ $test_count -gt 0 ]; then
|
||||
test_runner="-C"
|
||||
fi
|
||||
|
||||
if [ $rs_tests -gt 0 ]; then
|
||||
test_runner=$test_runner"-R"
|
||||
fi
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"wasm-test.toml"*)
|
||||
test_runner=$test_runner"-W"
|
||||
;;
|
||||
*"playwright-test.toml"*)
|
||||
test_runner=$test_runner"-P"
|
||||
;;
|
||||
*"cargo-leptos-test.toml"*)
|
||||
test_runner=$test_runner"-L"
|
||||
;;
|
||||
esac
|
||||
done <"./Makefile.toml"
|
||||
|
||||
if [ $w_configs -gt 0 ]; then
|
||||
test_runner=$test_runner"-W"
|
||||
fi
|
||||
|
||||
if [ $pw_configs -gt 0 ]; then
|
||||
test_runner=$test_runner"-P"
|
||||
fi
|
||||
|
||||
if [ $cl_configs -gt 0 ]; then
|
||||
test_runner=$test_runner"-L"
|
||||
fi
|
||||
|
||||
if [ ! -z "$1" ]; then
|
||||
# Show all examples
|
||||
echo "$clean_name ${BOLD}${test_runner}${RESET}"
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
elif [ ! -z $test_runner ]; then
|
||||
# Filter out examples that do not run tests in `ci`
|
||||
echo "$clean_name ${BOLD}${test_runner}${RESET}"
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
fi
|
||||
|
||||
cd $example_root_dir
|
||||
cd ${start_path}
|
||||
done
|
||||
echo
|
||||
echo "${ITALIC}Test Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, R = RS Test, W = WASM Test${RESET}"
|
||||
echo "${ITALIC}Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, W = WASM Test${RESET}"
|
||||
echo
|
||||
'''
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn Nav(cx: Scope) -> impl IntoView {
|
||||
<A href="/job">
|
||||
<strong>"Jobs"</strong>
|
||||
</A>
|
||||
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
|
||||
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
|
||||
"Built with Leptos"
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn Nav(cx: Scope) -> impl IntoView {
|
||||
<A href="/job">
|
||||
<strong>"Jobs"</strong>
|
||||
</A>
|
||||
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
|
||||
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
|
||||
"Built with Leptos"
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
@@ -14,7 +14,7 @@ fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
|
||||
r#"
|
||||
<script crossorigin=""{nonce_str}>(function () {{
|
||||
{}
|
||||
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
let ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
let msg = JSON.parse(ev.data);
|
||||
if (msg.all) window.location.reload();
|
||||
|
||||
@@ -4,7 +4,7 @@ version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/gbj/leptos"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces."
|
||||
readme = "../README.md"
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ pub fn AnimatedShow(
|
||||
});
|
||||
|
||||
on_cleanup(cx, move || {
|
||||
if let Some(h) = handle.get_value() {
|
||||
if let Some(Some(h)) = handle.try_get_value() {
|
||||
h.clear();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -156,18 +156,10 @@ 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::{
|
||||
event_target, event_target_checked, event_target_value,
|
||||
request_animation_frame, request_animation_frame_with_handle,
|
||||
request_idle_callback, request_idle_callback_with_handle, set_interval,
|
||||
set_interval_with_handle, set_timeout, set_timeout_with_handle,
|
||||
window_event_listener, window_event_listener_untyped,
|
||||
},
|
||||
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, 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,
|
||||
};
|
||||
|
||||
/// Types to make it easier to handle errors in your application.
|
||||
|
||||
@@ -72,9 +72,6 @@ where
|
||||
let current_id = HydrationCtx::next_component();
|
||||
|
||||
let child = DynChild::new({
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
let current_id = current_id;
|
||||
|
||||
let children = Rc::new(orig_children(cx).into_view(cx));
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
let orig_children = Rc::clone(&orig_children);
|
||||
|
||||
@@ -222,16 +222,31 @@ where
|
||||
// node
|
||||
let ret = if let Some(prev_t) = prev_t {
|
||||
// Here, our child is also a text node
|
||||
if let Some(new_t) = new_child.get_text() {
|
||||
|
||||
// nb: the match/ownership gymnastics here
|
||||
// are so that, if we can reuse the text node,
|
||||
// we can take ownership of new_t so we don't clone
|
||||
// the contents, which in O(n) on the length of the text
|
||||
if matches!(new_child, View::Text(_)) {
|
||||
if !was_child_moved && child != new_child {
|
||||
let mut new_t = match new_child {
|
||||
View::Text(t) => t,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
prev_t
|
||||
.unchecked_ref::<web_sys::Text>()
|
||||
.set_data(&new_t.content);
|
||||
|
||||
// replace new_t's text node with the prev node
|
||||
// see discussion: https://github.com/leptos-rs/leptos/pull/1472
|
||||
new_t.node = prev_t.clone();
|
||||
|
||||
let new_child = View::Text(new_t);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(Some(prev_t), disposer)
|
||||
} else {
|
||||
let new_t = new_child.as_text().unwrap();
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
|
||||
@@ -377,6 +377,9 @@ where
|
||||
|
||||
let component = EachRepr::default();
|
||||
|
||||
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
||||
let opening = component.opening.node.clone().unchecked_into();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let (children, closing) =
|
||||
(component.children.clone(), component.closing.node.clone());
|
||||
@@ -387,7 +390,11 @@ where
|
||||
move |prev_hash_run: Option<HashRun<FxIndexSet<K>>>| {
|
||||
let mut children_borrow = children.borrow_mut();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[cfg(all(
|
||||
not(debug_assertions),
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
let opening = if let Some(Some(child)) = children_borrow.get(0)
|
||||
{
|
||||
// correctly remove opening <!--<EachItem/>-->
|
||||
|
||||
@@ -1073,7 +1073,6 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
|
||||
let id = *element.hydration_id();
|
||||
|
||||
let mut element = Element::new(element);
|
||||
let children = children;
|
||||
|
||||
if attrs.iter_mut().any(|(name, _)| name == "id") {
|
||||
attrs.push(("leptos-hk".into(), format!("_{id}").into()));
|
||||
|
||||
@@ -6,6 +6,9 @@ mod hydration {
|
||||
use std::{cell::RefCell, collections::HashMap};
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
/// See ["createTreeWalker"](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker)
|
||||
const FILTER_SHOW_COMMENT: u32 = 0b10000000;
|
||||
|
||||
// We can tell if we start in hydration mode by checking to see if the
|
||||
// id "_0-1" is present in the DOM. If it is, we know we are hydrating from
|
||||
// the server, if not, we are starting off in CSR
|
||||
@@ -14,7 +17,7 @@ mod hydration {
|
||||
let document = crate::document();
|
||||
let body = document.body().unwrap();
|
||||
let walker = document
|
||||
.create_tree_walker_with_what_to_show(&body, 128)
|
||||
.create_tree_walker_with_what_to_show(&body, FILTER_SHOW_COMMENT)
|
||||
.unwrap();
|
||||
let mut map = HashMap::new();
|
||||
while let Ok(Some(node)) = walker.next_node() {
|
||||
@@ -34,7 +37,7 @@ mod hydration {
|
||||
let document = crate::document();
|
||||
let body = document.body().unwrap();
|
||||
let walker = document
|
||||
.create_tree_walker_with_what_to_show(&body, 128)
|
||||
.create_tree_walker_with_what_to_show(&body, FILTER_SHOW_COMMENT)
|
||||
.unwrap();
|
||||
let mut map = HashMap::new();
|
||||
while let Ok(Some(node)) = walker.next_node() {
|
||||
|
||||
@@ -37,7 +37,7 @@ pub use hydration::{HydrationCtx, HydrationKey};
|
||||
use leptos_reactive::Scope;
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
pub use logging::*;
|
||||
pub use macro_helpers::*;
|
||||
@@ -211,6 +211,20 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoView for MaybeProp<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", name = "MaybeSignal<T>", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
DynChild::new(move || self.get()).into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects an iterator or collection into a [`View`].
|
||||
pub trait CollectView {
|
||||
/// Collects an iterator or collection into a [`View`].
|
||||
@@ -272,8 +286,12 @@ cfg_if! {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
|
||||
let attrs =
|
||||
self.attrs.iter().map(|(n, v)| format!(" {n}=\"{v}\"")).collect::<String>();
|
||||
let attrs = self.attrs.iter().fold(String::new(), |mut output, (n, v)| {
|
||||
// can safely ignore output
|
||||
// see https://rust-lang.github.io/rust-clippy/master/index.html#/format_collect
|
||||
let _ = write!(output, " {n}=\"{v}\"");
|
||||
output
|
||||
});
|
||||
|
||||
if self.is_void {
|
||||
write!(f, "<{}{attrs} />", self.name)
|
||||
|
||||
@@ -357,10 +357,10 @@ fn fragments_to_chunks(
|
||||
r#"
|
||||
<template id="{fragment_id}f">{html}</template>
|
||||
<script{nonce_str}>
|
||||
var id = "{fragment_id}";
|
||||
var open = undefined;
|
||||
var close = undefined;
|
||||
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
||||
(function() {{ let id = "{fragment_id}";
|
||||
let open = undefined;
|
||||
let close = undefined;
|
||||
let walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
||||
while(walker.nextNode()) {{
|
||||
if(walker.currentNode.textContent == `suspense-open-${{id}}`) {{
|
||||
open = walker.currentNode;
|
||||
@@ -368,12 +368,12 @@ fn fragments_to_chunks(
|
||||
close = walker.currentNode;
|
||||
}}
|
||||
}}
|
||||
var range = new Range();
|
||||
let range = new Range();
|
||||
range.setStartAfter(open);
|
||||
range.setEndBefore(close);
|
||||
range.deleteContents();
|
||||
var tpl = document.getElementById("{fragment_id}f");
|
||||
close.parentNode.insertBefore(tpl.content.cloneNode(true), close);
|
||||
let tpl = document.getElementById("{fragment_id}f");
|
||||
close.parentNode.insertBefore(tpl.content.cloneNode(true), close);}})()
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
@@ -483,18 +483,36 @@ impl View {
|
||||
true,
|
||||
Box::new(move || {
|
||||
if let Some(child) = *child {
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
if !cfg!(debug_assertions) {
|
||||
format!("<!>{}", t.content).into()
|
||||
// if we don't check if the string is empty,
|
||||
// the HTML is an empty string; but an empty string
|
||||
// is not a text node in HTML, so can't be updated
|
||||
// in the future. so we put a one-space text node instead
|
||||
let was_empty = t.content.is_empty();
|
||||
let content = if was_empty {
|
||||
" ".into()
|
||||
} else {
|
||||
t.content
|
||||
};
|
||||
// escape content unless we're in a <script> or <style>
|
||||
let content = if dont_escape_text {
|
||||
content
|
||||
} else {
|
||||
html_escape::encode_safe(&content)
|
||||
.to_string()
|
||||
.into()
|
||||
};
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if !cfg!(debug_assertions) {
|
||||
format!("<!>{content}",).into()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
} else {
|
||||
child.render_to_string_helper(
|
||||
@@ -710,12 +728,12 @@ pub(crate) fn render_serializers(
|
||||
let json = json.replace('<', "\\u003c");
|
||||
format!(
|
||||
r#"<script{nonce_str}>
|
||||
var val = {json:?};
|
||||
(function() {{ let val = {json:?};
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)
|
||||
}} else {{
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, val);
|
||||
}}
|
||||
}} }})();
|
||||
</script>"#,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -416,34 +416,50 @@ impl View {
|
||||
Box::new(
|
||||
move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
if let Some(child) = *child {
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
let content = if dont_escape_text {
|
||||
// if we don't check if the string is empty,
|
||||
// the HTML is an empty string; but an empty string
|
||||
// is not a text node in HTML, so can't be updated
|
||||
// in the future. so we put a one-space text node instead
|
||||
let was_empty =
|
||||
t.content.is_empty();
|
||||
let content = if was_empty {
|
||||
" ".into()
|
||||
} else {
|
||||
t.content
|
||||
};
|
||||
// escape content unless we're in a <script> or <style>
|
||||
let content = if dont_escape_text {
|
||||
content
|
||||
} else {
|
||||
html_escape::encode_safe(
|
||||
&t.content,
|
||||
&content,
|
||||
)
|
||||
.to_string()
|
||||
.into()
|
||||
};
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
chunks.push_back(
|
||||
if !cfg!(debug_assertions) {
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!>{}",
|
||||
content
|
||||
html_escape::encode_safe(
|
||||
&content
|
||||
)
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
StreamChunk::Sync(content)
|
||||
StreamChunk::Sync(html_escape::encode_safe(
|
||||
&content
|
||||
).to_string().into())
|
||||
},
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -33,6 +33,7 @@ log = "0.4"
|
||||
typed-builder = "0.14"
|
||||
trybuild = "1"
|
||||
leptos = { path = "../leptos" }
|
||||
insta = "1.29"
|
||||
|
||||
[features]
|
||||
csr = []
|
||||
|
||||
@@ -28,7 +28,7 @@ pub fn TestComponent(
|
||||
/// # use example::TestComponent;
|
||||
/// <TestComponent key="hello"/>
|
||||
#[prop(optional)]
|
||||
another:usize,
|
||||
another: usize,
|
||||
/// rust unclosed
|
||||
/// ```view
|
||||
/// use example::TestComponent;
|
||||
@@ -38,3 +38,22 @@ pub fn TestComponent(
|
||||
_ = (key, another, and_another);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TestMutCallback<'a, F>(
|
||||
cx: Scope,
|
||||
mut callback: F,
|
||||
value: &'a str,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: FnMut(u32) + 'static,
|
||||
{
|
||||
let value = value.to_owned();
|
||||
view! { cx,
|
||||
<button on:click=move |_| {
|
||||
callback(5);
|
||||
}>
|
||||
{value}
|
||||
</button>
|
||||
<TestComponent key="test"/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +284,8 @@ impl ToTokens for Model {
|
||||
) #ret #(+ #lifetimes)*
|
||||
#where_clause
|
||||
{
|
||||
// allowed for lifetimes that are needed for props struct
|
||||
#[allow(clippy::needless_lifetimes)]
|
||||
#body
|
||||
|
||||
#destructure_props
|
||||
@@ -589,12 +591,14 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
quote!()
|
||||
};
|
||||
|
||||
let PatIdent { ident, by_ref, .. } = &name;
|
||||
|
||||
quote! {
|
||||
#docs
|
||||
#builder_docs
|
||||
#builder_attrs
|
||||
#allow_missing_docs
|
||||
#vis #name: #ty,
|
||||
#vis #by_ref #ident: #ty,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -604,7 +608,12 @@ fn prop_names(props: &[Prop]) -> TokenStream {
|
||||
props
|
||||
.iter()
|
||||
.filter(|Prop { ty, .. }| !is_valid_scope_type(ty))
|
||||
.map(|Prop { name, .. }| quote! { #name, })
|
||||
.map(|Prop { name, .. }| {
|
||||
// fields like mutability are removed because unneeded
|
||||
// in the contexts in which this is used
|
||||
let ident = &name.ident;
|
||||
quote! { #ident, }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,9 @@ impl Default for Mode {
|
||||
|
||||
mod params;
|
||||
mod view;
|
||||
use template::render_template;
|
||||
use view::render_view;
|
||||
use view::{client_template::render_template, render_view};
|
||||
mod component;
|
||||
mod slot;
|
||||
mod template;
|
||||
|
||||
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
|
||||
/// same rules as HTML, with the following differences:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
511
leptos_macro/src/view/client_builder.rs
Normal file
511
leptos_macro/src/view/client_builder.rs
Normal file
@@ -0,0 +1,511 @@
|
||||
use super::{
|
||||
component_builder::component_to_tokens,
|
||||
expr_to_ident, fancy_class_name, fancy_style_name,
|
||||
ide_helper::IdeTagHelper,
|
||||
is_ambiguous_element, is_custom_element, is_math_ml_element,
|
||||
is_svg_element, parse_event_name,
|
||||
slot_helper::{get_slot, slot_to_tokens},
|
||||
};
|
||||
use crate::attribute_value;
|
||||
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::{quote, quote_spanned};
|
||||
use rstml::node::{KeyedAttribute, Node, NodeAttribute, NodeElement, NodeName};
|
||||
use std::collections::HashMap;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum TagType {
|
||||
Unknown,
|
||||
Html,
|
||||
Svg,
|
||||
Math,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn fragment_to_tokens(
|
||||
cx: &Ident,
|
||||
_span: Span,
|
||||
nodes: &[Node],
|
||||
lazy: bool,
|
||||
parent_type: TagType,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> Option<TokenStream> {
|
||||
let mut slots = HashMap::new();
|
||||
let has_slots = parent_slots.is_some();
|
||||
|
||||
let mut nodes = nodes
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
let node = node_to_tokens(
|
||||
cx,
|
||||
node,
|
||||
parent_type,
|
||||
has_slots.then_some(&mut slots),
|
||||
global_class,
|
||||
None,
|
||||
)?;
|
||||
|
||||
Some(quote! {
|
||||
#node.into_view(#cx)
|
||||
})
|
||||
})
|
||||
.peekable();
|
||||
|
||||
if nodes.peek().is_none() {
|
||||
_ = nodes.collect::<Vec<_>>();
|
||||
if let Some(parent_slots) = parent_slots {
|
||||
for (slot, mut values) in slots.drain() {
|
||||
parent_slots
|
||||
.entry(slot)
|
||||
.and_modify(|entry| entry.append(&mut values))
|
||||
.or_insert(values);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let tokens = if lazy {
|
||||
quote! {
|
||||
{
|
||||
::leptos::Fragment::lazy(|| [
|
||||
#(#nodes),*
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
{
|
||||
::leptos::Fragment::new([
|
||||
#(#nodes),*
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(parent_slots) = parent_slots {
|
||||
for (slot, mut values) in slots.drain() {
|
||||
parent_slots
|
||||
.entry(slot)
|
||||
.and_modify(|entry| entry.append(&mut values))
|
||||
.or_insert(values);
|
||||
}
|
||||
}
|
||||
|
||||
Some(tokens)
|
||||
}
|
||||
|
||||
pub(crate) fn node_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &Node,
|
||||
parent_type: TagType,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> Option<TokenStream> {
|
||||
match node {
|
||||
Node::Fragment(fragment) => fragment_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
true,
|
||||
parent_type,
|
||||
None,
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => Some(quote! {}),
|
||||
Node::Text(node) => Some(quote! {
|
||||
::leptos::leptos_dom::html::text(#node)
|
||||
}),
|
||||
Node::Block(node) => Some(quote! { #node }),
|
||||
Node::RawText(r) => {
|
||||
let text = r.to_string_best();
|
||||
let text = syn::LitStr::new(&text, r.span());
|
||||
Some(quote! { #text })
|
||||
}
|
||||
Node::Element(node) => element_to_tokens(
|
||||
cx,
|
||||
node,
|
||||
parent_type,
|
||||
parent_slots,
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn element_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
mut parent_type: TagType,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> Option<TokenStream> {
|
||||
let name = node.name();
|
||||
if is_component_node(node) {
|
||||
if let Some(slot) = get_slot(node) {
|
||||
slot_to_tokens(cx, node, slot, parent_slots, global_class);
|
||||
None
|
||||
} else {
|
||||
Some(component_to_tokens(cx, node, global_class))
|
||||
}
|
||||
} else {
|
||||
let tag = name.to_string();
|
||||
// collect close_tag name to emit semantic information for IDE.
|
||||
let mut ide_helper_close_tag = IdeTagHelper::new();
|
||||
let close_tag = node.close_tag.as_ref().map(|c| &c.name);
|
||||
let name = if is_custom_element(&tag) {
|
||||
let name = node.name().to_string();
|
||||
// link custom ident to name span for IDE docs
|
||||
let custom = Ident::new("custom", name.span());
|
||||
quote! { ::leptos::leptos_dom::html::#custom(#cx, ::leptos::leptos_dom::html::Custom::new(#name)) }
|
||||
} else if is_svg_element(&tag) {
|
||||
parent_type = TagType::Svg;
|
||||
quote! { ::leptos::leptos_dom::svg::#name(#cx) }
|
||||
} else if is_math_ml_element(&tag) {
|
||||
parent_type = TagType::Math;
|
||||
quote! { ::leptos::leptos_dom::math::#name(#cx) }
|
||||
} else if is_ambiguous_element(&tag) {
|
||||
match parent_type {
|
||||
TagType::Unknown => {
|
||||
// We decided this warning was too aggressive, but I'll leave it here in case we want it later
|
||||
/* proc_macro_error::emit_warning!(name.span(), "The view macro is assuming this is an HTML element, \
|
||||
but it is ambiguous; if it is an SVG or MathML element, prefix with svg:: or math::"); */
|
||||
quote! {
|
||||
::leptos::leptos_dom::html::#name(#cx)
|
||||
}
|
||||
}
|
||||
TagType::Html => {
|
||||
quote! { ::leptos::leptos_dom::html::#name(#cx) }
|
||||
}
|
||||
TagType::Svg => {
|
||||
quote! { ::leptos::leptos_dom::svg::#name(#cx) }
|
||||
}
|
||||
TagType::Math => {
|
||||
quote! { ::leptos::leptos_dom::math::#name(#cx) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parent_type = TagType::Html;
|
||||
quote! { ::leptos::leptos_dom::html::#name(#cx) }
|
||||
};
|
||||
|
||||
if let Some(close_tag) = close_tag {
|
||||
ide_helper_close_tag.save_tag_completion(close_tag)
|
||||
}
|
||||
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
let name = name.trim();
|
||||
if name.starts_with("class:")
|
||||
|| fancy_class_name(name, cx, node).is_some()
|
||||
|| name.starts_with("style:")
|
||||
|| fancy_style_name(name, cx, node).is_some()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(attribute_to_tokens(cx, node, global_class))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let class_attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if let Some((fancy, _, _)) = fancy_class_name(&name, cx, node) {
|
||||
Some(fancy)
|
||||
} else if name.trim().starts_with("class:") {
|
||||
Some(attribute_to_tokens(cx, node, global_class))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let style_attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if let Some((fancy, _, _)) = fancy_style_name(&name, cx, node) {
|
||||
Some(fancy)
|
||||
} else if name.trim().starts_with("style:") {
|
||||
Some(attribute_to_tokens(cx, node, global_class))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let global_class_expr = match global_class {
|
||||
None => quote! {},
|
||||
Some(class) => {
|
||||
quote! {
|
||||
.classes(
|
||||
#[allow(unused_braces)]
|
||||
#class
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
let children = node.children.iter().map(|node| {
|
||||
let (child, is_static) = match node {
|
||||
Node::Fragment(fragment) => (
|
||||
fragment_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
true,
|
||||
parent_type,
|
||||
None,
|
||||
global_class,
|
||||
None,
|
||||
)
|
||||
.unwrap_or({
|
||||
let span = Span::call_site();
|
||||
quote_spanned! {
|
||||
span => ::leptos::leptos_dom::Unit
|
||||
}
|
||||
}),
|
||||
false,
|
||||
),
|
||||
Node::Text(node) => (quote! { #node }, true),
|
||||
Node::RawText(node) => {
|
||||
let text = node.to_string_best();
|
||||
let text = syn::LitStr::new(&text, node.span());
|
||||
(quote! { #text }, true)
|
||||
}
|
||||
Node::Block(node) => (
|
||||
quote! {
|
||||
#node
|
||||
},
|
||||
false,
|
||||
),
|
||||
Node::Element(node) => (
|
||||
element_to_tokens(
|
||||
cx,
|
||||
node,
|
||||
parent_type,
|
||||
None,
|
||||
global_class,
|
||||
None,
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
false,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => (quote! {}, false),
|
||||
};
|
||||
if is_static {
|
||||
quote! {
|
||||
.child(#child)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.child((#cx, #child))
|
||||
}
|
||||
}
|
||||
});
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let ide_helper_close_tag = ide_helper_close_tag.into_iter();
|
||||
Some(quote! {
|
||||
{
|
||||
#(#ide_helper_close_tag)*
|
||||
#name
|
||||
#(#attrs)*
|
||||
#(#class_attrs)*
|
||||
#(#style_attrs)*
|
||||
#global_class_expr
|
||||
#(#children)*
|
||||
#view_marker
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attribute_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &KeyedAttribute,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let span = node.key.span();
|
||||
let name = node.key.to_string();
|
||||
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
|
||||
let value = expr_to_ident(attribute_value(node));
|
||||
let node_ref = quote_spanned! { span => node_ref };
|
||||
|
||||
quote! {
|
||||
.#node_ref(#value)
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("on:") {
|
||||
let handler = attribute_value(node);
|
||||
|
||||
let (event_type, is_custom, is_force_undelegated) =
|
||||
parse_event_name(name);
|
||||
|
||||
let event_name_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => {
|
||||
if parts.len() >= 2 {
|
||||
Some(&parts[1])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let undelegated_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => parts.last().and_then(|last| {
|
||||
if last.to_string() == "undelegated" {
|
||||
Some(last)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let on = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let on = {
|
||||
let span = on.span();
|
||||
quote_spanned! {
|
||||
span => .on
|
||||
}
|
||||
};
|
||||
let event_type = if is_custom {
|
||||
event_type
|
||||
} else if let Some(ev_name) = event_name_ident {
|
||||
let span = ev_name.span();
|
||||
quote_spanned! {
|
||||
span => #ev_name
|
||||
}
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
|
||||
let event_type = if is_force_undelegated {
|
||||
let undelegated = if let Some(undelegated) = undelegated_ident {
|
||||
let span = undelegated.span();
|
||||
quote_spanned! {
|
||||
span => #undelegated
|
||||
}
|
||||
} else {
|
||||
quote! { undelegated }
|
||||
};
|
||||
quote! { ::leptos::ev::#undelegated(::leptos::ev::#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::ev::#event_type }
|
||||
};
|
||||
|
||||
quote! {
|
||||
#on(#event_type, #handler)
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("prop:") {
|
||||
let value = attribute_value(node);
|
||||
let prop = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let prop = {
|
||||
let span = prop.span();
|
||||
quote_spanned! {
|
||||
span => .prop
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#prop(#name, (#cx, #[allow(unused_braces)] #value))
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("class:") {
|
||||
let value = attribute_value(node);
|
||||
let class = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let class = {
|
||||
let span = class.span();
|
||||
quote_spanned! {
|
||||
span => .class
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#class(#name, (#cx, #[allow(unused_braces)] #value))
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("style:") {
|
||||
let value = attribute_value(node);
|
||||
let style = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let style = {
|
||||
let span = style.span();
|
||||
quote_spanned! {
|
||||
span => .style
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#style(#name, (#cx, #[allow(unused_braces)] #value))
|
||||
}
|
||||
} else {
|
||||
let name = name.replacen("attr:", "", 1);
|
||||
|
||||
if let Some((fancy, _, _)) = fancy_class_name(&name, cx, node) {
|
||||
return fancy;
|
||||
}
|
||||
|
||||
// special case of global_class and class attribute
|
||||
if name == "class"
|
||||
&& global_class.is_some()
|
||||
&& node.value().and_then(value_to_string).is_none()
|
||||
{
|
||||
let span = node.key.span();
|
||||
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
|
||||
and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \
|
||||
toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \
|
||||
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
|
||||
};
|
||||
|
||||
// all other attributes
|
||||
let value = match node.value() {
|
||||
Some(value) => {
|
||||
quote! { #value }
|
||||
}
|
||||
None => quote_spanned! { span => "" },
|
||||
};
|
||||
|
||||
let attr = match &node.key {
|
||||
NodeName::Punctuated(parts) => Some(&parts[0]),
|
||||
_ => None,
|
||||
};
|
||||
let attr = if let Some(attr) = attr {
|
||||
let span = attr.span();
|
||||
quote_spanned! {
|
||||
span => .attr
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.attr
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#attr(#name, (#cx, #value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{attribute_value, view::IdeTagHelper};
|
||||
use super::{component_builder::component_to_tokens, IdeTagHelper};
|
||||
use crate::attribute_value;
|
||||
use itertools::Either;
|
||||
use leptos_hot_reload::parsing::{
|
||||
block_to_primitive_expression, is_component_node, value_to_string,
|
||||
@@ -9,13 +10,10 @@ use rstml::node::{
|
||||
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement,
|
||||
};
|
||||
use syn::spanned::Spanned;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) fn render_template(cx: &Ident, nodes: &[Node]) -> TokenStream {
|
||||
let template_uid = Ident::new(
|
||||
&format!("TEMPLATE_{}", Uuid::new_v4().simple()),
|
||||
Span::call_site(),
|
||||
);
|
||||
// No reason to make template unique, because its "static" is in inner scope.
|
||||
let template_uid = Ident::new("__TEMPLATE", Span::call_site());
|
||||
|
||||
match nodes.first() {
|
||||
Some(Node::Element(node)) => {
|
||||
@@ -36,7 +34,7 @@ fn root_element_to_tokens(
|
||||
let mut expressions = Vec::new();
|
||||
|
||||
if is_component_node(node) {
|
||||
crate::view::component_to_tokens(cx, node, None)
|
||||
component_to_tokens(cx, node, None)
|
||||
} else {
|
||||
element_to_tokens(
|
||||
cx,
|
||||
@@ -65,11 +63,11 @@ fn root_element_to_tokens(
|
||||
quote! {
|
||||
{
|
||||
thread_local! {
|
||||
static #template_uid: leptos::web_sys::HtmlTemplateElement = {
|
||||
let document = leptos::document();
|
||||
static #template_uid: ::leptos::web_sys::HtmlTemplateElement = {
|
||||
let document = ::leptos::document();
|
||||
let el = document.create_element("template").unwrap();
|
||||
el.set_inner_html(#template);
|
||||
leptos::wasm_bindgen::JsCast::unchecked_into(el)
|
||||
::leptos::wasm_bindgen::JsCast::unchecked_into(el)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +77,10 @@ fn root_element_to_tokens(
|
||||
#(#navigations)*
|
||||
#(#expressions;)*
|
||||
|
||||
leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
|
||||
::leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
|
||||
#[cfg(debug_assertions)]
|
||||
name: #tag_name.into(),
|
||||
element: leptos::wasm_bindgen::JsCast::unchecked_into(root),
|
||||
element: ::leptos::wasm_bindgen::JsCast::unchecked_into(root),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None
|
||||
})
|
||||
@@ -149,7 +147,7 @@ fn element_to_tokens(
|
||||
quote_spanned! {
|
||||
span => let #this_el_ident = #debug_name;
|
||||
let #this_el_ident =
|
||||
leptos::wasm_bindgen::JsCast::unchecked_into::<leptos::web_sys::Node>(#parent.clone());
|
||||
::leptos::wasm_bindgen::JsCast::unchecked_into::<leptos::web_sys::Node>(#parent.clone());
|
||||
//debug!("=> got {}", #this_el_ident.node_name());
|
||||
}
|
||||
} else if let Some(prev_sib) = &prev_sib {
|
||||
@@ -302,7 +300,7 @@ fn attr_to_tokens(
|
||||
let (event_type, handler) =
|
||||
crate::view::event_from_attribute_node(node, false);
|
||||
expressions.push(quote! {
|
||||
leptos::leptos_dom::add_event_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #event_type, #handler);
|
||||
::leptos::leptos_dom::add_event_helper(::leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #event_type, #handler);
|
||||
})
|
||||
}
|
||||
// Properties
|
||||
@@ -310,7 +308,7 @@ fn attr_to_tokens(
|
||||
let value = attribute_value(node);
|
||||
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos::leptos_dom::property(#cx, leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name, #value.into_property(#cx))
|
||||
span => ::leptos::leptos_dom::property(#cx, ::leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name, #value.into_property(#cx))
|
||||
});
|
||||
}
|
||||
// Classes
|
||||
@@ -318,7 +316,7 @@ fn attr_to_tokens(
|
||||
let value = attribute_value(node);
|
||||
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos::leptos_dom::class_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name.into(), #value.into_class(#cx))
|
||||
span => ::leptos::leptos_dom::class_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name.into(), #value.into_class(#cx))
|
||||
});
|
||||
}
|
||||
// Attributes
|
||||
@@ -342,7 +340,7 @@ fn attr_to_tokens(
|
||||
// For client-side rendering, dynamic attributes don't need to be rendered in the template
|
||||
// They'll immediately be set synchronously before the cloned template is mounted
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos::leptos_dom::attribute_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name.into(), {#value}.into_attribute(#cx))
|
||||
span => ::leptos::leptos_dom::attribute_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name.into(), {#value}.into_attribute(#cx))
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -493,10 +491,10 @@ fn block_to_tokens(
|
||||
|
||||
let mount_kind = match &next_sib {
|
||||
Some(child) => {
|
||||
quote! { leptos::leptos_dom::MountKind::Before(&#child.clone()) }
|
||||
quote! { ::leptos::leptos_dom::MountKind::Before(&#child.clone()) }
|
||||
}
|
||||
None => {
|
||||
quote! { leptos::leptos_dom::MountKind::Append(&#parent) }
|
||||
quote! { ::leptos::leptos_dom::MountKind::Append(&#parent) }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -516,7 +514,7 @@ fn block_to_tokens(
|
||||
navigations.push(location);
|
||||
|
||||
expressions.push(quote! {
|
||||
leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
|
||||
::leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
|
||||
});
|
||||
|
||||
if let Some(name) = name {
|
||||
176
leptos_macro/src/view/component_builder.rs
Normal file
176
leptos_macro/src/view/component_builder.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use super::{
|
||||
client_builder::{fragment_to_tokens, TagType},
|
||||
event_from_attribute_node, ident_from_tag_name,
|
||||
};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote};
|
||||
use rstml::node::{NodeAttribute, NodeElement};
|
||||
use std::collections::HashMap;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
pub(crate) fn component_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let name = node.name();
|
||||
#[cfg(debug_assertions)]
|
||||
let component_name = ident_from_tag_name(node.name());
|
||||
let span = node.name().span();
|
||||
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let props = attrs
|
||||
.clone()
|
||||
.filter(|attr| {
|
||||
!attr.key.to_string().starts_with("bind:")
|
||||
&& !attr.key.to_string().starts_with("clone:")
|
||||
&& !attr.key.to_string().starts_with("on:")
|
||||
})
|
||||
.map(|attr| {
|
||||
let name = &attr.key;
|
||||
|
||||
let value = attr
|
||||
.value()
|
||||
.map(|v| {
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
|
||||
quote! {
|
||||
.#name(#[allow(unused_braces)] #value)
|
||||
}
|
||||
});
|
||||
|
||||
let items_to_bind = attrs
|
||||
.clone()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("bind:")
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let items_to_clone = attrs
|
||||
.clone()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("clone:")
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let events = attrs
|
||||
.filter(|attr| attr.key.to_string().starts_with("on:"))
|
||||
.map(|attr| {
|
||||
let (event_type, handler) = event_from_attribute_node(attr, true);
|
||||
|
||||
quote! {
|
||||
.on(#event_type, #handler)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut slots = HashMap::new();
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let marker = format!("<{component_name}/>-children");
|
||||
let view_marker = quote! { .with_view_marker(#marker) };
|
||||
} else {
|
||||
let view_marker = quote! {};
|
||||
}
|
||||
}
|
||||
|
||||
let children = fragment_to_tokens(
|
||||
cx,
|
||||
span,
|
||||
&node.children,
|
||||
true,
|
||||
TagType::Unknown,
|
||||
Some(&mut slots),
|
||||
global_class,
|
||||
None,
|
||||
);
|
||||
|
||||
if let Some(children) = children {
|
||||
let bindables =
|
||||
items_to_bind.iter().map(|ident| quote! { #ident, });
|
||||
|
||||
let clonables = items_to_clone
|
||||
.iter()
|
||||
.map(|ident| quote! { let #ident = #ident.clone(); });
|
||||
|
||||
if bindables.len() > 0 {
|
||||
quote! {
|
||||
.children({
|
||||
#(#clonables)*
|
||||
|
||||
move |#cx, #(#bindables)*| #children #view_marker
|
||||
})
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.children({
|
||||
#(#clonables)*
|
||||
|
||||
Box::new(move |#cx| #children #view_marker)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
};
|
||||
|
||||
let slots = slots.drain().map(|(slot, values)| {
|
||||
let slot = Ident::new(&slot, span);
|
||||
if values.len() > 1 {
|
||||
quote! {
|
||||
.#slot(vec![
|
||||
#(#values)*
|
||||
])
|
||||
}
|
||||
} else {
|
||||
let value = &values[0];
|
||||
quote! { .#slot(#value) }
|
||||
}
|
||||
});
|
||||
|
||||
#[allow(unused_mut)] // used in debug
|
||||
let mut component = quote! {
|
||||
::leptos::component_view(
|
||||
&#name,
|
||||
#cx,
|
||||
::leptos::component_props_builder(&#name)
|
||||
#(#props)*
|
||||
#(#slots)*
|
||||
#children
|
||||
.build()
|
||||
)
|
||||
};
|
||||
|
||||
// (Temporarily?) removed
|
||||
// See note on the function itself below.
|
||||
/* #[cfg(debug_assertions)]
|
||||
IdeTagHelper::add_component_completion(cx, &mut component, node); */
|
||||
|
||||
if events.is_empty() {
|
||||
component
|
||||
} else {
|
||||
quote! {
|
||||
#component.into_view(#cx)
|
||||
#(#events)*
|
||||
}
|
||||
}
|
||||
}
|
||||
152
leptos_macro/src/view/ide_helper.rs
Normal file
152
leptos_macro/src/view/ide_helper.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use leptos_hot_reload::parsing::is_component_tag_name;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::quote;
|
||||
use rstml::node::{NodeElement, NodeName};
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
/// Helper type to emit semantic info about tags, for IDE.
|
||||
/// Implement `IntoIterator` with `Item="let _ = foo::docs;"`.
|
||||
///
|
||||
/// `IdeTagHelper` uses warning instead of errors everywhere,
|
||||
/// it's aim is to add usability, not introduce additional typecheck in `view`/`template` code.
|
||||
/// On stable `emit_warning` don't produce anything.
|
||||
pub(crate) struct IdeTagHelper(Vec<TokenStream>);
|
||||
|
||||
// TODO: Unhandled cases:
|
||||
// - svg::div, my_elements::foo - tags with custom paths, that doesnt look like component
|
||||
// - my_component::Foo - components with custom paths
|
||||
// - html:div - tags punctuated by `:`
|
||||
// - {div}, {"div"} - any rust expression
|
||||
impl IdeTagHelper {
|
||||
pub fn new() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
/// Save stmts for tag name.
|
||||
/// Emit warning if tag is component.
|
||||
pub fn save_tag_completion(&mut self, name: &NodeName) {
|
||||
let tag_name = name.to_string();
|
||||
if is_component_tag_name(&tag_name) {
|
||||
proc_macro_error::emit_warning!(
|
||||
name.span(),
|
||||
"BUG: Component tag is used in regular tag completion."
|
||||
);
|
||||
}
|
||||
for path in Self::completion_stmts(name) {
|
||||
self.0.push(quote! {
|
||||
let _ = #path;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Save stmts for open and close tags.
|
||||
/// Emit warning if tag is component.
|
||||
pub fn save_element_completion(&mut self, node: &NodeElement) {
|
||||
self.save_tag_completion(node.name());
|
||||
if let Some(close_tag) = node.close_tag.as_ref().map(|c| &c.name) {
|
||||
self.save_tag_completion(close_tag)
|
||||
}
|
||||
}
|
||||
|
||||
/* This has been (temporarily?) removed.
|
||||
* Its purpose was simply to add syntax highlighting and IDE hints for
|
||||
* component closing tags in debug mode by associating the closing tag
|
||||
* ident with the component function.
|
||||
*
|
||||
* Doing this in a way that correctly inferred types, however, required
|
||||
* duplicating the entire component constructor.
|
||||
*
|
||||
* In view trees with many nested components, this led to a massive explosion
|
||||
* in compile times.
|
||||
*
|
||||
* See https://github.com/leptos-rs/leptos/issues/1283
|
||||
*
|
||||
/// Add completion to the closing tag of the component.
|
||||
///
|
||||
/// In order to ensure that generics are passed through correctly in the
|
||||
/// current builder pattern, this clones the whole component constructor,
|
||||
/// but it will never be used.
|
||||
///
|
||||
/// ```no_build
|
||||
/// if false {
|
||||
/// close_tag(cx, unreachable!())
|
||||
/// }
|
||||
/// else {
|
||||
/// open_tag(open_tag.props().slots().children().build())
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn add_component_completion(
|
||||
cx: &Ident,
|
||||
component: &mut TokenStream,
|
||||
node: &NodeElement,
|
||||
) {
|
||||
// emit ide helper info
|
||||
if let Some(close_tag) = node.close_tag.as_ref().map(|c| &c.name) {
|
||||
*component = quote! {
|
||||
{
|
||||
let #close_tag = |cx| #component;
|
||||
#close_tag(#cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// Returns `syn::Path`-like `TokenStream` to the fn in docs.
|
||||
/// If tag name is `Component` returns `None`.
|
||||
fn create_regular_tag_fn_path(name: &Ident) -> TokenStream {
|
||||
let tag_name = name.to_string();
|
||||
let namespace = if crate::view::is_svg_element(&tag_name) {
|
||||
quote! { ::leptos::leptos_dom::svg }
|
||||
} else if crate::view::is_math_ml_element(&tag_name) {
|
||||
quote! { ::leptos::leptos_dom::math }
|
||||
} else {
|
||||
// todo: check is html, and emit_warning in case of custom tag
|
||||
quote! { ::leptos::leptos_dom::html }
|
||||
};
|
||||
quote!( #namespace::#name)
|
||||
}
|
||||
|
||||
/// Returns `syn::Path`-like `TokenStream` to the `custom` section in docs.
|
||||
fn create_custom_tag_fn_path(span: Span) -> TokenStream {
|
||||
let custom_ident = Ident::new("custom", span);
|
||||
quote! {leptos::leptos_dom::html::#custom_ident::<leptos::leptos_dom::html::Custom>}
|
||||
}
|
||||
|
||||
// Extract from NodeName completion idents.
|
||||
// Custom tags (like foo-bar-baz) is mapped
|
||||
// to vec!["custom", "custom",.. ] for each token in tag, even for "-".
|
||||
// Only last ident from `Path` is used.
|
||||
fn completion_stmts(name: &NodeName) -> Vec<TokenStream> {
|
||||
match name {
|
||||
NodeName::Block(_) => vec![],
|
||||
NodeName::Punctuated(c) => c
|
||||
.pairs()
|
||||
.flat_map(|c| {
|
||||
let mut idents =
|
||||
vec![Self::create_custom_tag_fn_path(c.value().span())];
|
||||
if let Some(p) = c.punct() {
|
||||
idents.push(Self::create_custom_tag_fn_path(p.span()))
|
||||
}
|
||||
idents
|
||||
})
|
||||
.collect(),
|
||||
NodeName::Path(e) => e
|
||||
.path
|
||||
.segments
|
||||
.last()
|
||||
.map(|p| &p.ident)
|
||||
.map(Self::create_regular_tag_fn_path)
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for IdeTagHelper {
|
||||
type Item = TokenStream;
|
||||
type IntoIter = <Vec<TokenStream> as IntoIterator>::IntoIter;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
548
leptos_macro/src/view/mod.rs
Normal file
548
leptos_macro/src/view/mod.rs
Normal file
@@ -0,0 +1,548 @@
|
||||
use crate::{attribute_value, Mode};
|
||||
use convert_case::{Case::Snake, Casing};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::{quote, quote_spanned};
|
||||
use rstml::node::{KeyedAttribute, Node, NodeElement, NodeName};
|
||||
use syn::{spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprPath, Lit};
|
||||
|
||||
pub mod client_builder;
|
||||
pub mod client_template;
|
||||
pub mod component_builder;
|
||||
pub mod ide_helper;
|
||||
pub mod server_template;
|
||||
pub mod slot_helper;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(crate) use ide_helper::*;
|
||||
|
||||
pub(crate) fn render_view(
|
||||
cx: &Ident,
|
||||
nodes: &[Node],
|
||||
mode: Mode,
|
||||
global_class: Option<&TokenTree>,
|
||||
call_site: Option<String>,
|
||||
) -> TokenStream {
|
||||
let empty = {
|
||||
let span = Span::call_site();
|
||||
quote_spanned! {
|
||||
span => leptos::leptos_dom::Unit
|
||||
}
|
||||
};
|
||||
|
||||
if mode == Mode::Ssr {
|
||||
match nodes.len() {
|
||||
0 => empty,
|
||||
1 => server_template::root_node_to_tokens_ssr(
|
||||
cx,
|
||||
&nodes[0],
|
||||
global_class,
|
||||
call_site,
|
||||
),
|
||||
_ => server_template::fragment_to_tokens_ssr(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
nodes,
|
||||
global_class,
|
||||
call_site,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
match nodes.len() {
|
||||
0 => empty,
|
||||
1 => client_builder::node_to_tokens(
|
||||
cx,
|
||||
&nodes[0],
|
||||
client_builder::TagType::Unknown,
|
||||
None,
|
||||
global_class,
|
||||
call_site,
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
_ => client_builder::fragment_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
nodes,
|
||||
true,
|
||||
client_builder::TagType::Unknown,
|
||||
None,
|
||||
global_class,
|
||||
call_site,
|
||||
)
|
||||
.unwrap_or(empty),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep list alphabetized for binary search
|
||||
const TYPED_EVENTS: [&str; 126] = [
|
||||
"DOMContentLoaded",
|
||||
"abort",
|
||||
"afterprint",
|
||||
"animationcancel",
|
||||
"animationend",
|
||||
"animationiteration",
|
||||
"animationstart",
|
||||
"auxclick",
|
||||
"beforeinput",
|
||||
"beforeprint",
|
||||
"beforeunload",
|
||||
"blur",
|
||||
"canplay",
|
||||
"canplaythrough",
|
||||
"change",
|
||||
"click",
|
||||
"close",
|
||||
"compositionend",
|
||||
"compositionstart",
|
||||
"compositionupdate",
|
||||
"contextmenu",
|
||||
"copy",
|
||||
"cuechange",
|
||||
"cut",
|
||||
"dblclick",
|
||||
"devicemotion",
|
||||
"deviceorientation",
|
||||
"drag",
|
||||
"dragend",
|
||||
"dragenter",
|
||||
"dragleave",
|
||||
"dragover",
|
||||
"dragstart",
|
||||
"drop",
|
||||
"durationchange",
|
||||
"emptied",
|
||||
"ended",
|
||||
"error",
|
||||
"focus",
|
||||
"focusin",
|
||||
"focusout",
|
||||
"formdata",
|
||||
"fullscreenchange",
|
||||
"fullscreenerror",
|
||||
"gamepadconnected",
|
||||
"gamepaddisconnected",
|
||||
"gotpointercapture",
|
||||
"hashchange",
|
||||
"input",
|
||||
"invalid",
|
||||
"keydown",
|
||||
"keypress",
|
||||
"keyup",
|
||||
"languagechange",
|
||||
"load",
|
||||
"loadeddata",
|
||||
"loadedmetadata",
|
||||
"loadstart",
|
||||
"lostpointercapture",
|
||||
"message",
|
||||
"messageerror",
|
||||
"mousedown",
|
||||
"mouseenter",
|
||||
"mouseleave",
|
||||
"mousemove",
|
||||
"mouseout",
|
||||
"mouseover",
|
||||
"mouseup",
|
||||
"offline",
|
||||
"online",
|
||||
"orientationchange",
|
||||
"pagehide",
|
||||
"pageshow",
|
||||
"paste",
|
||||
"pause",
|
||||
"play",
|
||||
"playing",
|
||||
"pointercancel",
|
||||
"pointerdown",
|
||||
"pointerenter",
|
||||
"pointerleave",
|
||||
"pointerlockchange",
|
||||
"pointerlockerror",
|
||||
"pointermove",
|
||||
"pointerout",
|
||||
"pointerover",
|
||||
"pointerup",
|
||||
"popstate",
|
||||
"progress",
|
||||
"ratechange",
|
||||
"readystatechange",
|
||||
"rejectionhandled",
|
||||
"reset",
|
||||
"resize",
|
||||
"scroll",
|
||||
"securitypolicyviolation",
|
||||
"seeked",
|
||||
"seeking",
|
||||
"select",
|
||||
"selectionchange",
|
||||
"selectstart",
|
||||
"slotchange",
|
||||
"stalled",
|
||||
"storage",
|
||||
"submit",
|
||||
"suspend",
|
||||
"timeupdate",
|
||||
"toggle",
|
||||
"touchcancel",
|
||||
"touchend",
|
||||
"touchmove",
|
||||
"touchstart",
|
||||
"transitioncancel",
|
||||
"transitionend",
|
||||
"transitionrun",
|
||||
"transitionstart",
|
||||
"unhandledrejection",
|
||||
"unload",
|
||||
"visibilitychange",
|
||||
"volumechange",
|
||||
"waiting",
|
||||
"webkitanimationend",
|
||||
"webkitanimationiteration",
|
||||
"webkitanimationstart",
|
||||
"webkittransitionend",
|
||||
"wheel",
|
||||
];
|
||||
|
||||
const CUSTOM_EVENT: &str = "Custom";
|
||||
|
||||
pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
|
||||
let (name, is_force_undelegated) = parse_event(name);
|
||||
|
||||
let (event_type, is_custom) = TYPED_EVENTS
|
||||
.binary_search(&name)
|
||||
.map(|_| (name, false))
|
||||
.unwrap_or((CUSTOM_EVENT, true));
|
||||
|
||||
let Ok(event_type) = event_type.parse::<TokenStream>() else {
|
||||
abort!(event_type, "couldn't parse event name");
|
||||
};
|
||||
|
||||
let event_type = if is_custom {
|
||||
quote! { Custom::new(#name) }
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
(event_type, is_custom, is_force_undelegated)
|
||||
}
|
||||
|
||||
fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
|
||||
match expr {
|
||||
syn::Expr::Block(block) => block.block.stmts.last().and_then(|stmt| {
|
||||
if let syn::Stmt::Expr(expr, ..) = stmt {
|
||||
expr_to_ident(expr)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
syn::Expr::Path(path) => Some(path),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_to_snake_case(name: String) -> String {
|
||||
if !name.is_case(Snake) {
|
||||
name.to_case(Snake)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
fn is_custom_element(tag: &str) -> bool {
|
||||
tag.contains('-')
|
||||
}
|
||||
|
||||
fn is_self_closing(node: &NodeElement) -> bool {
|
||||
// self-closing tags
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
|
||||
[
|
||||
"area", "base", "br", "col", "embed", "hr", "img", "input", "link",
|
||||
"meta", "param", "source", "track", "wbr",
|
||||
]
|
||||
.binary_search(&node.name().to_string().as_str())
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn camel_case_tag_name(tag_name: &str) -> String {
|
||||
let mut chars = tag_name.chars();
|
||||
let first = chars.next();
|
||||
let underscore = if tag_name == "option" { "_" } else { "" };
|
||||
first
|
||||
.map(|f| f.to_ascii_uppercase())
|
||||
.into_iter()
|
||||
.chain(chars)
|
||||
.collect::<String>()
|
||||
+ underscore
|
||||
}
|
||||
|
||||
fn is_svg_element(tag: &str) -> bool {
|
||||
// Keep list alphabetized for binary search
|
||||
[
|
||||
"animate",
|
||||
"animateMotion",
|
||||
"animateTransform",
|
||||
"circle",
|
||||
"clipPath",
|
||||
"defs",
|
||||
"desc",
|
||||
"discard",
|
||||
"ellipse",
|
||||
"feBlend",
|
||||
"feColorMatrix",
|
||||
"feComponentTransfer",
|
||||
"feComposite",
|
||||
"feConvolveMatrix",
|
||||
"feDiffuseLighting",
|
||||
"feDisplacementMap",
|
||||
"feDistantLight",
|
||||
"feDropShadow",
|
||||
"feFlood",
|
||||
"feFuncA",
|
||||
"feFuncB",
|
||||
"feFuncG",
|
||||
"feFuncR",
|
||||
"feGaussianBlur",
|
||||
"feImage",
|
||||
"feMerge",
|
||||
"feMergeNode",
|
||||
"feMorphology",
|
||||
"feOffset",
|
||||
"fePointLight",
|
||||
"feSpecularLighting",
|
||||
"feSpotLight",
|
||||
"feTile",
|
||||
"feTurbulence",
|
||||
"filter",
|
||||
"foreignObject",
|
||||
"g",
|
||||
"hatch",
|
||||
"hatchpath",
|
||||
"image",
|
||||
"line",
|
||||
"linearGradient",
|
||||
"marker",
|
||||
"mask",
|
||||
"metadata",
|
||||
"mpath",
|
||||
"path",
|
||||
"pattern",
|
||||
"polygon",
|
||||
"polyline",
|
||||
"radialGradient",
|
||||
"rect",
|
||||
"set",
|
||||
"stop",
|
||||
"svg",
|
||||
"switch",
|
||||
"symbol",
|
||||
"text",
|
||||
"textPath",
|
||||
"tspan",
|
||||
"use",
|
||||
"use_",
|
||||
"view",
|
||||
]
|
||||
.binary_search(&tag)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn is_math_ml_element(tag: &str) -> bool {
|
||||
// Keep list alphabetized for binary search
|
||||
[
|
||||
"annotation",
|
||||
"maction",
|
||||
"math",
|
||||
"menclose",
|
||||
"merror",
|
||||
"mfenced",
|
||||
"mfrac",
|
||||
"mi",
|
||||
"mmultiscripts",
|
||||
"mn",
|
||||
"mo",
|
||||
"mover",
|
||||
"mpadded",
|
||||
"mphantom",
|
||||
"mprescripts",
|
||||
"mroot",
|
||||
"mrow",
|
||||
"ms",
|
||||
"mspace",
|
||||
"msqrt",
|
||||
"mstyle",
|
||||
"msub",
|
||||
"msubsup",
|
||||
"msup",
|
||||
"mtable",
|
||||
"mtd",
|
||||
"mtext",
|
||||
"mtr",
|
||||
"munder",
|
||||
"munderover",
|
||||
"semantics",
|
||||
]
|
||||
.binary_search(&tag)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn is_ambiguous_element(tag: &str) -> bool {
|
||||
tag == "a" || tag == "script" || tag == "title"
|
||||
}
|
||||
|
||||
fn parse_event(event_name: &str) -> (&str, bool) {
|
||||
if let Some(event_name) = event_name.strip_suffix(":undelegated") {
|
||||
(event_name, true)
|
||||
} else {
|
||||
(event_name, false)
|
||||
}
|
||||
}
|
||||
|
||||
fn fancy_class_name<'a>(
|
||||
name: &str,
|
||||
cx: &Ident,
|
||||
node: &'a KeyedAttribute,
|
||||
) -> Option<(TokenStream, String, &'a Expr)> {
|
||||
// special case for complex class names:
|
||||
// e.g., Tailwind `class=("mt-[calc(100vh_-_3rem)]", true)`
|
||||
if name == "class" {
|
||||
if let Some(Tuple(tuple)) = node.value() {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let class = quote_spanned! {
|
||||
span => .class
|
||||
};
|
||||
let class_name = &tuple.elems[0];
|
||||
let class_name = if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
..
|
||||
}) = class_name
|
||||
{
|
||||
s.value()
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
class_name.span(),
|
||||
"class name must be a string literal"
|
||||
);
|
||||
Default::default()
|
||||
};
|
||||
let value = &tuple.elems[1];
|
||||
return Some((
|
||||
quote! {
|
||||
#class(#class_name, (#cx, #value))
|
||||
},
|
||||
class_name,
|
||||
value,
|
||||
));
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
tuple.span(),
|
||||
"class tuples must have two elements."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
|
||||
match tag_name {
|
||||
NodeName::Path(path) => path
|
||||
.path
|
||||
.segments
|
||||
.iter()
|
||||
.last()
|
||||
.map(|segment| segment.ident.clone())
|
||||
.expect("element needs to have a name"),
|
||||
NodeName::Block(_) => {
|
||||
let span = tag_name.span();
|
||||
proc_macro_error::emit_error!(
|
||||
span,
|
||||
"blocks not allowed in tag-name position"
|
||||
);
|
||||
Ident::new("", span)
|
||||
}
|
||||
_ => Ident::new(
|
||||
&tag_name.to_string().replace(['-', ':'], "_"),
|
||||
tag_name.span(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn fancy_style_name<'a>(
|
||||
name: &str,
|
||||
cx: &Ident,
|
||||
node: &'a KeyedAttribute,
|
||||
) -> Option<(TokenStream, String, &'a Expr)> {
|
||||
// special case for complex dynamic style names:
|
||||
if name == "style" {
|
||||
if let Some(Tuple(tuple)) = node.value() {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let style = quote_spanned! {
|
||||
span => .style
|
||||
};
|
||||
let style_name = &tuple.elems[0];
|
||||
let style_name = if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
..
|
||||
}) = style_name
|
||||
{
|
||||
s.value()
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
style_name.span(),
|
||||
"style name must be a string literal"
|
||||
);
|
||||
Default::default()
|
||||
};
|
||||
let value = &tuple.elems[1];
|
||||
return Some((
|
||||
quote! {
|
||||
#style(#style_name, (#cx, #value))
|
||||
},
|
||||
style_name,
|
||||
value,
|
||||
));
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
tuple.span(),
|
||||
"style tuples must have two elements."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn event_from_attribute_node(
|
||||
attr: &KeyedAttribute,
|
||||
force_undelegated: bool,
|
||||
) -> (TokenStream, &Expr) {
|
||||
let event_name = attr
|
||||
.key
|
||||
.to_string()
|
||||
.strip_prefix("on:")
|
||||
.expect("expected `on:` directive")
|
||||
.to_owned();
|
||||
|
||||
let handler = attribute_value(attr);
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let (name, name_undelegated) = parse_event(&event_name);
|
||||
|
||||
let event_type = TYPED_EVENTS
|
||||
.binary_search(&name)
|
||||
.map(|_| (name))
|
||||
.unwrap_or(CUSTOM_EVENT);
|
||||
|
||||
let Ok(event_type) = event_type.parse::<TokenStream>() else {
|
||||
abort!(attr.key, "couldn't parse event name");
|
||||
};
|
||||
|
||||
let event_type = if force_undelegated || name_undelegated {
|
||||
quote! { ::leptos::leptos_dom::ev::undelegated(::leptos::leptos_dom::ev::#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::leptos_dom::ev::#event_type }
|
||||
};
|
||||
(event_type, handler)
|
||||
}
|
||||
707
leptos_macro/src/view/server_template.rs
Normal file
707
leptos_macro/src/view/server_template.rs
Normal file
@@ -0,0 +1,707 @@
|
||||
use super::{
|
||||
camel_case_tag_name,
|
||||
component_builder::component_to_tokens,
|
||||
fancy_class_name, fancy_style_name,
|
||||
ide_helper::IdeTagHelper,
|
||||
is_custom_element, is_math_ml_element, is_self_closing, is_svg_element,
|
||||
parse_event_name,
|
||||
slot_helper::{get_slot, slot_to_tokens},
|
||||
};
|
||||
use crate::attribute_value;
|
||||
use leptos_hot_reload::parsing::{
|
||||
block_to_primitive_expression, is_component_node, value_to_string,
|
||||
};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::quote;
|
||||
use rstml::node::{
|
||||
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
pub(crate) enum SsrElementChunks {
|
||||
String {
|
||||
template: String,
|
||||
holes: Vec<TokenStream>,
|
||||
},
|
||||
View(TokenStream),
|
||||
}
|
||||
|
||||
pub(crate) fn root_node_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &Node,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
match node {
|
||||
Node::Fragment(fragment) => fragment_to_tokens_ssr(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => quote! {},
|
||||
Node::Text(node) => {
|
||||
quote! {
|
||||
leptos::leptos_dom::html::text(#node)
|
||||
}
|
||||
}
|
||||
Node::RawText(r) => {
|
||||
let text = r.to_string_best();
|
||||
let text = syn::LitStr::new(&text, r.span());
|
||||
quote! {
|
||||
leptos::leptos_dom::html::text(#text)
|
||||
}
|
||||
}
|
||||
Node::Block(node) => {
|
||||
quote! {
|
||||
#node
|
||||
}
|
||||
}
|
||||
Node::Element(node) => {
|
||||
root_element_to_tokens_ssr(cx, node, global_class, view_marker)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fragment_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
_span: Span,
|
||||
nodes: &[Node],
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let nodes = nodes.iter().map(|node| {
|
||||
let node = root_node_to_tokens_ssr(cx, node, global_class, None);
|
||||
quote! {
|
||||
#node.into_view(#cx)
|
||||
}
|
||||
});
|
||||
quote! {
|
||||
{
|
||||
leptos::Fragment::lazy(|| [
|
||||
#(#nodes),*
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn root_element_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> Option<TokenStream> {
|
||||
// TODO: simplify, this is checked twice, second time in `element_to_tokens_ssr` body
|
||||
if is_component_node(node) {
|
||||
if let Some(slot) = get_slot(node) {
|
||||
slot_to_tokens(cx, node, slot, None, global_class);
|
||||
None
|
||||
} else {
|
||||
Some(component_to_tokens(cx, node, global_class))
|
||||
}
|
||||
} else {
|
||||
let mut stmts_for_ide = IdeTagHelper::new();
|
||||
let mut exprs_for_compiler = Vec::<TokenStream>::new();
|
||||
|
||||
let mut template = String::new();
|
||||
let mut holes = Vec::new();
|
||||
let mut chunks = Vec::new();
|
||||
element_to_tokens_ssr(
|
||||
cx,
|
||||
node,
|
||||
None,
|
||||
&mut template,
|
||||
&mut holes,
|
||||
&mut chunks,
|
||||
&mut stmts_for_ide,
|
||||
&mut exprs_for_compiler,
|
||||
true,
|
||||
global_class,
|
||||
);
|
||||
|
||||
// push final chunk
|
||||
if !template.is_empty() {
|
||||
chunks.push(SsrElementChunks::String { template, holes })
|
||||
}
|
||||
|
||||
let chunks = chunks.into_iter().map(|chunk| match chunk {
|
||||
SsrElementChunks::String { template, holes } => {
|
||||
if holes.is_empty() {
|
||||
let template = template.replace("\\{", "{").replace("\\}", "}");
|
||||
quote! {
|
||||
leptos::leptos_dom::html::StringOrView::String(#template.into())
|
||||
}
|
||||
} else {
|
||||
let template = template.replace("\\{", "{{").replace("\\}", "}}");
|
||||
quote! {
|
||||
leptos::leptos_dom::html::StringOrView::String(
|
||||
format!(
|
||||
#template,
|
||||
#(#holes),*
|
||||
)
|
||||
.into()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SsrElementChunks::View(view) => {
|
||||
quote! {
|
||||
#[allow(unused_braces)]
|
||||
{
|
||||
let view = #view;
|
||||
leptos::leptos_dom::html::StringOrView::View(std::rc::Rc::new(move || view.clone()))
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let tag_name = node.name().to_string();
|
||||
let is_custom_element = is_custom_element(&tag_name);
|
||||
|
||||
// Use any other span instead of node.name.span(), to avoid misunderstanding in IDE.
|
||||
// We can use open_tag.span(), to provide similar (to name span) diagnostic
|
||||
// in case of expansion error, but it will also highlight "<" token.
|
||||
let typed_element_name = if is_custom_element {
|
||||
Ident::new("Custom", Span::call_site())
|
||||
} else {
|
||||
let camel_cased = camel_case_tag_name(
|
||||
tag_name
|
||||
.trim_start_matches("svg::")
|
||||
.trim_start_matches("math::")
|
||||
.trim_end_matches('_'),
|
||||
);
|
||||
Ident::new(&camel_cased, Span::call_site())
|
||||
};
|
||||
let typed_element_name = if is_svg_element(&tag_name) {
|
||||
quote! { svg::#typed_element_name }
|
||||
} else if is_math_ml_element(&tag_name) {
|
||||
quote! { math::#typed_element_name }
|
||||
} else {
|
||||
quote! { html::#typed_element_name }
|
||||
};
|
||||
let full_name = if is_custom_element {
|
||||
quote! {
|
||||
::leptos::leptos_dom::html::Custom::new(#tag_name)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
::leptos::leptos_dom::#typed_element_name::default()
|
||||
}
|
||||
};
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let stmts_for_ide = stmts_for_ide.into_iter();
|
||||
Some(quote! {
|
||||
{
|
||||
#(#stmts_for_ide)*
|
||||
#(#exprs_for_compiler)*
|
||||
::leptos::HtmlElement::from_chunks(#cx, #full_name, [#(#chunks),*])#view_marker
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn element_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
chunks: &mut Vec<SsrElementChunks>,
|
||||
stmts_for_ide: &mut IdeTagHelper,
|
||||
exprs_for_compiler: &mut Vec<TokenStream>,
|
||||
is_root: bool,
|
||||
global_class: Option<&TokenTree>,
|
||||
) {
|
||||
if is_component_node(node) {
|
||||
if let Some(slot) = get_slot(node) {
|
||||
slot_to_tokens(cx, node, slot, parent_slots, global_class);
|
||||
return;
|
||||
}
|
||||
|
||||
let component = component_to_tokens(cx, node, global_class);
|
||||
|
||||
if !template.is_empty() {
|
||||
chunks.push(SsrElementChunks::String {
|
||||
template: std::mem::take(template),
|
||||
holes: std::mem::take(holes),
|
||||
})
|
||||
}
|
||||
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#component}.into_view(#cx)
|
||||
}));
|
||||
} else {
|
||||
let tag_name = node.name().to_string();
|
||||
let tag_name = tag_name
|
||||
.trim_start_matches("svg::")
|
||||
.trim_start_matches("math::")
|
||||
.trim_end_matches('_');
|
||||
let is_script_or_style = tag_name == "script" || tag_name == "style";
|
||||
template.push('<');
|
||||
template.push_str(tag_name);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
stmts_for_ide.save_element_completion(node);
|
||||
|
||||
let mut inner_html = None;
|
||||
|
||||
for attr in node.attributes() {
|
||||
if let NodeAttribute::Attribute(attr) = attr {
|
||||
inner_html = attribute_to_tokens_ssr(
|
||||
cx,
|
||||
attr,
|
||||
template,
|
||||
holes,
|
||||
exprs_for_compiler,
|
||||
global_class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// insert hydration ID
|
||||
let hydration_id = if is_root {
|
||||
quote! { ::leptos::leptos_dom::HydrationCtx::peek() }
|
||||
} else {
|
||||
quote! { ::leptos::leptos_dom::HydrationCtx::id() }
|
||||
};
|
||||
match node
|
||||
.attributes()
|
||||
.iter()
|
||||
.find(|node| matches!(node, NodeAttribute::Attribute(attr) if attr.key.to_string() == "id"))
|
||||
{
|
||||
Some(_) => {
|
||||
template.push_str(" leptos-hk=\"_{}\"");
|
||||
}
|
||||
None => {
|
||||
template.push_str(" id=\"_{}\"");
|
||||
}
|
||||
}
|
||||
holes.push(hydration_id);
|
||||
|
||||
set_class_attribute_ssr(cx, node, template, holes, global_class);
|
||||
set_style_attribute_ssr(cx, node, template, holes);
|
||||
|
||||
if is_self_closing(node) {
|
||||
template.push_str("/>");
|
||||
} else {
|
||||
template.push('>');
|
||||
|
||||
if let Some(inner_html) = inner_html {
|
||||
template.push_str("{}");
|
||||
let value = inner_html;
|
||||
|
||||
holes.push(quote! {
|
||||
(#value).into_attribute(#cx).as_nameless_value_string().unwrap_or_default()
|
||||
})
|
||||
} else {
|
||||
for child in &node.children {
|
||||
match child {
|
||||
Node::Element(child) => {
|
||||
element_to_tokens_ssr(
|
||||
cx,
|
||||
child,
|
||||
None,
|
||||
template,
|
||||
holes,
|
||||
chunks,
|
||||
stmts_for_ide,
|
||||
exprs_for_compiler,
|
||||
false,
|
||||
global_class,
|
||||
);
|
||||
}
|
||||
Node::Text(text) => {
|
||||
let value = text.value_string();
|
||||
let value = if is_script_or_style {
|
||||
value.into()
|
||||
} else {
|
||||
html_escape::encode_safe(&value)
|
||||
};
|
||||
template.push_str(
|
||||
&value.replace('{', "\\{").replace('}', "\\}"),
|
||||
);
|
||||
}
|
||||
Node::RawText(r) => {
|
||||
let value = r.to_string_best();
|
||||
let value = if is_script_or_style {
|
||||
value.into()
|
||||
} else {
|
||||
html_escape::encode_safe(&value)
|
||||
};
|
||||
template.push_str(
|
||||
&value.replace('{', "\\{").replace('}', "\\}"),
|
||||
);
|
||||
}
|
||||
Node::Block(NodeBlock::ValidBlock(block)) => {
|
||||
if let Some(value) =
|
||||
block_to_primitive_expression(block)
|
||||
.and_then(value_to_string)
|
||||
{
|
||||
template.push_str(&value);
|
||||
} else {
|
||||
if !template.is_empty() {
|
||||
chunks.push(SsrElementChunks::String {
|
||||
template: std::mem::take(template),
|
||||
holes: std::mem::take(holes),
|
||||
})
|
||||
}
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#block}.into_view(#cx)
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Keep invalid blocks for faster IDE diff (on user type)
|
||||
Node::Block(block @ NodeBlock::Invalid { .. }) => {
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#block}.into_view(#cx)
|
||||
}));
|
||||
}
|
||||
Node::Fragment(_) => abort!(
|
||||
Span::call_site(),
|
||||
"You can't nest a fragment inside an element."
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template.push_str("</");
|
||||
template.push_str(tag_name);
|
||||
template.push('>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns `inner_html`
|
||||
fn attribute_to_tokens_ssr<'a>(
|
||||
cx: &Ident,
|
||||
attr: &'a KeyedAttribute,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
exprs_for_compiler: &mut Vec<TokenStream>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> Option<&'a syn::Expr> {
|
||||
let name = attr.key.to_string();
|
||||
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
|
||||
// ignore refs on SSR
|
||||
} else if let Some(name) = name.strip_prefix("on:") {
|
||||
let handler = attribute_value(attr);
|
||||
let (event_type, _, _) = parse_event_name(name);
|
||||
|
||||
exprs_for_compiler.push(quote! {
|
||||
leptos::leptos_dom::helpers::ssr_event_listener(::leptos::ev::#event_type, #handler);
|
||||
})
|
||||
} else if name.strip_prefix("prop:").is_some()
|
||||
|| name.strip_prefix("class:").is_some()
|
||||
|| name.strip_prefix("style:").is_some()
|
||||
{
|
||||
// ignore props for SSR
|
||||
// ignore classes and sdtyles: we'll handle these separately
|
||||
if name.starts_with("prop:") {
|
||||
let value = attr.value();
|
||||
exprs_for_compiler.push(quote! {
|
||||
#[allow(unused_braces)]
|
||||
{ _ = #value; }
|
||||
});
|
||||
}
|
||||
} else if name == "inner_html" {
|
||||
return attr.value();
|
||||
} else {
|
||||
let name = name.replacen("attr:", "", 1);
|
||||
|
||||
// special case of global_class and class attribute
|
||||
if name == "class"
|
||||
&& global_class.is_some()
|
||||
&& attr.value().and_then(value_to_string).is_none()
|
||||
{
|
||||
let span = attr.key.span();
|
||||
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
|
||||
and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \
|
||||
toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \
|
||||
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
|
||||
};
|
||||
|
||||
if name != "class" && name != "style" {
|
||||
template.push(' ');
|
||||
|
||||
if let Some(value) = attr.value() {
|
||||
if let Some(value) = value_to_string(value) {
|
||||
template.push_str(&name);
|
||||
template.push_str("=\"");
|
||||
template.push_str(&html_escape::encode_quoted_attribute(
|
||||
&value,
|
||||
));
|
||||
template.push('"');
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
holes.push(quote! {
|
||||
&{#value}.into_attribute(#cx)
|
||||
.as_nameless_value_string()
|
||||
.map(|a| format!("{}=\"{}\"", #name, leptos::leptos_dom::ssr::escape_attr(&a)))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
template.push_str(&name);
|
||||
}
|
||||
}
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn set_class_attribute_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) {
|
||||
let (static_global_class, dyn_global_class) = match global_class {
|
||||
Some(TokenTree::Literal(lit)) => {
|
||||
let str = lit.to_string();
|
||||
// A lit here can be a string, byte_string, char, byte_char, int or float.
|
||||
// If it's a string we remove the quotes so folks can use them directly
|
||||
// without needing braces. E.g. view!{cx, class="my-class", ... }
|
||||
let str = if str.starts_with('"') && str.ends_with('"') {
|
||||
str[1..str.len() - 1].to_string()
|
||||
} else {
|
||||
str
|
||||
};
|
||||
(str, None)
|
||||
}
|
||||
None => (String::new(), None),
|
||||
Some(val) => (String::new(), Some(val)),
|
||||
};
|
||||
let static_class_attr = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| match a {
|
||||
NodeAttribute::Attribute(attr)
|
||||
if attr.key.to_string() == "class" =>
|
||||
{
|
||||
attr.value().and_then(value_to_string)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.chain(Some(static_global_class))
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
let dyn_class_attr = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
if let NodeAttribute::Attribute(a) = a {
|
||||
if a.key.to_string() == "class" {
|
||||
if a.value().and_then(value_to_string).is_some()
|
||||
|| fancy_class_name(&a.key.to_string(), cx, a).is_some()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some((a.key.span(), a.value()))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let class_attrs = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if name == "class" {
|
||||
return if let Some((_, name, value)) =
|
||||
fancy_class_name(&name, cx, node)
|
||||
{
|
||||
let span = node.key.span();
|
||||
Some((span, name, value))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
if name.starts_with("class:") || name.starts_with("class-") {
|
||||
let name = if name.starts_with("class:") {
|
||||
name.replacen("class:", "", 1)
|
||||
} else if name.starts_with("class-") {
|
||||
name.replacen("class-", "", 1)
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let value = attribute_value(node);
|
||||
let span = node.key.span();
|
||||
Some((span, name, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !static_class_attr.is_empty()
|
||||
|| !dyn_class_attr.is_empty()
|
||||
|| !class_attrs.is_empty()
|
||||
|| dyn_global_class.is_some()
|
||||
{
|
||||
template.push_str(" class=\"");
|
||||
|
||||
template.push_str(&html_escape::encode_quoted_attribute(
|
||||
&static_class_attr,
|
||||
));
|
||||
|
||||
for (_span, value) in dyn_class_attr {
|
||||
if let Some(value) = value {
|
||||
template.push_str(" {}");
|
||||
holes.push(quote! {
|
||||
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (_span, name, value) in &class_attrs {
|
||||
template.push_str(" {}");
|
||||
holes.push(quote! {
|
||||
(#cx, #value).into_class(#cx).as_value_string(#name)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(dyn_global_class) = dyn_global_class {
|
||||
template.push_str(" {}");
|
||||
holes.push(quote! { #dyn_global_class });
|
||||
}
|
||||
|
||||
template.push('"');
|
||||
}
|
||||
}
|
||||
|
||||
fn set_style_attribute_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
) {
|
||||
let static_style_attr = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| match a {
|
||||
NodeAttribute::Attribute(attr)
|
||||
if attr.key.to_string() == "style" =>
|
||||
{
|
||||
attr.value().and_then(value_to_string)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.next()
|
||||
.map(|style| format!("{style};"));
|
||||
|
||||
let dyn_style_attr = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
if let NodeAttribute::Attribute(a) = a {
|
||||
if a.key.to_string() == "style" {
|
||||
if a.value().and_then(value_to_string).is_some()
|
||||
|| fancy_style_name(&a.key.to_string(), cx, a).is_some()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some((a.key.span(), a.value()))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let style_attrs = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if name == "style" {
|
||||
return if let Some((_, name, value)) =
|
||||
fancy_style_name(&name, cx, node)
|
||||
{
|
||||
let span = node.key.span();
|
||||
Some((span, name, value))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
if name.starts_with("style:") || name.starts_with("style-") {
|
||||
let name = if name.starts_with("style:") {
|
||||
name.replacen("style:", "", 1)
|
||||
} else if name.starts_with("style-") {
|
||||
name.replacen("style-", "", 1)
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let value = attribute_value(node);
|
||||
let span = node.key.span();
|
||||
Some((span, name, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if static_style_attr.is_some()
|
||||
|| !dyn_style_attr.is_empty()
|
||||
|| !style_attrs.is_empty()
|
||||
{
|
||||
template.push_str(" style=\"");
|
||||
|
||||
template.push_str(&static_style_attr.unwrap_or_default());
|
||||
|
||||
for (_span, value) in dyn_style_attr {
|
||||
if let Some(value) = value {
|
||||
template.push_str(" {};");
|
||||
holes.push(quote! {
|
||||
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (_span, name, value) in &style_attrs {
|
||||
template.push_str(" {}");
|
||||
holes.push(quote! {
|
||||
(#cx, #value).into_style(#cx).as_value_string(#name).unwrap_or_default()
|
||||
});
|
||||
}
|
||||
|
||||
template.push('"');
|
||||
}
|
||||
}
|
||||
191
leptos_macro/src/view/slot_helper.rs
Normal file
191
leptos_macro/src/view/slot_helper.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use super::{
|
||||
client_builder::{fragment_to_tokens, TagType},
|
||||
convert_to_snake_case, ident_from_tag_name,
|
||||
};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote};
|
||||
use rstml::node::{KeyedAttribute, NodeAttribute, NodeElement};
|
||||
use std::collections::HashMap;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
pub(crate) fn slot_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
slot: &KeyedAttribute,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) {
|
||||
let name = slot.key.to_string();
|
||||
let name = name.trim();
|
||||
let name = convert_to_snake_case(if name.starts_with("slot:") {
|
||||
name.replacen("slot:", "", 1)
|
||||
} else {
|
||||
node.name().to_string()
|
||||
});
|
||||
|
||||
let component_name = ident_from_tag_name(node.name());
|
||||
let span = node.name().span();
|
||||
|
||||
let Some(parent_slots) = parent_slots else {
|
||||
proc_macro_error::emit_error!(
|
||||
span,
|
||||
"slots cannot be used inside HTML elements"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
if is_slot(node) {
|
||||
None
|
||||
} else {
|
||||
Some(node)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let props = attrs
|
||||
.clone()
|
||||
.filter(|attr| {
|
||||
!attr.key.to_string().starts_with("bind:")
|
||||
&& !attr.key.to_string().starts_with("clone:")
|
||||
})
|
||||
.map(|attr| {
|
||||
let name = &attr.key;
|
||||
|
||||
let value = attr
|
||||
.value()
|
||||
.map(|v| {
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
|
||||
quote! {
|
||||
.#name(#[allow(unused_braces)] #value)
|
||||
}
|
||||
});
|
||||
|
||||
let items_to_bind = attrs
|
||||
.clone()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("bind:")
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let items_to_clone = attrs
|
||||
.clone()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("clone:")
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut slots = HashMap::new();
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let marker = format!("<{component_name}/>-children");
|
||||
let view_marker = quote! { .with_view_marker(#marker) };
|
||||
} else {
|
||||
let view_marker = quote! {};
|
||||
}
|
||||
}
|
||||
|
||||
let children = fragment_to_tokens(
|
||||
cx,
|
||||
span,
|
||||
&node.children,
|
||||
true,
|
||||
TagType::Unknown,
|
||||
Some(&mut slots),
|
||||
global_class,
|
||||
None,
|
||||
);
|
||||
|
||||
if let Some(children) = children {
|
||||
let bindables =
|
||||
items_to_bind.iter().map(|ident| quote! { #ident, });
|
||||
|
||||
let clonables = items_to_clone
|
||||
.iter()
|
||||
.map(|ident| quote! { let #ident = #ident.clone(); });
|
||||
|
||||
if bindables.len() > 0 {
|
||||
quote! {
|
||||
.children({
|
||||
#(#clonables)*
|
||||
|
||||
move |#cx, #(#bindables)*| #children #view_marker
|
||||
})
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.children({
|
||||
#(#clonables)*
|
||||
|
||||
Box::new(move |#cx| #children #view_marker)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
};
|
||||
|
||||
let slots = slots.drain().map(|(slot, values)| {
|
||||
let slot = Ident::new(&slot, span);
|
||||
if values.len() > 1 {
|
||||
quote! {
|
||||
.#slot([
|
||||
#(#values)*
|
||||
].to_vec())
|
||||
}
|
||||
} else {
|
||||
let value = &values[0];
|
||||
quote! { .#slot(#value) }
|
||||
}
|
||||
});
|
||||
|
||||
let slot = quote! {
|
||||
#component_name::builder()
|
||||
#(#props)*
|
||||
#(#slots)*
|
||||
#children
|
||||
.build()
|
||||
.into(),
|
||||
};
|
||||
|
||||
parent_slots
|
||||
.entry(name)
|
||||
.and_modify(|entry| entry.push(slot.clone()))
|
||||
.or_insert(vec![slot]);
|
||||
}
|
||||
|
||||
pub(crate) fn is_slot(node: &KeyedAttribute) -> bool {
|
||||
let key = node.key.to_string();
|
||||
let key = key.trim();
|
||||
key == "slot" || key.starts_with("slot:")
|
||||
}
|
||||
|
||||
pub(crate) fn get_slot(node: &NodeElement) -> Option<&KeyedAttribute> {
|
||||
node.attributes().iter().find_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
if is_slot(node) {
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
source: leptos_macro/src/view/tests.rs
|
||||
expression: pretty(result)
|
||||
---
|
||||
fn view() {
|
||||
::leptos::component_view(
|
||||
&SimpleCounter,
|
||||
cx,
|
||||
::leptos::component_props_builder(&SimpleCounter)
|
||||
.initial_value(#[allow(unused_braces)] 0)
|
||||
.step(#[allow(unused_braces)] 1)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
source: leptos_macro/src/view/tests.rs
|
||||
expression: result
|
||||
---
|
||||
TokenStream [
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: leptos,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: component_view,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '&',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: SimpleCounter,
|
||||
span: bytes(11..24),
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: cx,
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
spacing: Alone,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: leptos,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: component_props_builder,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '&',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: SimpleCounter,
|
||||
span: bytes(11..24),
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: initial_value,
|
||||
span: bytes(37..50),
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '#',
|
||||
spacing: Alone,
|
||||
},
|
||||
Group {
|
||||
delimiter: Bracket,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: allow,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: unused_braces,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Literal {
|
||||
lit: 0,
|
||||
span: bytes(51..52),
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: step,
|
||||
span: bytes(65..69),
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '#',
|
||||
spacing: Alone,
|
||||
},
|
||||
Group {
|
||||
delimiter: Bracket,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: allow,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: unused_braces,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Literal {
|
||||
lit: 1,
|
||||
span: bytes(70..71),
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: build,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
---
|
||||
source: leptos_macro/src/view/tests.rs
|
||||
expression: pretty(result)
|
||||
---
|
||||
fn view() {
|
||||
{
|
||||
thread_local! {
|
||||
static __TEMPLATE : ::leptos::web_sys::HtmlTemplateElement = { let document =
|
||||
::leptos::document(); let el = document.create_element("template").unwrap();
|
||||
el
|
||||
.set_inner_html("<div><button>Clear</button><button>-1</button><span>Value: <!>!</span><button>+1</button></div>");
|
||||
::leptos::wasm_bindgen::JsCast::unchecked_into(el) }
|
||||
}
|
||||
let _ = ::leptos::leptos_dom::html::div;
|
||||
let _ = ::leptos::leptos_dom::html::div;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
let _ = ::leptos::leptos_dom::html::span;
|
||||
let _ = ::leptos::leptos_dom::html::span;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
let root = __TEMPLATE
|
||||
.with(|tpl| tpl.content().clone_node_with_deep(true))
|
||||
.unwrap()
|
||||
.first_child()
|
||||
.unwrap();
|
||||
let _el1 = "div";
|
||||
let _el1 = ::leptos::wasm_bindgen::JsCast::unchecked_into::<
|
||||
leptos::web_sys::Node,
|
||||
>(root.clone());
|
||||
let _el2 = "button";
|
||||
let _el2 = _el1
|
||||
.first_child()
|
||||
.unwrap_or_else(|| panic!("error: {} => {}", "button", "firstChild"));
|
||||
let _el3 = _el2
|
||||
.first_child()
|
||||
.unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "firstChild"));
|
||||
let _el4 = "button";
|
||||
let _el4 = _el2
|
||||
.next_sibling()
|
||||
.unwrap_or_else(|| panic!("error : {} => {} ", "button", "nextSibling"));
|
||||
let _el5 = _el4
|
||||
.first_child()
|
||||
.unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "firstChild"));
|
||||
let _el6 = "span";
|
||||
let _el6 = _el4
|
||||
.next_sibling()
|
||||
.unwrap_or_else(|| panic!("error : {} => {} ", "span", "nextSibling"));
|
||||
let _el7 = _el6
|
||||
.first_child()
|
||||
.unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "firstChild"));
|
||||
let _el8 = _el7
|
||||
.next_sibling()
|
||||
.unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "nextSibling"));
|
||||
let _el9 = _el8
|
||||
.next_sibling()
|
||||
.unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "nextSibling"));
|
||||
let _el10 = "button";
|
||||
let _el10 = _el6
|
||||
.next_sibling()
|
||||
.unwrap_or_else(|| panic!("error : {} => {} ", "button", "nextSibling"));
|
||||
let _el11 = _el10
|
||||
.first_child()
|
||||
.unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "firstChild"));
|
||||
::leptos::leptos_dom::add_event_helper(
|
||||
::leptos::wasm_bindgen::JsCast::unchecked_ref(&_el2),
|
||||
::leptos::leptos_dom::ev::click,
|
||||
move |_| set_value(0),
|
||||
);
|
||||
::leptos::leptos_dom::add_event_helper(
|
||||
::leptos::wasm_bindgen::JsCast::unchecked_ref(&_el4),
|
||||
::leptos::leptos_dom::ev::click,
|
||||
move |_| set_value.update(|value| *value -= step),
|
||||
);
|
||||
::leptos::leptos_dom::mount_child(
|
||||
::leptos::leptos_dom::MountKind::Before(&_el8.clone()),
|
||||
&{ { value } }.into_view(cx),
|
||||
);
|
||||
::leptos::leptos_dom::add_event_helper(
|
||||
::leptos::wasm_bindgen::JsCast::unchecked_ref(&_el10),
|
||||
::leptos::leptos_dom::ev::click,
|
||||
move |_| set_value.update(|value| *value += step),
|
||||
);
|
||||
::leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
|
||||
#[cfg(debug_assertions)]
|
||||
name: "div".into(),
|
||||
element: ::leptos::wasm_bindgen::JsCast::unchecked_into(root),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
source: leptos_macro/src/view/tests.rs
|
||||
expression: pretty(result)
|
||||
---
|
||||
fn view() {
|
||||
::leptos::component_view(
|
||||
&SimpleCounter,
|
||||
cx,
|
||||
::leptos::component_props_builder(&SimpleCounter)
|
||||
.initial_value(#[allow(unused_braces)] 0)
|
||||
.step(#[allow(unused_braces)] 1)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
source: leptos_macro/src/view/tests.rs
|
||||
expression: result
|
||||
---
|
||||
TokenStream [
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: leptos,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: component_view,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '&',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: SimpleCounter,
|
||||
span: bytes(11..24),
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: cx,
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
spacing: Alone,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: leptos,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: component_props_builder,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '&',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: SimpleCounter,
|
||||
span: bytes(11..24),
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: initial_value,
|
||||
span: bytes(37..50),
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '#',
|
||||
spacing: Alone,
|
||||
},
|
||||
Group {
|
||||
delimiter: Bracket,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: allow,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: unused_braces,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Literal {
|
||||
lit: 0,
|
||||
span: bytes(51..52),
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: step,
|
||||
span: bytes(65..69),
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '#',
|
||||
spacing: Alone,
|
||||
},
|
||||
Group {
|
||||
delimiter: Bracket,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: allow,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: unused_braces,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Literal {
|
||||
lit: 1,
|
||||
span: bytes(70..71),
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: build,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
---
|
||||
source: leptos_macro/src/view/tests.rs
|
||||
expression: pretty(result)
|
||||
---
|
||||
fn view() {
|
||||
{
|
||||
let _ = ::leptos::leptos_dom::html::div;
|
||||
::leptos::leptos_dom::html::div(cx)
|
||||
.child((
|
||||
cx,
|
||||
{
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
::leptos::leptos_dom::html::button(cx)
|
||||
.on(::leptos::ev::click, move |_| set_value(0))
|
||||
.child("Clear")
|
||||
},
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
{
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
::leptos::leptos_dom::html::button(cx)
|
||||
.on(
|
||||
::leptos::ev::click,
|
||||
move |_| set_value.update(|value| *value -= step),
|
||||
)
|
||||
.child("-1")
|
||||
},
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
{
|
||||
let _ = ::leptos::leptos_dom::html::span;
|
||||
::leptos::leptos_dom::html::span(cx)
|
||||
.child("Value: ")
|
||||
.child((cx, { value }))
|
||||
.child("!")
|
||||
},
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
{
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
::leptos::leptos_dom::html::button(cx)
|
||||
.on(
|
||||
::leptos::ev::click,
|
||||
move |_| set_value.update(|value| *value += step),
|
||||
)
|
||||
.child("+1")
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
source: leptos_macro/src/view/tests.rs
|
||||
expression: pretty(result)
|
||||
---
|
||||
fn view() {
|
||||
::leptos::component_view(
|
||||
&SimpleCounter,
|
||||
cx,
|
||||
::leptos::component_props_builder(&SimpleCounter)
|
||||
.initial_value(#[allow(unused_braces)] 0)
|
||||
.step(#[allow(unused_braces)] 1)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
source: leptos_macro/src/view/tests.rs
|
||||
expression: result
|
||||
---
|
||||
TokenStream [
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: leptos,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: component_view,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '&',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: SimpleCounter,
|
||||
span: bytes(11..24),
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: cx,
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
spacing: Alone,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: leptos,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: component_props_builder,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '&',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: SimpleCounter,
|
||||
span: bytes(11..24),
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: initial_value,
|
||||
span: bytes(37..50),
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '#',
|
||||
spacing: Alone,
|
||||
},
|
||||
Group {
|
||||
delimiter: Bracket,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: allow,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: unused_braces,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Literal {
|
||||
lit: 0,
|
||||
span: bytes(51..52),
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: step,
|
||||
span: bytes(65..69),
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '#',
|
||||
spacing: Alone,
|
||||
},
|
||||
Group {
|
||||
delimiter: Bracket,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: allow,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Ident {
|
||||
sym: unused_braces,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Literal {
|
||||
lit: 1,
|
||||
span: bytes(70..71),
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: build,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
---
|
||||
source: leptos_macro/src/view/tests.rs
|
||||
expression: pretty(result)
|
||||
---
|
||||
fn view() {
|
||||
{
|
||||
let _ = ::leptos::leptos_dom::html::div;
|
||||
let _ = ::leptos::leptos_dom::html::div;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
let _ = ::leptos::leptos_dom::html::span;
|
||||
let _ = ::leptos::leptos_dom::html::span;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
let _ = ::leptos::leptos_dom::html::button;
|
||||
leptos::leptos_dom::helpers::ssr_event_listener(
|
||||
::leptos::ev::click,
|
||||
move |_| set_value(0),
|
||||
);
|
||||
leptos::leptos_dom::helpers::ssr_event_listener(
|
||||
::leptos::ev::click,
|
||||
move |_| set_value.update(|value| *value -= step),
|
||||
);
|
||||
leptos::leptos_dom::helpers::ssr_event_listener(
|
||||
::leptos::ev::click,
|
||||
move |_| set_value.update(|value| *value += step),
|
||||
);
|
||||
::leptos::HtmlElement::from_chunks(
|
||||
cx,
|
||||
::leptos::leptos_dom::html::Div::default(),
|
||||
[
|
||||
leptos::leptos_dom::html::StringOrView::String(
|
||||
format!(
|
||||
"<div id=\"_{}\"><button id=\"_{}\">Clear</button><button id=\"_{}\">-1</button><span id=\"_{}\">Value: ",
|
||||
::leptos::leptos_dom::HydrationCtx::peek(),
|
||||
::leptos::leptos_dom::HydrationCtx::id(),
|
||||
::leptos::leptos_dom::HydrationCtx::id(),
|
||||
::leptos::leptos_dom::HydrationCtx::id()
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
#[allow(unused_braces)]
|
||||
{
|
||||
let view = { { value } }.into_view(cx);
|
||||
leptos::leptos_dom::html::StringOrView::View(
|
||||
std::rc::Rc::new(move || view.clone()),
|
||||
)
|
||||
},
|
||||
leptos::leptos_dom::html::StringOrView::String(
|
||||
format!(
|
||||
"!</span><button id=\"_{}\">+1</button></div>",
|
||||
::leptos::leptos_dom::HydrationCtx::id()
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
119
leptos_macro/src/view/tests.rs
Normal file
119
leptos_macro/src/view/tests.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use std::str::FromStr;
|
||||
use syn::parse_quote;
|
||||
|
||||
fn pretty(input: TokenStream) -> String {
|
||||
let type_item: syn::Item = parse_quote! {
|
||||
fn view(){
|
||||
#input
|
||||
}
|
||||
};
|
||||
|
||||
let file = syn::File {
|
||||
shebang: None,
|
||||
attrs: vec![],
|
||||
items: vec![type_item],
|
||||
};
|
||||
|
||||
prettyplease::unparse(&file)
|
||||
}
|
||||
|
||||
macro_rules! assert_snapshot
|
||||
{
|
||||
(@assert text $result:ident) => {
|
||||
insta::assert_snapshot!(pretty($result))
|
||||
};
|
||||
(@assert full $result:ident) => {
|
||||
insta::assert_debug_snapshot!($result)
|
||||
};
|
||||
(client_template($assert:ident) => $input: expr) => {
|
||||
{
|
||||
let tokens = TokenStream::from_str($input).unwrap();
|
||||
let cx = Ident::new("cx", Span::call_site());
|
||||
let nodes = rstml::parse2(tokens).unwrap();
|
||||
let result = crate::view::client_template::render_template(&cx, &nodes);
|
||||
|
||||
assert_snapshot!(@assert $assert result)
|
||||
}
|
||||
};
|
||||
(client_builder($assert:ident) => $input: expr) => {
|
||||
{
|
||||
let tokens = TokenStream::from_str($input).unwrap();
|
||||
let cx = Ident::new("cx", Span::call_site());
|
||||
let nodes = rstml::parse2(tokens).unwrap();
|
||||
let mode = crate::view::Mode::Client;
|
||||
let global_class = None;
|
||||
let call_site = None;
|
||||
let result = crate::view::render_view(&cx, &nodes, mode, global_class, call_site);
|
||||
|
||||
assert_snapshot!(@assert $assert result)
|
||||
}
|
||||
};
|
||||
(server_template($assert:ident) => $input: expr) => {
|
||||
{
|
||||
let tokens = TokenStream::from_str($input).unwrap();
|
||||
let cx = Ident::new("cx", Span::call_site());
|
||||
let nodes = rstml::parse2(tokens).unwrap();
|
||||
let mode = crate::view::Mode::Ssr;
|
||||
let global_class = None;
|
||||
let call_site = None;
|
||||
let result = crate::view::render_view(&cx, &nodes, mode, global_class, call_site);
|
||||
|
||||
assert_snapshot!(@assert $assert result)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
macro_rules! for_all_modes {
|
||||
(@ $module: ident, $type: ident => $(
|
||||
$test_name:ident => $raw_str:expr
|
||||
),*
|
||||
) => {
|
||||
mod $module {
|
||||
use super::*;
|
||||
$(
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
assert_snapshot!($type(text) => $raw_str)
|
||||
}
|
||||
)*
|
||||
mod full_span {
|
||||
use super::*;
|
||||
$(
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
assert_snapshot!($type(full) => $raw_str)
|
||||
}
|
||||
)*
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
( $(
|
||||
$tts:tt
|
||||
)*
|
||||
) => {
|
||||
for_all_modes!{@ csr, client_builder => $($tts)*}
|
||||
for_all_modes!{@ client_template, client_template => $($tts)*}
|
||||
for_all_modes!{@ ssr, server_template => $($tts)*}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
for_all_modes! {
|
||||
test_simple_counter => r#"
|
||||
<div>
|
||||
<button on:click=move |_| set_value(0)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
|
||||
</div>
|
||||
"#,
|
||||
test_counter_component => r#"
|
||||
<SimpleCounter
|
||||
initial_value=0
|
||||
step=1
|
||||
/>
|
||||
"#
|
||||
}
|
||||
@@ -370,7 +370,7 @@ fn push_cleanup(cx: Scope, cleanup_fn: Box<dyn FnOnce()>) {
|
||||
let cleanups = cleanups
|
||||
.entry(cx.id)
|
||||
.expect("trying to clean up a Scope that has already been disposed")
|
||||
.or_insert_with(Default::default);
|
||||
.or_default();
|
||||
cleanups.push(cleanup_fn);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -556,6 +556,15 @@ impl<T: Default> Default for MaybeSignal<T> {
|
||||
/// # });
|
||||
/// ```
|
||||
impl<T: Clone> SignalGet<T> for MaybeSignal<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::get()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn get(&self) -> T {
|
||||
match self {
|
||||
Self::Static(t) => t.clone(),
|
||||
@@ -563,6 +572,15 @@ impl<T: Clone> SignalGet<T> for MaybeSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::try_get()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn try_get(&self) -> Option<T> {
|
||||
match self {
|
||||
Self::Static(t) => Some(t.clone()),
|
||||
@@ -640,6 +658,15 @@ impl<T> SignalWith<T> for MaybeSignal<T> {
|
||||
}
|
||||
|
||||
impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::with_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
match self {
|
||||
Self::Static(t) => f(t),
|
||||
@@ -647,6 +674,15 @@ impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::try_with_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
match self {
|
||||
Self::Static(t) => Some(f(t)),
|
||||
@@ -656,6 +692,15 @@ impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::get_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn get_untracked(&self) -> T {
|
||||
match self {
|
||||
Self::Static(t) => t.clone(),
|
||||
@@ -663,6 +708,15 @@ impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::try_get_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn try_get_untracked(&self) -> Option<T> {
|
||||
match self {
|
||||
Self::Static(t) => Some(t.clone()),
|
||||
@@ -672,6 +726,15 @@ impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalStream<T> for MaybeSignal<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::to_stream()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn to_stream(
|
||||
&self,
|
||||
cx: Scope,
|
||||
@@ -764,4 +827,400 @@ impl From<&str> for MaybeSignal<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapping type for an optional component prop, which can either be a signal or a
|
||||
/// non-reactive value, and which may or may not have a value. In other words, this is
|
||||
/// an `Option<MaybeSignal<Option<T>>>` that automatically flattens its getters.
|
||||
///
|
||||
/// This creates an extremely flexible type for component libraries, etc.
|
||||
///
|
||||
/// ## Core Trait Implementations
|
||||
/// - [`.get()`](#impl-SignalGet<T>-for-MaybeProp<T>) (or calling the signal as a function) clones the current
|
||||
/// value of the signal. If you call it within an effect, it will cause that effect
|
||||
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
|
||||
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-MaybeProp<T>) clones the value of the signal
|
||||
/// without reactively tracking it.
|
||||
/// - [`.with()`](#impl-SignalWith<T>-for-MaybeProp<T>) allows you to reactively access the signal’s value without
|
||||
/// cloning by applying a callback function.
|
||||
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeProp<T>) allows you to access the signal’s
|
||||
/// value without reactively tracking it.
|
||||
/// - [`.to_stream()`](#impl-SignalStream<T>-for-MaybeProp<T>) converts the signal to an `async` stream of values.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (count, set_count) = create_signal(cx, Some(2));
|
||||
/// let double = |n| n * 2;
|
||||
/// let double_count = MaybeProp::derive(cx, move || count.get().map(double));
|
||||
/// let memoized_double_count =
|
||||
/// create_memo(cx, move |_| count.get().map(double));
|
||||
/// let static_value = 5;
|
||||
///
|
||||
/// // this function takes either a reactive or non-reactive value
|
||||
/// fn above_3(arg: &MaybeProp<i32>) -> bool {
|
||||
/// // ✅ calling the signal clones and returns the value
|
||||
/// // it is a shorthand for arg.get()q
|
||||
/// arg.get().map(|arg| arg > 3).unwrap_or(false)
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&None::<i32>.into()), false);
|
||||
/// assert_eq!(above_3(&static_value.into()), true);
|
||||
/// assert_eq!(above_3(&count.into()), false);
|
||||
/// assert_eq!(above_3(&double_count), true);
|
||||
/// assert_eq!(above_3(&memoized_double_count.into()), true);
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MaybeProp<T: 'static>(Option<MaybeSignal<Option<T>>>);
|
||||
|
||||
impl<T: Copy> Copy for MaybeProp<T> {}
|
||||
|
||||
impl<T> Default for MaybeProp<T> {
|
||||
fn default() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (count, set_count) = create_signal(cx, Some(2));
|
||||
/// let double = |n| n * 2;
|
||||
/// let double_count = MaybeProp::derive(cx, move || count.get().map(double));
|
||||
/// let memoized_double_count =
|
||||
/// create_memo(cx, move |_| count.get().map(double));
|
||||
/// let static_value = 5;
|
||||
///
|
||||
/// // this function takes either a reactive or non-reactive value
|
||||
/// fn above_3(arg: &MaybeProp<i32>) -> bool {
|
||||
/// // ✅ calling the signal clones and returns the value
|
||||
/// // it is a shorthand for arg.get()q
|
||||
/// arg.get().map(|arg| arg > 3).unwrap_or(false)
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&None::<i32>.into()), false);
|
||||
/// assert_eq!(above_3(&static_value.into()), true);
|
||||
/// assert_eq!(above_3(&count.into()), false);
|
||||
/// assert_eq!(above_3(&double_count), true);
|
||||
/// assert_eq!(above_3(&memoized_double_count.into()), true);
|
||||
/// # });
|
||||
/// ```
|
||||
impl<T: Clone> SignalGet<Option<T>> for MaybeProp<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::get()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn get(&self) -> Option<T> {
|
||||
self.0.as_ref().and_then(|s| s.get())
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::try_get()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn try_get(&self) -> Option<Option<T>> {
|
||||
self.0.as_ref().and_then(|s| s.try_get())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (name, set_name) = create_signal(cx, Some("Alice".to_string()));
|
||||
/// let name_upper = MaybeProp::derive(cx, move || {
|
||||
/// name.with(|n| n.as_ref().map(|n| n.to_uppercase()))
|
||||
/// });
|
||||
/// let memoized_lower = create_memo(cx, move |_| {
|
||||
/// name.with(|n| n.as_ref().map(|n| n.to_lowercase()))
|
||||
/// });
|
||||
/// let static_value: MaybeProp<String> = "Bob".to_string().into();
|
||||
///
|
||||
/// // this function takes any kind of wrapped signal
|
||||
/// fn current_len_inefficient(arg: &MaybeProp<String>) -> usize {
|
||||
/// // ❌ unnecessarily clones the string
|
||||
/// arg.get().map(|n| n.len()).unwrap_or(0)
|
||||
/// }
|
||||
///
|
||||
/// fn current_len(arg: &MaybeProp<String>) -> usize {
|
||||
/// // ✅ gets the length without cloning the `String`
|
||||
/// arg.with(|value| value.len()).unwrap_or(0)
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(current_len(&None::<String>.into()), 0);
|
||||
/// assert_eq!(current_len(&name_upper), 5);
|
||||
/// assert_eq!(current_len(&memoized_lower.into()), 5);
|
||||
/// assert_eq!(current_len(&static_value), 3);
|
||||
///
|
||||
/// assert_eq!(name.get(), Some("Alice".to_string()));
|
||||
/// assert_eq!(name_upper.get(), Some("ALICE".to_string()));
|
||||
/// assert_eq!(memoized_lower.get(), Some("alice".to_string()));
|
||||
/// assert_eq!(static_value.get(), Some("Bob".to_string()));
|
||||
/// # });
|
||||
/// ```
|
||||
impl<T> MaybeProp<T> {
|
||||
/// Applies a function to the current value, returning the result.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::with()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.and_then(|inner| inner.with(|value| value.as_ref().map(f)))
|
||||
}
|
||||
|
||||
/// Applies a function to the current value, returning the result. Returns `None`
|
||||
/// if the value has already been disposed.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::try_with()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
pub fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.and_then(|inner| inner.try_with(|value| value.as_ref().map(f)))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Applies a function to the current value, returning the result, without
|
||||
/// causing the current reactive scope to track changes.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::with_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
pub fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0.as_ref().and_then(|inner| {
|
||||
inner.with_untracked(|value| value.as_ref().map(f))
|
||||
})
|
||||
}
|
||||
|
||||
/// Applies a function to the current value, returning the result, without
|
||||
/// causing the current reactive scope to track changes. Returns `None` if
|
||||
/// the value has already been disposed.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::try_with_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
pub fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.and_then(|inner| {
|
||||
inner.try_with_untracked(|value| value.as_ref().map(f))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<Option<T>> for MaybeProp<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::get_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn get_untracked(&self) -> Option<T> {
|
||||
self.0.as_ref().and_then(|inner| inner.get_untracked())
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::try_get_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn try_get_untracked(&self) -> Option<Option<T>> {
|
||||
self.0.as_ref().and_then(|inner| inner.try_get_untracked())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalStream<Option<T>> for MaybeProp<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::to_stream()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn to_stream(
|
||||
&self,
|
||||
cx: Scope,
|
||||
) -> std::pin::Pin<Box<dyn futures::Stream<Item = Option<T>>>> {
|
||||
match &self.0 {
|
||||
None => Box::pin(futures::stream::once(async move { None })),
|
||||
Some(MaybeSignal::Static(t)) => {
|
||||
let t = t.clone();
|
||||
|
||||
let stream = futures::stream::once(async move { t });
|
||||
|
||||
Box::pin(stream)
|
||||
}
|
||||
Some(MaybeSignal::Dynamic(s)) => s.to_stream(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MaybeProp<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
/// Wraps a derived signal, i.e., any computation that accesses one or more
|
||||
/// reactive signals.
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (count, set_count) = create_signal(cx, Some(2));
|
||||
/// let double_count = Signal::derive(cx, move || count.get().map(|n| n * 2));
|
||||
///
|
||||
/// // this function takes any kind of wrapped signal
|
||||
/// fn above_3(arg: &MaybeProp<i32>) -> bool {
|
||||
/// arg.get().unwrap_or(0) > 3
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&count.into()), false);
|
||||
/// assert_eq!(above_3(&double_count.into()), true);
|
||||
/// assert_eq!(above_3(&2.into()), false);
|
||||
/// # });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::derive()",
|
||||
skip_all,
|
||||
fields(
|
||||
cx = ?cx.id,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn derive(
|
||||
cx: Scope,
|
||||
derived_signal: impl Fn() -> Option<T> + 'static,
|
||||
) -> Self {
|
||||
Self(Some(MaybeSignal::derive(cx, derived_signal)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MaybeProp<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self(Some(MaybeSignal::from(Some(value))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<T>> for MaybeProp<T> {
|
||||
fn from(value: Option<T>) -> Self {
|
||||
Self(Some(MaybeSignal::from(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<MaybeSignal<Option<T>>> for MaybeProp<T> {
|
||||
fn from(value: MaybeSignal<Option<T>>) -> Self {
|
||||
Self(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<MaybeSignal<Option<T>>>> for MaybeProp<T> {
|
||||
fn from(value: Option<MaybeSignal<Option<T>>>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ReadSignal<Option<T>>> for MaybeProp<T> {
|
||||
fn from(value: ReadSignal<Option<T>>) -> Self {
|
||||
Self(Some(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RwSignal<Option<T>>> for MaybeProp<T> {
|
||||
fn from(value: RwSignal<Option<T>>) -> Self {
|
||||
Self(Some(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Memo<Option<T>>> for MaybeProp<T> {
|
||||
fn from(value: Memo<Option<T>>) -> Self {
|
||||
Self(Some(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Signal<Option<T>>> for MaybeProp<T> {
|
||||
fn from(value: Signal<Option<T>>) -> Self {
|
||||
Self(Some(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for MaybeProp<String> {
|
||||
fn from(value: &str) -> Self {
|
||||
Self(Some(MaybeSignal::from(Some(value.to_string()))))
|
||||
}
|
||||
}
|
||||
|
||||
impl_get_fn_traits![Signal, MaybeSignal];
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<T: Clone> FnOnce<()> for MaybeProp<T> {
|
||||
type Output = Option<T>;
|
||||
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<T: Clone> FnMut<()> for MaybeProp<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<T: Clone> Fn<()> for MaybeProp<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/gbj/leptos"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "RPC for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.4.6"
|
||||
version = "0.4.8"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/gbj/leptos"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Tools to set HTML metadata in the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.4.6"
|
||||
version = "0.4.8"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -5,7 +5,7 @@ use thiserror::Error;
|
||||
/// This is a result type into which any error can be converted,
|
||||
/// and which can be used directly in your `view`.
|
||||
///
|
||||
/// All errors will be stored as [`Error`].
|
||||
/// All errors will be stored as [`struct@Error`].
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
/// A generic wrapper for any error.
|
||||
@@ -110,7 +110,7 @@ where
|
||||
|
||||
/// Type for errors that can occur when using server functions.
|
||||
///
|
||||
/// Unlike [`ServerFnErrorErr`], this implements [`std::error::Error`]. This means
|
||||
/// Unlike [`ServerFnError`], this implements [`std::error::Error`]. This means
|
||||
/// it can be used in situations in which the `Error` trait is required, but it’s
|
||||
/// not possible to create a blanket implementation that converts other errors into
|
||||
/// this type.
|
||||
|
||||
Reference in New Issue
Block a user