Compare commits

..

9 Commits

Author SHA1 Message Date
Greg Johnston
8c10dd0fe1 Check server function name uniqueness at registration time, and stop leaking information by including src file in release mode. 2023-01-26 21:41:33 -05:00
Greg Johnston
e883d9589c Fix broken links and other issues in docs 2023-01-26 21:34:37 -05:00
Greg Johnston
203c032546 Document which types are provided as contexts in server integrations 2023-01-26 21:21:16 -05:00
Greg Johnston
57ac135cdc Add "Is it production ready?" to FAQs 2023-01-26 21:01:28 -05:00
Greg Johnston
ebd1f7f44e Document need to enable features on leptos_router and leptos_meta 2023-01-26 20:57:12 -05:00
Greg Johnston
5df8f84403 Restore missing CSR READMEs 2023-01-26 20:54:46 -05:00
Greg Johnston
c454c39975 Fix example links in docs 2023-01-26 20:54:38 -05:00
Greg Johnston
4467bd17f7 Suppress warning for unused cx in server function arguments 2023-01-26 20:16:25 -05:00
Greg Johnston
a9e9bd9344 implements From<Signal<T>> for MaybeSignal<T> 2023-01-26 20:09:17 -05:00
49 changed files with 333 additions and 1205 deletions

View File

@@ -24,17 +24,17 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.1.3"
version = "0.1.1"
[workspace.dependencies]
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" }
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" }
[profile.release]
codegen-units = 1

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.1"
leptos = "0.0.18"

View File

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

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.1"
leptos = "0.0.18"

View File

