mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 18:22:34 -05:00
Compare commits
36 Commits
Fix-missin
...
fix-option
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a74b3e2ddc | ||
|
|
62061f90ea | ||
|
|
9a231ddef0 | ||
|
|
ce6a093f9f | ||
|
|
f07fa0e0be | ||
|
|
43ad91512a | ||
|
|
116d23f2c3 | ||
|
|
2ecb345a79 | ||
|
|
f55f833426 | ||
|
|
7101a2f55e | ||
|
|
f8b76387ec | ||
|
|
11fc51577b | ||
|
|
ae1ca969ef | ||
|
|
895f9d8487 | ||
|
|
1e45b182a0 | ||
|
|
4c26dc597d | ||
|
|
2863d49a1c | ||
|
|
087eb18c8b | ||
|
|
c7c672717c | ||
|
|
c69cc02f30 | ||
|
|
9eb81f00f9 | ||
|
|
72fe3d45f0 | ||
|
|
7802d941bd | ||
|
|
f10784f686 | ||
|
|
35197691c0 | ||
|
|
4afbef87f6 | ||
|
|
218485e3be | ||
|
|
8d60a191eb | ||
|
|
1ba01a46af | ||
|
|
fdece25051 | ||
|
|
590056e047 | ||
|
|
817bb1628e | ||
|
|
f911cdd56f | ||
|
|
76a9c719a3 | ||
|
|
395336a8c0 | ||
|
|
b84906e6dc |
18
Cargo.toml
18
Cargo.toml
@@ -24,17 +24,17 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.1.1" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.1" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.1" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.1" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.1" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.1" }
|
||||
leptos_router = { path = "./router", version = "0.1.1" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.1" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.1.3" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.3" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.3" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.3" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.3" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.3" }
|
||||
leptos_router = { path = "./router", version = "0.1.3" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.3" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0.18"
|
||||
leptos = "0.1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0.18"
|
||||
leptos = "0.1"
|
||||
|
||||
@@ -4,7 +4,9 @@ fn main() {
|
||||
mount_to_body(|cx| {
|
||||
let name = "gbj";
|
||||
let userid = 0;
|
||||
let _input_element: Element;
|
||||
|
||||
// This will be filled by _ref=input below.
|
||||
let input_element = NodeRef::<HtmlElement<Input>>::new(cx);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
@@ -17,7 +19,7 @@ fn main() {
|
||||
prop:value="todo" // `prop:` lets you set a property on a DOM node
|
||||
value="initial" // side note: the DOM `value` attribute only sets *initial* value
|
||||
// this is very important when working with forms!
|
||||
_ref=_input_element // `_ref` stores tis element in a variable
|
||||
_ref=input_element // `_ref` stores tis element in a variable
|
||||
/>
|
||||
<ul data-user=userid> // attributes can take expressions as values
|
||||
<li class="todo my-todo" // here we set the `class` attribute
|
||||
|
||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0.18"
|
||||
leptos = "0.1"
|
||||
|
||||
@@ -6,7 +6,7 @@ const APP_ID: &str = "dev.leptos.Counter";
|
||||
|
||||
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
|
||||
fn main() {
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
// Create a new application
|
||||
let app = Application::builder().application_id(APP_ID).build();
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
This example highlights four different ways that child components can communicate with their parent:
|
||||
|
||||
1. <ButtonA/>: passing a WriteSignal as one of the child component props,
|
||||
1. `<ButtonA/>`: passing a WriteSignal as one of the child component props,
|
||||
for the child component to write into and the parent to read
|
||||
2. <ButtonB/>: passing a closure as one of the child component props, for
|
||||
2. `<ButtonB/>`: passing a closure as one of the child component props, for
|
||||
the child component to call
|
||||
3. <ButtonC/>: adding a simple event listener on the child component itself
|
||||
4. <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
|
||||
3. `<ButtonC/>`: adding a simple event listener on the child component itself
|
||||
4. `<ButtonD/>`: providing a context that is used in the component (rather than prop drilling)
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ console_error_panic_hook = "0.1.7"
|
||||
uuid = { version = "1", features = ["v4", "js", "serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
web-sys = { version = "0.3", features = ["Storage"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
@@ -339,9 +339,7 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
let (head, tail) = html_parts(&options);
|
||||
|
||||
stream_app(app, head, tail, res_options, additional_context).await
|
||||
stream_app(&options, app, res_options, additional_context).await
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -429,9 +427,7 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
let (head, tail) = html_parts(&options);
|
||||
|
||||
stream_app(app, head, tail, res_options, |_cx| {}).await
|
||||
stream_app(&options, app, res_options, |_cx| {}).await
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -457,23 +453,31 @@ fn leptos_corrected_path(req: &HttpRequest) -> String {
|
||||
}
|
||||
|
||||
async fn stream_app(
|
||||
options: &LeptosOptions,
|
||||
app: impl FnOnce(leptos::Scope) -> View + 'static,
|
||||
head: String,
|
||||
tail: String,
|
||||
res_options: ResponseOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
) -> HttpResponse<BoxBody> {
|
||||
let (stream, runtime, _) = render_to_stream_with_prefix_undisposed_with_context(
|
||||
let (stream, runtime, scope) = render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| {
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
let head = meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body>").into()
|
||||
let body_meta = meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.body.as_string())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body{body_meta}>").into()
|
||||
},
|
||||
additional_context,
|
||||
);
|
||||
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let (head, tail) = html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
let mut stream = Box::pin(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
.chain(stream)
|
||||
@@ -516,7 +520,7 @@ async fn stream_app(
|
||||
res
|
||||
}
|
||||
|
||||
fn html_parts(options: &LeptosOptions) -> (String, String) {
|
||||
fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (String, String) {
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
@@ -557,9 +561,12 @@ fn html_parts(options: &LeptosOptions) -> (String, String) {
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let html_metadata = meta_context
|
||||
.and_then(|mc| mc.html.as_string())
|
||||
.unwrap_or_default();
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html{html_metadata}>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
|
||||
@@ -420,61 +420,6 @@ where
|
||||
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let output_name = &options.output_name;
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let site_ip = &options.site_address.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
|
||||
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
var 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();
|
||||
if (msg.css) {{
|
||||
const link = document.querySelector("link#leptos");
|
||||
if (link) {{
|
||||
let href = link.getAttribute('href').split('?')[0];
|
||||
let newHref = href + '?version=' + new Date().getMilliseconds();
|
||||
link.setAttribute('href', newHref);
|
||||
}} else {{
|
||||
console.warn("Could not find link#leptos");
|
||||
}}
|
||||
}};
|
||||
}};
|
||||
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
|
||||
}})()
|
||||
</script>
|
||||
"#
|
||||
),
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
let tail = "</body></html>";
|
||||
|
||||
let (mut tx, rx) = futures::channel::mpsc::channel(8);
|
||||
|
||||
spawn_blocking({
|
||||
@@ -518,13 +463,18 @@ where
|
||||
},
|
||||
add_context,
|
||||
);
|
||||
|
||||
let cx = Scope { runtime, id: scope };
|
||||
let (head, tail) = html_parts(&options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
_ = tx.send(head).await;
|
||||
let mut shell = Box::pin(bundle);
|
||||
while let Some(fragment) = shell.next().await {
|
||||
_ = tx.send(fragment).await;
|
||||
}
|
||||
_ = tx.send(tail.to_string()).await;
|
||||
|
||||
// Extract the value of ResponseOptions from here
|
||||
let cx = Scope { runtime, id: scope };
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
|
||||
@@ -543,12 +493,7 @@ where
|
||||
}
|
||||
});
|
||||
|
||||
let mut stream = Box::pin(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
.chain(rx)
|
||||
.chain(futures::stream::once(async { tail.to_string() }))
|
||||
.map(|html| Ok(Bytes::from(html))),
|
||||
);
|
||||
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
|
||||
|
||||
// Get the first, second, and third chunks in the stream, which renders the app shell, and thus allows Resources to run
|
||||
let first_chunk = stream.next().await;
|
||||
@@ -581,6 +526,65 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn html_parts(options: &LeptosOptions, meta: Option<&MetaContext>) -> (String, &'static str) {
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let output_name = &options.output_name;
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let site_ip = &options.site_address.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
|
||||
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
var 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();
|
||||
if (msg.css) {{
|
||||
const link = document.querySelector("link#leptos");
|
||||
if (link) {{
|
||||
let href = link.getAttribute('href').split('?')[0];
|
||||
let newHref = href + '?version=' + new Date().getMilliseconds();
|
||||
link.setAttribute('href', newHref);
|
||||
}} else {{
|
||||
console.warn("Could not find link#leptos");
|
||||
}}
|
||||
}};
|
||||
}};
|
||||
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
|
||||
}})()
|
||||
</script>
|
||||
"#
|
||||
),
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let html_metadata = meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html{html_metadata}>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
let tail = "</body></html>";
|
||||
(head, tail)
|
||||
}
|
||||
|
||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
use leptos_dom::{Errors, Fragment, IntoView};
|
||||
use crate::Children;
|
||||
use leptos_dom::{Errors, IntoView};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
|
||||
|
||||
/// When you render a `Result<_, _>` in your view, in the `Err` case it will
|
||||
/// render nothing, and search up through the view tree for an `<ErrorBoundary/>`.
|
||||
/// This component lets you define a fallback that should be rendered in that
|
||||
/// error case, allowing you to handle errors within a section of the interface.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use leptos_macro::*;
|
||||
/// # use leptos_dom::*; use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// let (value, set_value) = create_signal(cx, Ok(0));
|
||||
/// let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <input type="text" on:input=on_input/>
|
||||
/// <ErrorBoundary
|
||||
/// fallback=move |_, _| view! { cx, <p class="error">"Enter a valid number."</p>}
|
||||
/// >
|
||||
/// <p>"Value is: " {value}</p>
|
||||
/// </ErrorBoundary>
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
pub fn ErrorBoundary<F, IV>(
|
||||
cx: Scope,
|
||||
/// The components inside the tag which will get rendered
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
children: Children,
|
||||
/// A fallback that will be shown if an error occurs.
|
||||
fallback: F,
|
||||
) -> impl IntoView
|
||||
|
||||
@@ -164,3 +164,11 @@ pub use transition::*;
|
||||
pub use leptos_reactive::debug_warn;
|
||||
|
||||
extern crate self as leptos;
|
||||
|
||||
/// The most common type for the `children` property on components,
|
||||
/// which can only be called once.
|
||||
pub type Children = Box<dyn FnOnce(Scope) -> Fragment>;
|
||||
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once.
|
||||
pub type ChildrenFn = Box<dyn Fn(Scope) -> Fragment>;
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
use crate::Children;
|
||||
use leptos::component;
|
||||
use leptos_dom::{Fragment, IntoView};
|
||||
use leptos_dom::IntoView;
|
||||
use leptos_reactive::Scope;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
/// A component that will show it's children when the passed in closure is True, and show the fallback
|
||||
/// when the closure is false
|
||||
/// A component that will show its children when the `when` condition is `true`,
|
||||
/// and show the fallback when it is `false`, without rerendering every time
|
||||
/// the condition changes.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use leptos_macro::*;
|
||||
/// # use leptos_dom::*; use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// let (value, set_value) = create_signal(cx, 0);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <Show
|
||||
/// when=move || value() < 5
|
||||
/// fallback=|cx| view! { cx, "Big number!" }
|
||||
/// >
|
||||
/// "Small number!"
|
||||
/// </Show>
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[component]
|
||||
pub fn Show<F, W, IV>(
|
||||
/// The scope the component is running in
|
||||
cx: Scope,
|
||||
/// The components Show wraps
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
children: Children,
|
||||
/// A closure that returns a bool that determines whether this thing runs
|
||||
when: W,
|
||||
/// A closure that returns what gets rendered if the when statement is false
|
||||
|
||||
@@ -129,3 +129,22 @@ fn ssr_with_styles() {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[test]
|
||||
fn ssr_option() {
|
||||
use leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<option/>
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<option id=\"_0-1\"></option>"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ cfg-if = "1"
|
||||
drain_filter_polyfill = "0.1"
|
||||
educe = "0.4"
|
||||
futures = "0.3"
|
||||
gloo = { version = "0.8", features = ["futures"] }
|
||||
html-escape = "0.2"
|
||||
indexmap = "1.9"
|
||||
itertools = "0.10"
|
||||
@@ -34,8 +33,11 @@ leptos = { path = "../leptos" }
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"console",
|
||||
"Comment",
|
||||
"Document",
|
||||
"DomTokenList",
|
||||
"Location",
|
||||
"Range",
|
||||
"Text",
|
||||
"HtmlCollection",
|
||||
@@ -49,6 +51,7 @@ features = [
|
||||
"DeviceMotionEvent",
|
||||
"DeviceOrientationEvent",
|
||||
"DragEvent",
|
||||
"ErrorEvent",
|
||||
"FocusEvent",
|
||||
"GamepadEvent",
|
||||
"HashChangeEvent",
|
||||
|
||||
@@ -19,23 +19,28 @@ where
|
||||
match use_context::<RwSignal<Errors>>(cx) {
|
||||
Some(errors) => {
|
||||
let id = HydrationCtx::id();
|
||||
errors.update(move |errors: &mut Errors| errors.insert(id, error));
|
||||
errors.update({
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let id = id.clone();
|
||||
move |errors: &mut Errors| errors.insert(id, error)
|
||||
});
|
||||
|
||||
// remove the error from the list if this drops,
|
||||
// i.e., if it's in a DynChild that switches from Err to Ok
|
||||
// Only can run on the client, will panic on the server
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "hydrate", feature="csr"))] {
|
||||
use leptos_reactive::{on_cleanup, queue_microtask};
|
||||
on_cleanup(cx, move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
errors.remove::<E>(&id);
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use leptos_reactive::{on_cleanup, queue_microtask};
|
||||
on_cleanup(cx, move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
crate::log!("removing error at {id}");
|
||||
errors.remove::<E>(&id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
@@ -6,12 +6,8 @@ use cfg_if::cfg_if;
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use crate::events::*;
|
||||
use crate::macro_helpers::Property;
|
||||
use crate::macro_helpers::{
|
||||
attribute_expression, class_expression, property_expression,
|
||||
};
|
||||
use crate::macro_helpers::*;
|
||||
use crate::{mount_child, MountKind};
|
||||
use leptos_reactive::create_render_effect;
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
@@ -46,7 +42,7 @@ cfg_if! {
|
||||
use crate::{
|
||||
ev::EventDescriptor,
|
||||
hydration::HydrationCtx,
|
||||
macro_helpers::{Attribute, Class, IntoAttribute, IntoClass, IntoProperty},
|
||||
macro_helpers::{IntoAttribute, IntoClass, IntoProperty},
|
||||
Element, Fragment, IntoView, NodeRef, Text, View,
|
||||
};
|
||||
use leptos_reactive::Scope;
|
||||
@@ -203,10 +199,8 @@ impl Custom {
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
gloo::console::warn!(
|
||||
"element with id",
|
||||
format!("_{id}"),
|
||||
"not found, ignoring it for hydration"
|
||||
crate::warn!(
|
||||
"element with id {id} not found, ignoring it for hydration"
|
||||
);
|
||||
|
||||
crate::document().create_element(&name).unwrap()
|
||||
@@ -495,26 +489,18 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
let el = self.element.as_ref();
|
||||
let value = attr.into_attribute(self.cx);
|
||||
match value {
|
||||
Attribute::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) {
|
||||
attribute_expression(&el, &name, new.clone());
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
_ => attribute_expression(el, &name, value),
|
||||
};
|
||||
attribute_helper(
|
||||
self.element.as_ref(),
|
||||
name,
|
||||
attr.into_attribute(self.cx),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
use crate::macro_helpers::Attribute;
|
||||
|
||||
let mut this = self;
|
||||
|
||||
let mut attr = attr.into_attribute(this.cx);
|
||||
@@ -554,26 +540,16 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
let el = self.element.as_ref();
|
||||
let class_list = el.class_list();
|
||||
let value = class.into_class(self.cx);
|
||||
match value {
|
||||
Class::Fn(cx, f) => {
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) && (old.is_some() || new) {
|
||||
class_expression(&class_list, &name, new)
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Class::Value(value) => class_expression(&class_list, &name, value),
|
||||
};
|
||||
class_helper(el, name, value);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
use crate::macro_helpers::Class;
|
||||
|
||||
let mut this = self;
|
||||
|
||||
let class = class.into_class(this.cx);
|
||||
@@ -609,25 +585,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
let name = name.into();
|
||||
let value = value.into_property(self.cx);
|
||||
let el = self.element.as_ref();
|
||||
match value {
|
||||
Property::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
if old.as_ref() != Some(&new)
|
||||
&& !(old.is_none() && new == wasm_bindgen::JsValue::UNDEFINED)
|
||||
{
|
||||
property_expression(&el, prop_name, new.clone())
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Property::Value(value) => {
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
property_expression(el, prop_name, value)
|
||||
}
|
||||
};
|
||||
property_helper(el, name, value);
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
@@ -848,63 +806,18 @@ macro_rules! generate_html_tags {
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let element = if HydrationCtx::is_hydrating() {
|
||||
if let Some(el) = crate::document().get_element_by_id(
|
||||
&format!("_{id}")
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
stringify!([<$tag:upper>]),
|
||||
"SSR and CSR elements have the same `TopoId` \
|
||||
but different node kinds. This is either a \
|
||||
discrepancy between SSR and CSR rendering
|
||||
logic, which is considered a bug, or it \
|
||||
can also be a leptos hydration issue."
|
||||
);
|
||||
|
||||
el.remove_attribute("id").unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else if let Ok(Some(el)) = crate::document().query_selector(
|
||||
&format!("[leptos-hk=_{id}]")
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
stringify!([<$tag:upper>]),
|
||||
"SSR and CSR elements have the same `TopoId` \
|
||||
but different node kinds. This is either a \
|
||||
discrepancy between SSR and CSR rendering
|
||||
logic, which is considered a bug, or it \
|
||||
can also be a leptos hydration issue."
|
||||
);
|
||||
|
||||
el.remove_attribute("leptos-hk").unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
gloo::console::warn!(
|
||||
"element with id",
|
||||
format!("_{id}"),
|
||||
"not found, ignoring it for hydration"
|
||||
);
|
||||
|
||||
let element = create_leptos_element(
|
||||
&stringify!([<$tag:upper>]),
|
||||
id,
|
||||
|| {
|
||||
[<$tag:upper>]
|
||||
.with(|el|
|
||||
el.clone_node()
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
[<$tag:upper>]
|
||||
.with(|el|
|
||||
el.clone_node()
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
)
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -979,24 +892,70 @@ macro_rules! generate_html_tags {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
fn create_leptos_element(
|
||||
tag: &str,
|
||||
id: crate::HydrationKey,
|
||||
clone_element: fn() -> web_sys::HtmlElement,
|
||||
) -> web_sys::HtmlElement {
|
||||
if HydrationCtx::is_hydrating() {
|
||||
if let Some(el) = crate::document().get_element_by_id(&format!("_{id}")) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
&el.node_name().to_ascii_uppercase(),
|
||||
tag,
|
||||
"SSR and CSR elements have the same `TopoId` but different node \
|
||||
kinds. This is either a discrepancy between SSR and CSR rendering
|
||||
logic, which is considered a bug, or it can also be a leptos \
|
||||
hydration issue."
|
||||
);
|
||||
|
||||
el.remove_attribute("id").unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else if let Ok(Some(el)) =
|
||||
crate::document().query_selector(&format!("[leptos-hk=_{id}]"))
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
tag,
|
||||
"SSR and CSR elements have the same `TopoId` but different node \
|
||||
kinds. This is either a discrepancy between SSR and CSR rendering
|
||||
logic, which is considered a bug, or it can also be a leptos \
|
||||
hydration issue."
|
||||
);
|
||||
|
||||
el.remove_attribute("leptos-hk").unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
crate::warn!("element with id {id} not found, ignoring it for hydration");
|
||||
|
||||
clone_element()
|
||||
}
|
||||
} else {
|
||||
clone_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
||||
fn warn_on_ambiguous_a(parent: &web_sys::Element, child: &View) {
|
||||
if let View::Element(el) = &child {
|
||||
if el.name == "a"
|
||||
if (el.name == "a"
|
||||
|| el.name == "script"
|
||||
|| el.name == "style"
|
||||
|| el.name == "title"
|
||||
|| el.name == "title")
|
||||
&& parent.namespace_uri() != el.element.namespace_uri()
|
||||
{
|
||||
if parent.namespace_uri() != el.element.namespace_uri() {
|
||||
crate::warn!(
|
||||
"Warning: you are appending an SVG element to an HTML element, or \
|
||||
an HTML element to an SVG. Typically, this occurs when you create \
|
||||
an <a/> or <script/> with the `view` macro and append it to an \
|
||||
SVG, but the framework assumed it was HTML when you created it. To \
|
||||
specify that it is an SVG element, use <svg::{{tag name}}/> in the \
|
||||
view macro."
|
||||
)
|
||||
}
|
||||
crate::warn!(
|
||||
"Warning: you are appending an SVG element to an HTML element, or an \
|
||||
HTML element to an SVG. Typically, this occurs when you create an \
|
||||
<a/> or <script/> with the `view` macro and append it to an SVG, but \
|
||||
the framework assumed it was HTML when you created it. To specify \
|
||||
that it is an SVG element, use <svg::{{tag name}}/> in the view \
|
||||
macro."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +95,8 @@ macro_rules! generate_math_tags {
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
gloo::console::warn!(
|
||||
"element with id",
|
||||
format!("_{id}"),
|
||||
"not found, ignoring it for hydration"
|
||||
crate::warn!(
|
||||
"element with id {id} not found, ignoring it for hydration"
|
||||
);
|
||||
|
||||
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
|
||||
|
||||
@@ -92,10 +92,8 @@ macro_rules! generate_svg_tags {
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
gloo::console::warn!(
|
||||
"element with id",
|
||||
format!("_{id}"),
|
||||
"not found, ignoring it for hydration"
|
||||
crate::warn!(
|
||||
"element with id {id} not found, ignoring it for hydration"
|
||||
);
|
||||
|
||||
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
|
||||
|
||||
@@ -21,7 +21,7 @@ cfg_if! {
|
||||
while let Ok(Some(node)) = walker.next_node() {
|
||||
if let Some(content) = node.text_content() {
|
||||
if let Some(hk) = content.strip_prefix("hk=") {
|
||||
if let Some(hk) = hk.split("|").next() {
|
||||
if let Some(hk) = hk.split('|').next() {
|
||||
map.insert(hk.into(), node.unchecked_into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,10 +308,8 @@ impl Comment {
|
||||
|
||||
marker.remove();
|
||||
} else {
|
||||
gloo::console::warn!(
|
||||
"component with id",
|
||||
id,
|
||||
"not found, ignoring it for hydration"
|
||||
crate::warn!(
|
||||
"component with id {id} not found, ignoring it for hydration"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +49,9 @@ impl Attribute {
|
||||
|
||||
/// Converts the attribute to its HTML value at that moment, not including
|
||||
/// the attribute name, so it can be rendered on the server.
|
||||
pub fn as_nameless_value_string(&self) -> String {
|
||||
pub fn as_nameless_value_string(&self) -> Option<String> {
|
||||
match self {
|
||||
Attribute::String(value) => value.to_string(),
|
||||
Attribute::String(value) => Some(value.to_string()),
|
||||
Attribute::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(_, f) = value {
|
||||
@@ -59,11 +59,16 @@ impl Attribute {
|
||||
}
|
||||
value.as_nameless_value_string()
|
||||
}
|
||||
Attribute::Option(_, value) => value
|
||||
.as_ref()
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_default(),
|
||||
Attribute::Bool(_) => String::new(),
|
||||
Attribute::Option(_, value) => {
|
||||
value.as_ref().map(|value| value.to_string())
|
||||
}
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
Some("".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +173,30 @@ attr_type!(f32);
|
||||
attr_type!(f64);
|
||||
attr_type!(char);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::borrow::Cow;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn attribute_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Attribute,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
match value {
|
||||
Attribute::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) {
|
||||
attribute_expression(&el, &name, new.clone());
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
_ => attribute_expression(el, &name, value),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn attribute_expression(
|
||||
el: &web_sys::Element,
|
||||
|
||||
@@ -66,6 +66,32 @@ impl<T: IntoClass> IntoClass for (Scope, T) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn class_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Class,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
let class_list = el.class_list();
|
||||
match value {
|
||||
Class::Fn(cx, f) => {
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) && (old.is_some() || new) {
|
||||
class_expression(&class_list, &name, new)
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Class::Value(value) => class_expression(&class_list, &name, value),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn class_expression(
|
||||
class_list: &web_sys::DomTokenList,
|
||||
|
||||
@@ -76,6 +76,38 @@ prop_type!(f32);
|
||||
prop_type!(f64);
|
||||
prop_type!(bool);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn property_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Property,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
match value {
|
||||
Property::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
if old.as_ref() != Some(&new)
|
||||
&& !(old.is_none() && new == wasm_bindgen::JsValue::UNDEFINED)
|
||||
{
|
||||
property_expression(&el, prop_name, new.clone())
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Property::Value(value) => {
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
property_expression(el, prop_name, value)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn property_expression(
|
||||
el: &web_sys::Element,
|
||||
|
||||
@@ -435,10 +435,13 @@ fn attribute_to_tokens_ssr(
|
||||
template.push_str(&value);
|
||||
template.push('"');
|
||||
} else {
|
||||
template.push_str("=\"{}\"");
|
||||
template.push_str("{}");
|
||||
let value = value.as_ref();
|
||||
holes.push(quote! {
|
||||
leptos::escape_attr(&{#value}.into_attribute(#cx).as_nameless_value_string()),
|
||||
&{#value}.into_attribute(#cx)
|
||||
.as_nameless_value_string()
|
||||
.map(|a| format!("{}=\"{}\"", #name, leptos::escape_attr(&a)))
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -555,7 +558,9 @@ fn set_class_attribute_ssr(
|
||||
template.push_str(" {}");
|
||||
let value = value.as_ref();
|
||||
holes.push(quote! {
|
||||
leptos::escape_attr(&(cx, #value).into_attribute(#cx).as_nameless_value_string()),
|
||||
&(cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
.map(|a| leptos::escape_attr(&a).to_string())
|
||||
.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1058,11 +1063,17 @@ fn is_self_closing(node: &NodeElement) -> bool {
|
||||
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" || tag_name == "use" {
|
||||
"_"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
first
|
||||
.map(|f| f.to_ascii_uppercase())
|
||||
.into_iter()
|
||||
.chain(chars)
|
||||
.collect()
|
||||
.collect::<String>()
|
||||
+ underscore
|
||||
}
|
||||
|
||||
fn is_svg_element(tag: &str) -> bool {
|
||||
|
||||
@@ -96,6 +96,11 @@ where
|
||||
self.0.with(|a| a.pending.read_only())
|
||||
}
|
||||
|
||||
/// Updates whether the action is currently pending.
|
||||
pub fn set_pending(&self, pending: bool) {
|
||||
self.0.with(|a| a.pending.set(pending))
|
||||
}
|
||||
|
||||
/// The URL associated with the action (typically as part of a server function.)
|
||||
/// This enables integration with the `ActionForm` component in `leptos_router`.
|
||||
pub fn url(&self) -> Option<String> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
74
meta/src/body.rs
Normal file
74
meta/src/body.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use crate::TextProp;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
/// Contains the current metadata for the document's `<body>`.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BodyContext {
|
||||
class: Rc<RefCell<Option<TextProp>>>,
|
||||
}
|
||||
|
||||
impl BodyContext {
|
||||
/// Converts the <body> metadata into an HTML string.
|
||||
pub fn as_string(&self) -> Option<String> {
|
||||
self.class
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|class| format!(" class=\"{}\"", class.get()))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BodyContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("TitleContext").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A component to set metadata on the document’s `<body>` element from
|
||||
/// within the application.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
/// let (prefers_dark, set_prefers_dark) = create_signal(cx, false);
|
||||
/// let body_class = move || if prefers_dark() {
|
||||
/// "dark".to_string()
|
||||
/// } else {
|
||||
/// "light".to_string()
|
||||
/// };
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Body class=body_class/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
pub fn Body(
|
||||
cx: Scope,
|
||||
/// The `class` attribute on the `<body>`.
|
||||
#[prop(optional, into)]
|
||||
class: Option<TextProp>,
|
||||
) -> impl IntoView {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
let el = document().body().expect("there to be a <body> element");
|
||||
|
||||
if let Some(class) = class {
|
||||
create_render_effect(cx, move |_| {
|
||||
let value = class.get();
|
||||
_ = el.set_attribute("class", &value);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let meta = crate::use_head(cx);
|
||||
*meta.body.class.borrow_mut() = class;
|
||||
}
|
||||
}
|
||||
}
|
||||
85
meta/src/html.rs
Normal file
85
meta/src/html.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use crate::TextProp;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
/// Contains the current metadata for the document's `<html>`.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct HtmlContext {
|
||||
lang: Rc<RefCell<Option<TextProp>>>,
|
||||
dir: Rc<RefCell<Option<TextProp>>>,
|
||||
}
|
||||
|
||||
impl HtmlContext {
|
||||
/// Converts the <html> metadata into an HTML string.
|
||||
pub fn as_string(&self) -> Option<String> {
|
||||
match (self.lang.borrow().as_ref(), self.dir.borrow().as_ref()) {
|
||||
(None, None) => None,
|
||||
(Some(lang), None) => Some(format!(" lang=\"{}\"", lang.get())),
|
||||
(None, Some(dir)) => Some(format!(" dir=\"{}\"", dir.get())),
|
||||
(Some(lang), Some(dir)) => {
|
||||
Some(format!(" lang=\"{}\" dir=\"{}\"", lang.get(), dir.get()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for HtmlContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("TitleContext").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A component to set metadata on the document’s `<html>` element from
|
||||
/// within the application.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Html lang="he" dir="rtl"/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
pub fn Html(
|
||||
cx: Scope,
|
||||
/// The `lang` attribute on the `<html>`.
|
||||
#[prop(optional, into)]
|
||||
lang: Option<TextProp>,
|
||||
/// The `dir` attribute on the `<html>`.
|
||||
#[prop(optional, into)]
|
||||
dir: Option<TextProp>,
|
||||
) -> impl IntoView {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
let el = document().document_element().expect("there to be a <html> element");
|
||||
|
||||
if let Some(lang) = lang {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |_| {
|
||||
let value = lang.get();
|
||||
_ = el.set_attribute("lang", &value);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(dir) = dir {
|
||||
create_render_effect(cx, move |_| {
|
||||
let value = dir.get();
|
||||
_ = el.set_attribute("dir", &value);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let meta = crate::use_head(cx);
|
||||
*meta.html.lang.borrow_mut() = lang;
|
||||
*meta.html.dir.borrow_mut() = dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,12 +55,16 @@ use std::{
|
||||
|
||||
use leptos::{leptos_dom::debug_warn, *};
|
||||
|
||||
mod body;
|
||||
mod html;
|
||||
mod link;
|
||||
mod meta_tags;
|
||||
mod script;
|
||||
mod style;
|
||||
mod stylesheet;
|
||||
mod title;
|
||||
pub use body::*;
|
||||
pub use html::*;
|
||||
pub use link::*;
|
||||
pub use meta_tags::*;
|
||||
pub use script::*;
|
||||
@@ -74,13 +78,19 @@ pub use title::*;
|
||||
/// [provide_meta_context].
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct MetaContext {
|
||||
pub(crate) title: TitleContext,
|
||||
pub(crate) tags: MetaTagsContext,
|
||||
/// Metadata associated with the `<html>` element
|
||||
pub html: HtmlContext,
|
||||
/// Metadata associated with the `<title>` element.
|
||||
pub title: TitleContext,
|
||||
/// Metadata associated with the `<body>` element
|
||||
pub body: BodyContext,
|
||||
/// Other metadata tags.
|
||||
pub tags: MetaTagsContext,
|
||||
}
|
||||
|
||||
/// Manages all of the element created by components.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct MetaTagsContext {
|
||||
pub struct MetaTagsContext {
|
||||
next_id: Rc<Cell<MetaTagId>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
els: Rc<RefCell<HashMap<String, (HtmlElement<AnyElement>, Scope, Option<web_sys::Element>)>>>,
|
||||
@@ -93,7 +103,8 @@ impl std::fmt::Debug for MetaTagsContext {
|
||||
}
|
||||
|
||||
impl MetaTagsContext {
|
||||
#[cfg(feature = "ssr")]
|
||||
/// Converts metadata tags into an HTML string.
|
||||
#[cfg(any(feature = "ssr", docs))]
|
||||
pub fn as_string(&self) -> String {
|
||||
self.els
|
||||
.borrow()
|
||||
@@ -102,6 +113,7 @@ impl MetaTagsContext {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn register(&self, cx: Scope, id: String, builder_el: HtmlElement<AnyElement>) {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
@@ -209,7 +221,7 @@ impl MetaContext {
|
||||
///
|
||||
/// # #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
|
||||
/// run_scope(create_runtime(), |cx| {
|
||||
/// provide_context(cx, MetaContext::new());
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// let app = view! { cx,
|
||||
/// <main>
|
||||
|
||||
@@ -55,7 +55,7 @@ where
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_context(cx, MetaContext::new());
|
||||
/// provide_meta_context(cx);
|
||||
/// let formatter = |text| format!("{text} — Leptos Online");
|
||||
///
|
||||
/// view! { cx,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn Form<A>(
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
|
||||
/// Component children; should include the HTML of the form elements.
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
A: ToHref + 'static,
|
||||
@@ -136,7 +136,7 @@ pub fn ActionForm<I, O>(
|
||||
/// manually using [leptos_server::Action::using_server_fn].
|
||||
action: Action<I, Result<O, ServerFnError>>,
|
||||
/// Component children; should include the HTML of the form elements.
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
I: Clone + ServerFn + 'static,
|
||||
@@ -155,7 +155,10 @@ where
|
||||
let on_form_data = Rc::new(move |form_data: &web_sys::FormData| {
|
||||
let data = action_input_from_form_data(form_data);
|
||||
match data {
|
||||
Ok(data) => input.set(Some(data)),
|
||||
Ok(data) => {
|
||||
input.set(Some(data));
|
||||
action.set_pending(true);
|
||||
}
|
||||
Err(e) => log::error!("{e}"),
|
||||
}
|
||||
});
|
||||
@@ -167,11 +170,6 @@ where
|
||||
JsFuture::from(resp.text().expect("couldn't get .text() from Response")).await;
|
||||
match body {
|
||||
Ok(json) => {
|
||||
log::debug!(
|
||||
"body is {:?}\nO is {:?}",
|
||||
json.as_string().unwrap(),
|
||||
std::any::type_name::<O>()
|
||||
);
|
||||
match O::from_json(
|
||||
&json.as_string().expect("couldn't get String from JsString"),
|
||||
) {
|
||||
@@ -182,7 +180,9 @@ where
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("{e:?}"),
|
||||
}
|
||||
};
|
||||
input.set(None);
|
||||
action.set_pending(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -210,7 +210,7 @@ pub fn MultiActionForm<I, O>(
|
||||
/// manually using [leptos_server::Action::using_server_fn].
|
||||
action: MultiAction<I, Result<O, ServerFnError>>,
|
||||
/// Component children; should include the HTML of the form elements.
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
I: Clone + ServerFn + 'static,
|
||||
|
||||
@@ -65,7 +65,7 @@ pub fn A<H>(
|
||||
#[prop(optional, into)]
|
||||
class: Option<MaybeSignal<String>>,
|
||||
/// The nodes or elements to be shown inside the link.
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
H: ToHref + 'static,
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn Route<E, F, P>(
|
||||
view: F,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Box<dyn FnOnce(Scope) -> Fragment>>,
|
||||
children: Option<Children>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
E: IntoView,
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn Router(
|
||||
/// The `<Router/>` should usually wrap your whole page. It can contain
|
||||
/// any elements, and should include a [Routes](crate::Routes) component somewhere
|
||||
/// to define and display [Route](crate::Route)s.
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
// create a new RouterContext and provide it to every component beneath the router
|
||||
let router = RouterContext::new(cx, base, fallback);
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::{
|
||||
pub fn Routes(
|
||||
cx: Scope,
|
||||
#[prop(optional)] base: Option<String>,
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
let router = use_context::<RouterContext>(cx)
|
||||
.expect("<Routes/> component should be nested within a <Router/>.");
|
||||
|
||||
Reference in New Issue
Block a user