@@ -4,9 +4,7 @@ fn main() {
mount_to_body(|cx| {
let name = "gbj";
let userid = 0;
// This will be filled by _ref=input below.
let input_element = NodeRef::<HtmlElement<Input>>::new(cx);
let _input_element: Element;
view! {
cx,
@@ -19,7 +17,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

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.1"
leptos = "0.0.18"

View File

@@ -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(create_runtime(), |cx| {
_ = create_scope(|cx| {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();

View File

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

View File

@@ -11,7 +11,6 @@ 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"

View File

@@ -13,5 +13,5 @@ futures = "0.3"
leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
parking_lot = "0.12.1"
regex = "1.7.0"
tokio = "1.24.1"

View File

@@ -18,9 +18,9 @@ use http::StatusCode;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use std::sync::Arc;
use tokio::sync::RwLock;
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
@@ -49,25 +49,25 @@ pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
impl ResponseOptions {
/// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
pub fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write();
pub async fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write().await;
*writable = parts
}
/// Set the status of the returned Response
pub fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write();
pub async fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.status = Some(status);
}
/// Insert a header, overwriting any previous value with the same key
pub fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write();
pub async fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub fn append_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write();
pub async fn append_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
}
@@ -76,13 +76,15 @@ impl ResponseOptions {
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead.
pub fn redirect(cx: leptos::Scope, path: &str) {
pub async fn redirect(cx: leptos::Scope, path: &str) {
let response_options = use_context::<ResponseOptions>(cx).unwrap();
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
);
response_options.set_status(StatusCode::FOUND).await;
response_options
.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
)
.await;
}
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
@@ -180,7 +182,7 @@ pub fn handle_server_fns_with_context(
runtime.dispose();
let mut res: HttpResponseBuilder;
let mut res_parts = res_options.0.write();
let mut res_parts = res_options.0.write().await;
if accept_header == Some("application/json")
|| accept_header == Some("application/x-www-form-urlencoded")
@@ -337,7 +339,9 @@ where
}
};
stream_app(&options, app, res_options, additional_context).await
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options, additional_context).await
}
})
}
@@ -425,7 +429,9 @@ where
}
};
stream_app(&options, app, res_options, |_cx| {}).await
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options, |_cx| {}).await
}
})
}
@@ -451,31 +457,23 @@ 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, scope) = render_to_stream_with_prefix_undisposed_with_context(
let (stream, runtime, _) = render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| {
let meta = use_context::<MetaContext>(cx);
let head = meta
.as_ref()
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
let body_meta = meta
.as_ref()
.and_then(|meta| meta.body.as_string())
.unwrap_or_default();
format!("{head}</head><body{body_meta}>").into()
format!("{head}</head><body>").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)
@@ -491,7 +489,7 @@ async fn stream_app(
let second_chunk = stream.next().await;
let third_chunk = stream.next().await;
let res_options = res_options.0.read();
let res_options = res_options.0.read().await;
let (status, mut headers) = (res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
@@ -518,7 +516,7 @@ async fn stream_app(
res
}
fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (String, String) {
fn html_parts(options: &LeptosOptions) -> (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
@@ -559,12 +557,9 @@ fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (S
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{html_metadata}>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>

View File

@@ -17,4 +17,3 @@ leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_config = { workspace = true }
tokio = { version = "1.0", features = ["full"] }
parking_lot = "0.12.1"

View File

@@ -19,9 +19,8 @@ use hyper::body;
use leptos::*;
use leptos_meta::MetaContext;
use leptos_router::*;
use parking_lot::RwLock;
use std::{io, pin::Pin, sync::Arc};
use tokio::{task::spawn_blocking, task::LocalSet};
use tokio::{sync::RwLock, task::spawn_blocking, task::LocalSet};
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
/// to construct this for Leptos to use in Axum
@@ -60,25 +59,25 @@ pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
impl ResponseOptions {
/// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
pub fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write();
pub async fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write().await;
*writable = parts
}
/// Set the status of the returned Response
pub fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write();
pub async fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.status = Some(status);
}
/// Insert a header, overwriting any previous value with the same key
pub fn insert_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write();
pub async fn insert_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub fn append_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write();
pub async fn append_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
}
@@ -87,13 +86,15 @@ impl ResponseOptions {
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
pub fn redirect(cx: leptos::Scope, path: &str) {
pub async fn redirect(cx: leptos::Scope, path: &str) {
let response_options = use_context::<ResponseOptions>(cx).unwrap();
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
);
response_options.set_status(StatusCode::FOUND).await;
response_options
.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
)
.await;
}
/// Decomposes an HTTP request into its parts, allowing you to read its headers
@@ -225,7 +226,7 @@ async fn handle_server_fns_inner(
// Add headers from ResponseParts if they exist. These should be added as long
// as the server function returns an OK response
let res_options_outer = res_options.unwrap().0;
let res_options_inner = res_options_outer.read();
let res_options_inner = res_options_outer.read().await;
let (status, mut res_headers) = (
res_options_inner.status,
res_options_inner.headers.clone(),
@@ -419,6 +420,61 @@ 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({
@@ -462,24 +518,19 @@ 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();
let new_res_parts = res_options.0.read().clone();
let new_res_parts = res_options.0.read().await.clone();
let mut writable = res_options2.0.write();
let mut writable = res_options2.0.write().await;
*writable = new_res_parts;
runtime.dispose();
@@ -492,7 +543,12 @@ where
}
});
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
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))),
);
// 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;
@@ -500,7 +556,7 @@ where
let third_chunk = stream.next().await;
// Extract the resources now that they've been rendered
let res_options = res_options3.0.read();
let res_options = res_options3.0.read().await;
let complete_stream = futures::stream::iter([
first_chunk.unwrap(),
@@ -525,65 +581,6 @@ 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.
@@ -604,7 +601,7 @@ where
.run_until(async move {
tokio::task::spawn_local(async move {
let routes = leptos_router::generate_route_list_inner(app_fn);
let mut writable = routes_inner.0.write();
let mut writable = routes_inner.0.write().await;
*writable = routes;
})
.await
@@ -612,7 +609,7 @@ where
})
.await;
let routes = routes.0.read().to_owned();
let routes = routes.0.read().await.to_owned();
// Axum's Router defines Root routes as "/" not ""
let routes: Vec<String> = routes
.into_iter()

View File

@@ -1,9 +0,0 @@
.DS_Store
/node_modules
**/*.rs.bk
wasm-pack.log
build/
/target
/dist

View File

@@ -1,30 +0,0 @@
[package]
name = "leptos-worker"
version = "0.0.0"
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
cfg-if = "0.1.2"
worker = "0.0.11"
serde_json = "1.0.67"
leptos = { path = "/Users/gjohnston/Documents/Projects/leptos-main/leptos/leptos", default-features = false, features = ["ssr"] }
leptos_router = { path = "/Users/gjohnston/Documents/Projects/leptos-main/leptos/router", default-features = false, features = ["ssr"] }
leptos_meta = { path = "/Users/gjohnston/Documents/Projects/leptos-main/leptos/meta", default-features = false, features = ["ssr"]}
tracing = "0.1"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.1", optional = true }
parking_lot = "0.12.1"
futures = "0.3.26"
[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"

View File

@@ -1,11 +0,0 @@
{
"private": true,
"version": "0.0.0",
"scripts": {
"deploy": "wrangler publish",
"dev": "wrangler dev --local"
},
"devDependencies": {
"wrangler": "^2.0.0"
}
}

View File

@@ -1,463 +0,0 @@
// Reader's guide...
// Look at these functions:
// - main
// - leptos_routes
// - render_app_to_string
// - <App/>
//
// TODO
// - What's the best way to serve static client-side JS/Wasm/CSS?
// - Does `leptos_routes` run on every single request? Simple log to check.
// The issue here is that it renders the app to extract the routes.
// This is O(n + 1) for N requests to the server, but this is expensive
// if this routing process is re-created every time.
use std::{sync::Arc, future::Future};
use futures::StreamExt;
use serde_json::json;
use worker::*;
mod utils;
fn log_request(req: &Request) {
console_log!(
"{} - [{}], located at: {:?}, within: {}",
Date::now().to_string(),
req.path(),
req.cf().coordinates().unwrap_or_default(),
req.cf().region().unwrap_or_else(|| "unknown region".into())
);
}
#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
log_request(&req);
utils::set_panic_hook();
// Use Cloudflare Worker router to serve all requests
let router = Router::new();
router
// leptos_routes mounts all possible routes from your Leptos app
// as Cloudflare router routes
.leptos_routes(
generate_route_list(|cx| view! { cx, <App/> }),
// this function takes the HTTP Request and router context (unused)
// `render_app_to_string` provides `Request` via context, so we can access
// the HTTP request during server rendering
|req, _| render_app_to_string("/pkg", "leptos_worker", req, |cx| view! { cx, <App/> })
)
.run(req, env)
.await
}
// integration
use parking_lot::RwLock;
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
#[derive(Debug, Clone, Default)]
pub struct ResponseParts {
pub headers: worker::Headers,
pub status: Option<u16>,
}
impl ResponseParts {
/// Insert a header, overwriting any previous value with the same key
pub fn set_header(&mut self, key: &str, value: &str) -> Result<()> {
self.headers.set(key, value)
}
/// Append a header, leaving any header with the same key intact
pub fn append_header(&mut self, key: &str, value: &str) -> Result<()> {
self.headers.append(key, value)
}
}
/// Adding this Struct to your Scope inside of a Server Fn or Elements will allow you to override details of the Response
/// like StatusCode and add Headers/Cookies. Because Elements and Server Fns are lower in the tree than the Response generation
/// code, it needs to be wrapped in an `Arc<RwLock<>>` so that it can be surfaced
#[derive(Debug, Clone, Default)]
pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
impl ResponseOptions {
/// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
pub fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write();
*writable = parts
}
/// Set the status of the returned Response
pub fn set_status(&self, status: u16) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.status = Some(status);
}
/// Set a header, overwriting any previous value with the same key
pub fn set_header(&self, key: &str, value: &str) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.set(key, value);
}
/// Append a header, leaving any header with the same key intact
pub fn append_header(&self, key: &str, value: &str) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
}
}
pub fn generate_route_list<IV>(app_fn: impl FnOnce(leptos::Scope) -> IV + 'static) -> Vec<String>
where
IV: IntoView + 'static,
{
let mut routes = leptos_router::generate_route_list_inner(app_fn);
// replace empty paths with "/"
// otherwise, CF router works very similar to Leptos
// with :params and *blobs
routes = routes
.iter()
.map(|s| {
if s.is_empty() {
return "/".to_string();
}
s.to_string()
})
.collect();
if routes.is_empty() {
vec!["/".to_string()]
} else {
routes
}
}
// This is having one of those annoying type/trait issues, which I'll figure out later
/*
async fn stream_app(
pkg_url: &str,
crate_name: &str,
app: impl FnOnce(leptos::Scope) -> leptos::View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> Response {
let (stream, runtime, scope) = render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| {
let meta = use_context::<MetaContext>(cx);
let head = meta
.as_ref()
.map(|meta| meta.dehydrate())
.unwrap_or_default();
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 meta = use_context::<MetaContext>(cx);
// see comment above re `leptos_meta`
let html_meta = meta.as_ref().and_then(|mc| mc.html.as_string()).unwrap_or_default();
let head = format!(r#"
<!DOCTYPE html>
<html{html_meta}>
<head>
<link rel="modulepreload" href="/{pkg_url}/{crate_name}.js">
<link rel="preload" href="/{pkg_url}/{crate_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_url}/{crate_name}.js'; init('/{pkg_url}/{crate_name}_bg.wasm').then(hydrate);</script>"#);
let tail = "</body></html>";
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })
.chain(stream)
.chain(futures::stream::once(async move {
runtime.dispose();
tail.to_string()
}))
);
// 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;
let second_chunk = stream.next().await;
let third_chunk = stream.next().await;
let res_options = res_options.0.read();
let (status, mut headers) = (res_options.status, res_options.headers.clone());
let status = status.unwrap_or(200);
let complete_stream = futures::stream::iter([
first_chunk.unwrap(),
second_chunk.unwrap(),
third_chunk.unwrap(),
])
.chain(stream)
.map(|html| Ok(html));
Response::from_stream(stream)
.expect("to create Response from Stream")
.with_status(status)
.with_headers(headers)
}
pub fn render_app_to_stream_with_additional_context<IV>(
pkg_url: &str,
crate_name: &str,
req: Request,
app_fn: impl FnOnce(Scope) -> IV + Clone + 'static,
additional_context: impl FnOnce(Scope) + Clone + Send + 'static
)-> Result<Response>
where IV: IntoView
{
let pkg_url = pkg_url.to_owned();
let crate_name = crate_name.to_owned();
let runtime = create_runtime();
let (html, _, _) = run_scope_undisposed(runtime, move |cx| {
let integration = RouterIntegrationContext::new(ServerIntegration { path: format!("https://leptos.dev{}", req.path()) });
provide_context(cx, integration);
provide_context(cx, MetaContext::new());
provide_context(cx, Arc::new(req.clone()));
let html = app_fn(cx).into_view(cx).render_to_string(cx);
let meta = use_context::<MetaContext>(cx);
let html_meta = meta.as_ref().and_then(|mc| mc.html.as_string()).unwrap_or_default();
let body_meta = meta.as_ref().and_then(|mc| mc.body.as_string()).unwrap_or_default();
let meta_tags = meta.map(|mc| mc.dehydrate()).unwrap_or_default();
format!(r#"
<!DOCTYPE html>
<html{html_meta}>
<head>
<link rel="modulepreload" href="/{pkg_url}/{crate_name}.js">
<link rel="preload" href="/{pkg_url}/{crate_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_url}/{crate_name}.js'; init('/{pkg_url}/{crate_name}_bg.wasm').then(hydrate);</script>
{meta_tags}
</head>
<body{body_meta}>
{html}
</body>
</html>"#)
});
runtime.dispose();
Response::from_html(html)
}*/
pub fn render_app_to_string<IV>(
pkg_url: &str,
crate_name: &str,
req: Request,
app_fn: impl FnOnce(Scope) -> IV + Clone + 'static
)-> Result<Response>
where IV: IntoView
{
// this stuff is so we know where to load client-side JS/Wasm from
let pkg_url = pkg_url.to_owned();
let crate_name = crate_name.to_owned();
let runtime = create_runtime();
// build the app shell
let (html, _, _) = run_scope_undisposed(runtime, move |cx| {
let integration = RouterIntegrationContext::new(ServerIntegration {
// add https://leptos.dev so URL crate doesn't fuss
path: format!("https://leptos.dev{}", req.path())
});
// this is how the router knows where we are when server rendering
provide_context(cx, integration);
// leptos_meta lets you
// 1. add attributes to the root <html> tag
// 2. add attributes to <body>
// 3. inject <link>, <style>, <meta>, and <script> tags
// into the <head>
// (all from within components that would otherwise be in the <body>)
//
// So we provide `MetaContext`, the app can use it, then
// (below) we extract some data from it to inject into HTML before serving
provide_context(cx, MetaContext::new());
// provide an Arc<Request> to be able to access headers, etc. during SSR
provide_context(cx, Arc::new(req.clone()));
// render the shell
let html = app_fn(cx).into_view(cx).render_to_string(cx);
// now inject `leptos_meta` stuff
let meta = use_context::<MetaContext>(cx);
let html_meta = meta.as_ref().and_then(|mc| mc.html.as_string()).unwrap_or_default();
let body_meta = meta.as_ref().and_then(|mc| mc.body.as_string()).unwrap_or_default();
let meta_tags = meta.map(|mc| mc.dehydrate()).unwrap_or_default();
format!(r#"
<!DOCTYPE html>
<html{html_meta}>
<head>
<link rel="modulepreload" href="/{pkg_url}/{crate_name}.js">
<link rel="preload" href="/{pkg_url}/{crate_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_url}/{crate_name}.js'; init('/{pkg_url}/{crate_name}_bg.wasm').then(hydrate);</script>
{meta_tags}
</head>
<body{body_meta}>
{html}
</body>
</html>"#)
});
runtime.dispose();
Response::from_html(html)
}
pub async fn render_preloaded_data_app_to_string<Data, Fut, IV>(
req: Request,
data_fn: impl Fn(&Request) -> Fut + Clone + 'static,
app_fn: impl FnOnce(Scope, Data) -> IV + Clone + 'static
) -> Result<Response>
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>>>,
IV: IntoView + 'static,
{
let data = data_fn(&req).await;
let data = match data {
Err(e) => return Response::error(e.to_string(), 500),
Ok(DataResponse::Response(r)) => return Ok(r),
Ok(DataResponse::Data(d)) => d
};
let runtime = create_runtime();
let (html, _, _) = run_scope_undisposed(runtime, move |cx| {
let integration = RouterIntegrationContext::new(ServerIntegration { path: format!("https://leptos.dev{}", req.path()) });
provide_context(cx, integration);
provide_context(cx, MetaContext::new());
provide_context(cx, Arc::new(req.clone()));
let html = app_fn(cx, data).into_view(cx).render_to_string(cx);
let meta = use_context::<MetaContext>(cx);
let html_meta = meta.as_ref().and_then(|mc| mc.html.as_string()).unwrap_or_default();
let body_meta = meta.as_ref().and_then(|mc| mc.body.as_string()).unwrap_or_default();
let meta_tags = meta.map(|mc| mc.dehydrate()).unwrap_or_default();
format!(r#"
<!DOCTYPE html>
<html{html_meta}>
<head>
{meta_tags}
</head>
<body{body_meta}>
{html}
</body>
</html>"#)
});
runtime.dispose();
Response::from_html(html)
}
pub enum DataResponse<T> {
Data(T),
Response(worker::Response),
}
/// This trait allows one to pass a list of routes and a render function to Cloudflare's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes<T> {
fn leptos_routes(
self,
paths: Vec<String>,
app_fn: fn(Request, worker::RouteContext<T>) -> Result<Response>
) -> Self;
fn leptos_preloaded_data_routes<Data, Fut, IV>(
self,
paths: Vec<String>,
data_fn: impl Fn(Request) -> Fut + Clone + 'static,
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
) -> Self
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>>>,
IV: IntoView + 'static;
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
impl<T> LeptosRoutes<T> for worker::Router<'_, T>
where T: 'static
{
fn leptos_routes(
self,
paths: Vec<String>,
app_fn: fn(Request, worker::RouteContext<T>) -> Result<Response>
) -> Self
{
let mut router = self;
for path in paths.iter() {
router = router.get(path, app_fn);
}
router
}
fn leptos_preloaded_data_routes<Data, Fut, IV>(
self,
paths: Vec<String>,
data_fn: impl Fn(Request) -> Fut + Clone + 'static,
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
) -> Self
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>>>,
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.get(
path,
|req, _| todo!() //render_preloaded_data_app_to_string(req, data_fn.clone(), app_fn.clone()),
);
}
router
}
}
// This app
use leptos::{component, Scope, IntoView, create_signal, view, render_to_string, provide_context, LeptosOptions, use_context, create_runtime, run_scope, run_scope_undisposed, get_configuration, render_to_stream_with_prefix_undisposed_with_context};
use leptos_router::*;
use leptos_meta::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<Meta name="color-scheme" content="dark"/>
<Title text="Hello from Leptos Cloudflare"/>
<nav>
<a href="/">"Home"</a>
<a href="/about">"About"</a>
</nav>
<main>
<Routes>
<Route path="/" view=|cx| view! { cx, <HomePage/> }/>
<Route path="/about" view=|cx| view! { cx, <About/> }/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn HomePage(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<h1>"Hello, Leptos Cloudflare!"</h1>
<button on:click=move |_| set_count.update(|n| *n += 1)>
"Click me: " {count}
</button>
}
}
#[component]
pub fn About(cx: Scope) -> impl IntoView {
view! { cx,
<h1>"About"</h1>
}
}

View File

@@ -1,12 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
// https://github.com/rustwasm/console_error_panic_hook#readme
if #[cfg(feature = "console_error_panic_hook")] {
extern crate console_error_panic_hook;
pub use self::console_error_panic_hook::set_once as set_panic_hook;
} else {
#[inline]
pub fn set_panic_hook() {}
}
}

View File

@@ -1,9 +0,0 @@
name = "" # todo
main = "build/worker/shim.mjs"
compatibility_date = "2022-01-20"
[vars]
WORKERS_RS_VERSION = "0.0.11"
[build]
command = "cargo install -q worker-build --version 0.0.7 && worker-build --release"

View File

@@ -16,7 +16,7 @@ leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_config = { workspace = true }
tracing = "0.1"
typed-builder = "0.12"
typed-builder = "0.11"
once_cell = "1.17.0"
[dev-dependencies]
@@ -24,30 +24,10 @@ leptos = { path = ".", default-features = false }
[features]
default = ["csr", "serde"]
csr = [
"leptos_dom/web",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
]
hydrate = [
"leptos_dom/web",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"leptos_server/hydrate",
]
ssr = [
"leptos_dom/ssr",
"leptos_macro/ssr",
"leptos_reactive/ssr",
"leptos_server/ssr",
]
stable = [
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
"leptos_server/stable",
]
csr = ["leptos_dom/web", "leptos_macro/csr", "leptos_reactive/csr", "leptos_server/csr"]
hydrate = ["leptos_dom/web", "leptos_macro/hydrate", "leptos_reactive/hydrate", "leptos_server/hydrate"]
ssr = ["leptos_dom/ssr", "leptos_macro/ssr", "leptos_reactive/ssr", "leptos_server/ssr"]
stable = ["leptos_dom/stable", "leptos_macro/stable", "leptos_reactive/stable", "leptos_server/stable"]
serde = ["leptos_reactive/serde"]
serde-lite = ["leptos_reactive/serde-lite"]
miniserde = ["leptos_reactive/miniserde"]

View File

@@ -1,36 +1,12 @@
use crate::Children;
use leptos_dom::{Errors, IntoView};
use leptos_dom::{Errors, Fragment, 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: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
/// A fallback that will be shown if an error occurs.
fallback: F,
) -> impl IntoView

View File

@@ -164,11 +164,3 @@ 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>;

View File

@@ -1,36 +1,16 @@
use crate::Children;
use leptos::component;
use leptos_dom::IntoView;
use leptos_dom::{Fragment, IntoView};
use leptos_reactive::Scope;
use once_cell::sync::Lazy;
/// 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>
/// }
/// # });
/// ```
/// A component that will show it's children when the passed in closure is True, and show the fallback
/// when the closure is false
#[component]
pub fn Show<F, W, IV>(
/// The scope the component is running in
cx: Scope,
/// The components Show wraps
children: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
/// A closure that returns a bool that determines whether this thing runs
when: W,
/// A closure that returns what gets rendered if the when statement is false

View File

@@ -129,22 +129,3 @@ 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>"
);
});
}

View File

@@ -14,4 +14,4 @@ fs = "0.0.5"
regex = "1.7.0"
serde = { version = "1.0.151", features = ["derive"] }
thiserror = "1.0.38"
typed-builder = "0.12"
typed-builder = "0.11"

View File

@@ -12,6 +12,7 @@ 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"
@@ -33,11 +34,8 @@ leptos = { path = "../leptos" }
[dependencies.web-sys]
version = "0.3"
features = [
"console",
"Comment",
"Document",
"DomTokenList",
"Location",
"Range",
"Text",
"HtmlCollection",
@@ -51,7 +49,6 @@ features = [
"DeviceMotionEvent",
"DeviceOrientationEvent",
"DragEvent",
"ErrorEvent",
"FocusEvent",
"GamepadEvent",
"HashChangeEvent",

View File

@@ -19,28 +19,23 @@ where
match use_context::<RwSignal<Errors>>(cx) {
Some(errors) => {
let id = HydrationCtx::id();
errors.update({
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let id = id.clone();
move |errors: &mut Errors| errors.insert(id, error)
});
errors.update(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(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);
});
});
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);
});
});
});
}
}
}
}
None => {
#[cfg(debug_assertions)]

View File

@@ -6,8 +6,12 @@ use cfg_if::cfg_if;
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use crate::events::*;
use crate::macro_helpers::*;
use crate::macro_helpers::Property;
use crate::macro_helpers::{
attribute_expression, class_expression, property_expression,
};
use crate::{mount_child, MountKind};
use leptos_reactive::create_render_effect;
use once_cell::unsync::Lazy as LazyCell;
use wasm_bindgen::JsCast;
@@ -42,7 +46,7 @@ cfg_if! {
use crate::{
ev::EventDescriptor,
hydration::HydrationCtx,
macro_helpers::{IntoAttribute, IntoClass, IntoProperty},
macro_helpers::{Attribute, Class, IntoAttribute, IntoClass, IntoProperty},
Element, Fragment, IntoView, NodeRef, Text, View,
};
use leptos_reactive::Scope;
@@ -199,8 +203,10 @@ impl Custom {
el.unchecked_into()
} else {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
crate::document().create_element(&name).unwrap()
@@ -489,18 +495,26 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
attribute_helper(
self.element.as_ref(),
name,
attr.into_attribute(self.cx),
);
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),
};
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);
@@ -540,16 +554,26 @@ 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);
class_helper(el, name, value);
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),
};
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);
@@ -585,7 +609,25 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
let name = name.into();
let value = value.into_property(self.cx);
let el = self.element.as_ref();
property_helper(el, name, value);
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(not(all(target_arch = "wasm32", feature = "web")))]
@@ -806,18 +848,63 @@ macro_rules! generate_html_tags {
let id = HydrationCtx::id();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let element = create_leptos_element(
&stringify!([<$tag:upper>]),
id,
|| {
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"
);
[<$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"))]
@@ -892,70 +979,24 @@ 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")
&& parent.namespace_uri() != el.element.namespace_uri()
|| el.name == "title"
{
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."
)
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."
)
}
}
}
}

View File

@@ -95,8 +95,10 @@ macro_rules! generate_math_tags {
el.unchecked_into()
} else {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]

View File

@@ -92,8 +92,10 @@ macro_rules! generate_svg_tags {
el.unchecked_into()
} else {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]

View File

@@ -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());
}
}

View File

@@ -308,8 +308,10 @@ impl Comment {
marker.remove();
} else {
crate::warn!(
"component with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"component with id",
id,
"not found, ignoring it for hydration"
);
}
}

View File

@@ -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) -> Option<String> {
pub fn as_nameless_value_string(&self) -> String {
match self {
Attribute::String(value) => Some(value.to_string()),
Attribute::String(value) => value.to_string(),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
@@ -59,16 +59,11 @@ impl Attribute {
}
value.as_nameless_value_string()
}
Attribute::Option(_, value) => {
value.as_ref().map(|value| value.to_string())
}
Attribute::Bool(include) => {
if *include {
Some("".to_string())
} else {
None
}
}
Attribute::Option(_, value) => value
.as_ref()
.map(|value| value.to_string())
.unwrap_or_default(),
Attribute::Bool(_) => String::new(),
}
}
}
@@ -174,31 +169,7 @@ 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(
pub fn attribute_expression(
el: &web_sys::Element,
attr_name: &str,
value: Attribute,

View File

@@ -67,33 +67,7 @@ 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(
pub fn class_expression(
class_list: &web_sys::DomTokenList,
class_name: &str,
value: bool,

View File

@@ -77,39 +77,7 @@ 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(
pub fn property_expression(
el: &web_sys::Element,
prop_name: &str,
value: JsValue,

View File

@@ -20,6 +20,7 @@ prettyplease = "0.1"
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
serde = "1"
syn = { version = "1", features = ["full"] }
syn-rsx = "0.9"
uuid = { version = "1", features = ["v4"] }
@@ -31,8 +32,7 @@ convert_case = "0.6.0"
[dev-dependencies]
log = "0.4"
typed-builder = "0.12"
leptos = { path = "../leptos" }
typed-builder = "0.11"
[features]
default = ["ssr"]

View File

@@ -435,13 +435,10 @@ 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! {
&{#value}.into_attribute(#cx)
.as_nameless_value_string()
.map(|a| format!("{}=\"{}\"", #name, leptos::escape_attr(&a)))
.unwrap_or_default(),
leptos::escape_attr(&{#value}.into_attribute(#cx).as_nameless_value_string()),
})
}
}
@@ -558,9 +555,7 @@ fn set_class_attribute_ssr(
template.push_str(" {}");
let value = value.as_ref();
holes.push(quote! {
&(cx, #value).into_attribute(#cx).as_nameless_value_string()
.map(|a| leptos::escape_attr(&a).to_string())
.unwrap_or_default(),
leptos::escape_attr(&(cx, #value).into_attribute(#cx).as_nameless_value_string()),
});
}
}
@@ -1063,17 +1058,11 @@ 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::<String>()
+ underscore
.collect()
}
fn is_svg_element(tag: &str) -> bool {

View File

@@ -96,11 +96,6 @@ 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> {

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.1.3"
version = "0.1.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -11,7 +11,7 @@ description = "Tools to set HTML metadata in the Leptos web framework."
cfg-if = "1"
leptos = { workspace = true }
tracing = "0.1"
typed-builder = "0.12"
typed-builder = "0.11"
[dependencies.web-sys]
version = "0.3"

View File

@@ -1,74 +0,0 @@
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 documents `<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;
}
}
}

View File

@@ -1,85 +0,0 @@
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 documents `<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;
}
}
}

View File

@@ -55,16 +55,12 @@ 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::*;
@@ -78,19 +74,13 @@ pub use title::*;
/// [provide_meta_context].
#[derive(Clone, Default, Debug)]
pub struct MetaContext {
/// 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,
pub(crate) title: TitleContext,
pub(crate) tags: MetaTagsContext,
}
/// Manages all of the element created by components.
#[derive(Clone, Default)]
pub struct MetaTagsContext {
pub(crate) struct MetaTagsContext {
next_id: Rc<Cell<MetaTagId>>,
#[allow(clippy::type_complexity)]
els: Rc<RefCell<HashMap<String, (HtmlElement<AnyElement>, Scope, Option<web_sys::Element>)>>>,
@@ -103,8 +93,7 @@ impl std::fmt::Debug for MetaTagsContext {
}
impl MetaTagsContext {
/// Converts metadata tags into an HTML string.
#[cfg(any(feature = "ssr", docs))]
#[cfg(feature = "ssr")]
pub fn as_string(&self) -> String {
self.els
.borrow()
@@ -113,7 +102,6 @@ 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"))] {
@@ -221,7 +209,7 @@ impl MetaContext {
///
/// # #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
/// run_scope(create_runtime(), |cx| {
/// provide_meta_context(cx);
/// provide_context(cx, MetaContext::new());
///
/// let app = view! { cx,
/// <main>

View File

@@ -55,7 +55,7 @@ where
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
/// provide_context(cx, MetaContext::new());
/// let formatter = |text| format!("{text} — Leptos Online");
///
/// view! { cx,

View File

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

View File

@@ -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: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> 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: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView
where
I: Clone + ServerFn + 'static,
@@ -155,10 +155,7 @@ 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));
action.set_pending(true);
}
Ok(data) => input.set(Some(data)),
Err(e) => log::error!("{e}"),
}
});
@@ -170,6 +167,11 @@ 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"),
) {
@@ -180,9 +182,7 @@ 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: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView
where
I: Clone + ServerFn + 'static,

View File

@@ -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: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView
where
H: ToHref + 'static,

View File

@@ -29,7 +29,7 @@ pub fn Route<E, F, P>(
view: F,
/// `children` may be empty or include nested routes.
#[prop(optional)]
children: Option<Children>,
children: Option<Box<dyn FnOnce(Scope) -> Fragment>>,
) -> impl IntoView
where
E: IntoView,

View File

@@ -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: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView {
// create a new RouterContext and provide it to every component beneath the router
let router = RouterContext::new(cx, base, fallback);

View File

@@ -22,7 +22,7 @@ use crate::{
pub fn Routes(
cx: Scope,
#[prop(optional)] base: Option<String>,
children: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView {
let router = use_context::<RouterContext>(cx)
.expect("<Routes/> component should be nested within a <Router/>.");