mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
16 Commits
server-rpc
...
remove-mut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abc1c07053 | ||
|
|
8a7ff0414a | ||
|
|
b3c7de8460 | ||
|
|
03821b8edb | ||
|
|
db69145fd9 | ||
|
|
51142ad894 | ||
|
|
8ea73565de | ||
|
|
19db83c933 | ||
|
|
c034e84b1d | ||
|
|
292c3d8bb1 | ||
|
|
ae0fad5465 | ||
|
|
15f3d66ef0 | ||
|
|
1041d04d9e | ||
|
|
beaeb769d6 | ||
|
|
7168f24dcb | ||
|
|
b3217c6523 |
@@ -20,7 +20,6 @@ members = [
|
||||
"examples/counters",
|
||||
"examples/counters-stable",
|
||||
"examples/fetch",
|
||||
"examples/gtk",
|
||||
"examples/hackernews/hackernews-app",
|
||||
"examples/hackernews/hackernews-client",
|
||||
"examples/hackernews/hackernews-server",
|
||||
@@ -31,11 +30,13 @@ members = [
|
||||
"examples/todomvc-ssr/todomvc-ssr-server",
|
||||
]
|
||||
exclude = [
|
||||
"benchmarks"
|
||||
"benchmarks",
|
||||
# not gonna lie, this is because my arm64 mac fails when linking a GTK binary
|
||||
"examples/gtk",
|
||||
]
|
||||
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
opt-level = 'z'
|
||||
|
||||
29
Makefile.toml
Normal file
29
Makefile.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
############
|
||||
# A make file for cargo-make, please install it with:
|
||||
# cargo install --force cargo-make
|
||||
############
|
||||
|
||||
[config]
|
||||
# make tasks run at the workspace root
|
||||
default_to_workspace = false
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["build", "test"]
|
||||
|
||||
[tasks.build]
|
||||
clear = true
|
||||
dependencies = ["build-all"]
|
||||
|
||||
[tasks.build-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = ["test-all"]
|
||||
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -1,5 +0,0 @@
|
||||
- output type can't always be `i32` (parse it in macro and use)
|
||||
- need to implement `from_form_data` using `form_urlencoded` and mapping each value to `.from_json()` to allow multiple serde formats (and prevent extra quotes on strings)
|
||||
- test with a variety of types
|
||||
- `<Form method="POST">` in `leptos_router`
|
||||
- import `leptos_router` and use `<Form/>` in this example
|
||||
@@ -1,4 +1,5 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
@@ -51,26 +52,36 @@ pub fn Counters(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<h1>"Server-Side Counters"</h1>
|
||||
<p>"Each of these counters stores its data in the same variable on the server."</p>
|
||||
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
|
||||
<div style="display: flex; justify-content: space-around">
|
||||
<div>
|
||||
<h2>"Simple Counter"</h2>
|
||||
<p>"This counter sets the value on the server and automatically reloads the new value."</p>
|
||||
<Counter/>
|
||||
</div>
|
||||
<div>
|
||||
<h2>"Form Counter"</h2>
|
||||
<p>"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"</p>
|
||||
<FormCounter/>
|
||||
</div>
|
||||
<div>
|
||||
<h2>"Multi-User Counter"</h2>
|
||||
<p>"This one uses server-sent events (SSE) to live-update when other users make changes."</p>
|
||||
<MultiuserCounter/>
|
||||
</div>
|
||||
</div>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"Server-Side Counters"</h1>
|
||||
<p>"Each of these counters stores its data in the same variable on the server."</p>
|
||||
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
|
||||
</header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><A href="">"Simple"</A></li>
|
||||
<li><A href="form">"Form-Based"</A></li>
|
||||
<li><A href="multi">"Multi-User"</A></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" element=|cx| view! {
|
||||
cx,
|
||||
<Counter/>
|
||||
}/>
|
||||
<Route path="form" element=|cx| view! {
|
||||
cx,
|
||||
<FormCounter/>
|
||||
}/>
|
||||
<Route path="multi" element=|cx| view! {
|
||||
cx,
|
||||
<MultiuserCounter/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -81,9 +92,9 @@ pub fn Counters(cx: Scope) -> Element {
|
||||
// This is the typical pattern for a CRUD app
|
||||
#[component]
|
||||
pub fn Counter(cx: Scope) -> Element {
|
||||
let dec = create_action(cx, || adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(cx, || adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(cx, clear_server_count);
|
||||
let dec = create_action(cx, |_| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(cx, |_| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
move || (dec.version.get(), inc.version.get(), clear.version.get()),
|
||||
@@ -104,10 +115,14 @@ pub fn Counter(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch()>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch()>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<button on:click=move |_| inc.dispatch()>"+1"</button>
|
||||
<h2>"Simple Counter"</h2>
|
||||
<p>"This counter sets the value on the server and automatically reloads the new value."</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -117,9 +132,16 @@ pub fn Counter(cx: Scope) -> Element {
|
||||
// but uses HTML forms to submit the actions
|
||||
#[component]
|
||||
pub fn FormCounter(cx: Scope) -> Element {
|
||||
let adjust = create_server_action::<AdjustServerCount>(cx);
|
||||
let clear = create_server_action::<ClearServerCount>(cx);
|
||||
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
move || (),
|
||||
{
|
||||
let adjust = adjust.version;
|
||||
let clear = clear.version;
|
||||
move || (adjust.get(), clear.get())
|
||||
},
|
||||
|_| {
|
||||
log::debug!("FormCounter running fetcher");
|
||||
|
||||
@@ -136,27 +158,33 @@ pub fn FormCounter(cx: Scope) -> Element {
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
let adjust2 = adjust.clone();
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
// calling a server function is the same as POSTing to its API URL
|
||||
// so we can just do that with a form and button
|
||||
<form method="POST" action=ClearServerCount::url()>
|
||||
<input type="submit" value="Clear"/>
|
||||
</form>
|
||||
// We can submit named arguments to the server functions
|
||||
// by including them as input values with the same name
|
||||
<form method="POST" action=AdjustServerCount::url()>
|
||||
<input type="hidden" name="delta" value="-1"/>
|
||||
<input type="hidden" name="msg" value="\"form value down\""/>
|
||||
<input type="submit" value="-1"/>
|
||||
</form>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<form method="POST" action=AdjustServerCount::url()>
|
||||
<input type="hidden" name="delta" value="1"/>
|
||||
<input type="hidden" name="msg" value="\"form value up\""/>
|
||||
<input type="submit" value="+1"/>
|
||||
</form>
|
||||
<h2>"Form Counter"</h2>
|
||||
<p>"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"</p>
|
||||
<div>
|
||||
// calling a server function is the same as POSTing to its API URL
|
||||
// so we can just do that with a form and button
|
||||
<ActionForm action=clear>
|
||||
<input type="submit" value="Clear"/>
|
||||
</ActionForm>
|
||||
// We can submit named arguments to the server functions
|
||||
// by including them as input values with the same name
|
||||
<ActionForm action=adjust>
|
||||
<input type="hidden" name="delta" value="-1"/>
|
||||
<input type="hidden" name="msg" value="\"form value down\""/>
|
||||
<input type="submit" value="-1"/>
|
||||
</ActionForm>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<ActionForm action=adjust2>
|
||||
<input type="hidden" name="delta" value="1"/>
|
||||
<input type="hidden" name="msg" value="\"form value up\""/>
|
||||
<input type="submit" value="+1"/>
|
||||
</ActionForm>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -167,9 +195,9 @@ pub fn FormCounter(cx: Scope) -> Element {
|
||||
// This is the primitive pattern for live chat, collaborative editing, etc.
|
||||
#[component]
|
||||
pub fn MultiuserCounter(cx: Scope) -> Element {
|
||||
let dec = create_action(cx, || adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc = create_action(cx, || adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(cx, clear_server_count);
|
||||
let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let multiplayer_value = {
|
||||
@@ -200,10 +228,14 @@ pub fn MultiuserCounter(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch()>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch()>"-1"</button>
|
||||
<span>"Multiplayer Value: " {move || multiplayer_value().unwrap_or_default().to_string()}</span>
|
||||
<button on:click=move |_| inc.dispatch()>"+1"</button>
|
||||
<h2>"Multi-User Counter"</h2>
|
||||
<p>"This one uses server-sent events (SSE) to live-update when other users make changes."</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Multiplayer Value: " {move || multiplayer_value().unwrap_or_default().to_string()}</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,6 @@ actix-files = "0.6"
|
||||
actix-web = { version = "4" }
|
||||
futures = "0.3"
|
||||
leptos = { path = "../../../leptos", default-features = false, features = ["ssr", "serde"] }
|
||||
leptos_router = { path = "../../../router", default-features = false, features = ["ssr"] }
|
||||
counter-isomorphic = { path = "../counter", default-features = false, features = ["ssr"] }
|
||||
lazy_static = "1"
|
||||
|
||||
@@ -2,9 +2,14 @@ use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use counter_isomorphic::*;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[get("{tail:.*}")]
|
||||
async fn render(req: HttpRequest) -> impl Responder {
|
||||
let path = req.path();
|
||||
let path = "http://leptos".to_string() + path;
|
||||
println!("path = {path}");
|
||||
|
||||
#[get("/")]
|
||||
async fn render() -> impl Responder {
|
||||
HttpResponse::Ok().content_type("text/html").body(format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -19,7 +24,10 @@ async fn render() -> impl Responder {
|
||||
<script type="module">import init, {{ main }} from './pkg/counter_client.js'; init().then(main);</script>
|
||||
</html>"#,
|
||||
run_scope({
|
||||
|cx| {
|
||||
move |cx| {
|
||||
let integration = ServerIntegration { path: path.clone() };
|
||||
provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
|
||||
view! { cx, <Counters/>}
|
||||
}
|
||||
})
|
||||
@@ -87,10 +95,10 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.service(render)
|
||||
.service(Files::new("/pkg", "../client/pkg"))
|
||||
.service(counter_events)
|
||||
.service(handle_server_fns)
|
||||
.service(render)
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(("127.0.0.1", 8081))?
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use leptos::{wasm_bindgen::JsValue, *};
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
use counters::{Counters, CountersProps};
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| view! { cx, <Counters/> });
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let add_counter = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
// add 3 counters
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
|
||||
// check HTML
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>0</span> from <span>3</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li></ul>");
|
||||
|
||||
let counters = div
|
||||
.query_selector("ul")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.children();
|
||||
|
||||
// click first counter once, second counter twice, etc.
|
||||
// `NodeList` isn't a `Vec` so we iterate over it in this slightly awkward way
|
||||
for idx in 0..counters.length() {
|
||||
let counter = counters.item(idx).unwrap();
|
||||
let inc_button = counter
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>();
|
||||
for _ in 0..=idx {
|
||||
inc_button.click();
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>6</span> from <span>3</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>1</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>2</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>3</span><button>+1</button><button>x</button></li></ul>");
|
||||
|
||||
// remove the first counter
|
||||
counters
|
||||
.item(0)
|
||||
.unwrap()
|
||||
.last_child()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.click();
|
||||
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>5</span> from <span>2</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>2</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>3</span><button>+1</button><button>x</button></li></ul>");
|
||||
|
||||
// decrement all by 1
|
||||
for idx in 0..counters.length() {
|
||||
let counter = counters.item(idx).unwrap();
|
||||
let dec_button = counter
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>();
|
||||
dec_button.click();
|
||||
}
|
||||
|
||||
// we can use RSX in test comparisons!
|
||||
// note that if RSX template creation is bugged, this probably won't catch it
|
||||
// (because the same bug will be reproduced in both sides of the assertion)
|
||||
// so I use HTML tests for most internal testing like this
|
||||
// but in user-land testing, RSX comparanda are cool
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Add Counter"</button>
|
||||
<button>"Add 1000 Counters"</button>
|
||||
<button>"Clear Counters"</button>
|
||||
<p>"Total: "<span>"3"</span>" from "<span>"2"</span>" counters."</p>
|
||||
<ul>
|
||||
<li>
|
||||
<button>"-1"</button>
|
||||
<input type="text"/>
|
||||
<span>"1"</span>
|
||||
<button>"+1"</button>
|
||||
<button>"x"</button>
|
||||
</li>
|
||||
<li>
|
||||
<button>"-1"</button>
|
||||
<input type="text"/>
|
||||
<span>"2"</span>
|
||||
<button>"+1"</button>
|
||||
<button>"x"</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
.outer_html()
|
||||
);
|
||||
}
|
||||
@@ -6,12 +6,14 @@ edition = "2021"
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
console_log = "0.2"
|
||||
leptos = { path = "../../../leptos", default-features = false, features = ["serde"] }
|
||||
leptos = { path = "../../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../../meta", default-features = false }
|
||||
leptos_router = { path = "../../../router", default-features = false }
|
||||
log = "0.4"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
@@ -21,4 +23,4 @@ console_error_panic_hook = "0.1.7"
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:reqwest"]
|
||||
ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr"]
|
||||
|
||||
@@ -7,28 +7,6 @@ use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct ActixIntegration {
|
||||
path: ReadSignal<String>,
|
||||
}
|
||||
|
||||
impl History for ActixIntegration {
|
||||
fn location(&self, cx: leptos::Scope) -> ReadSignal<LocationChange> {
|
||||
create_signal(
|
||||
cx,
|
||||
LocationChange {
|
||||
value: self.path.get(),
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state: State(None),
|
||||
},
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
fn navigate(&self, _loc: &LocationChange) {}
|
||||
}
|
||||
|
||||
#[get("/static/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
NamedFile::open_async("../hackernews-app/style.css").await
|
||||
@@ -47,10 +25,8 @@ async fn render_app(req: HttpRequest) -> impl Responder {
|
||||
};
|
||||
|
||||
let app = move |cx| {
|
||||
let integration = ActixIntegration {
|
||||
path: create_signal(cx, path.clone()).0,
|
||||
};
|
||||
provide_context(cx, RouterIntegrationContext(std::rc::Rc::new(integration)));
|
||||
let integration = ServerIntegration { path: path.clone() };
|
||||
provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
|
||||
view! { cx, <App/> }
|
||||
};
|
||||
|
||||
@@ -289,7 +289,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||
</li>
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
create_effect(cx, move |_| {
|
||||
if editing() {
|
||||
_ = input.unchecked_ref::<HtmlInputElement>().focus();
|
||||
|
||||
@@ -17,10 +17,37 @@ leptos_server = { path = "../leptos_server", default-features = false, version =
|
||||
|
||||
[features]
|
||||
default = ["csr", "serde"]
|
||||
csr = ["leptos_core/csr", "leptos_dom/csr", "leptos_macro/csr", "leptos_reactive/csr", "leptos_server/csr"]
|
||||
hydrate = ["leptos_core/hydrate", "leptos_dom/hydrate", "leptos_macro/hydrate", "leptos_reactive/hydrate", "leptos_server/hydrate"]
|
||||
ssr = ["leptos_core/ssr", "leptos_dom/ssr", "leptos_macro/ssr", "leptos_reactive/ssr", "leptos_server/ssr"]
|
||||
stable = ["leptos_core/stable", "leptos_dom/stable", "leptos_macro/stable", "leptos_reactive/stable", "leptos_server/stable"]
|
||||
csr = [
|
||||
"leptos_core/csr",
|
||||
"leptos_dom/csr",
|
||||
"leptos_macro/csr",
|
||||
"leptos_reactive/csr",
|
||||
"leptos_server/csr",
|
||||
]
|
||||
hydrate = [
|
||||
"leptos_core/hydrate",
|
||||
"leptos_dom/hydrate",
|
||||
"leptos_macro/hydrate",
|
||||
"leptos_reactive/hydrate",
|
||||
"leptos_server/hydrate",
|
||||
]
|
||||
ssr = [
|
||||
"leptos_core/ssr",
|
||||
"leptos_dom/ssr",
|
||||
"leptos_macro/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
]
|
||||
stable = [
|
||||
"leptos_core/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"]
|
||||
miniserde = ["leptos_reactive/miniserde"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
//!
|
||||
//! // create event handlers for our buttons
|
||||
//! // note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
|
||||
//! let clear = move |_| set_value(0);
|
||||
//! let clear = move |_| set_value.set(0);
|
||||
//! let decrement = move |_| set_value.update(|value| *value -= 1);
|
||||
//! let increment = move |_| set_value.update(|value| *value += 1);
|
||||
//!
|
||||
@@ -123,15 +123,3 @@ pub use leptos_server;
|
||||
pub use leptos_server::*;
|
||||
|
||||
pub use leptos_reactive::debug_warn;
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "ssr", feature = "hydrate")))]
|
||||
compile_error!("set one of the following feature flags: 'csr', 'ssr' or 'hydrate'");
|
||||
|
||||
#[cfg(all(feature = "csr", feature = "ssr"))]
|
||||
compile_error!("leptos features 'csr' and feature 'ssr' cannot be enabled at the same time");
|
||||
|
||||
#[cfg(all(feature = "csr", feature = "hydrate"))]
|
||||
compile_error!("leptos features 'csr' and feature 'hydrate' cannot be enabled at the same time");
|
||||
|
||||
#[cfg(all(feature = "hydrate", feature = "ssr"))]
|
||||
compile_error!("leptos features 'hydrate' and feature 'ssr' cannot be enabled at the same time");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[test]
|
||||
fn simple_ssr_test() {
|
||||
use leptos_dom::*;
|
||||
@@ -23,7 +23,7 @@ fn simple_ssr_test() {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[test]
|
||||
fn ssr_test_with_components() {
|
||||
use leptos_core as leptos;
|
||||
@@ -61,7 +61,7 @@ fn ssr_test_with_components() {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[test]
|
||||
fn test_classes() {
|
||||
use leptos_dom::*;
|
||||
|
||||
@@ -15,6 +15,13 @@ log = "0.4"
|
||||
|
||||
[features]
|
||||
csr = ["leptos_dom/csr", "leptos_macro/csr", "leptos_reactive/csr"]
|
||||
hydrate = ["leptos_dom/hydrate", "leptos_macro/hydrate", "leptos_reactive/hydrate"]
|
||||
hydrate = [
|
||||
"leptos_dom/hydrate",
|
||||
"leptos_macro/hydrate",
|
||||
"leptos_reactive/hydrate",
|
||||
]
|
||||
ssr = ["leptos_dom/ssr", "leptos_macro/ssr", "leptos_reactive/ssr"]
|
||||
stable = ["leptos_dom/stable", "leptos_macro/stable", "leptos_reactive/stable"]
|
||||
stable = ["leptos_dom/stable", "leptos_macro/stable", "leptos_reactive/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||
mod for_component;
|
||||
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||
mod map;
|
||||
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||
mod suspense;
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||
pub use for_component::*;
|
||||
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||
pub use map::*;
|
||||
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||
pub use suspense::*;
|
||||
|
||||
pub trait Prop {
|
||||
|
||||
@@ -160,25 +160,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_map_keyed() {
|
||||
create_scope(|cx| {
|
||||
let (rows, set_rows) =
|
||||
create_signal::<Vec<(usize, ReadSignal<i32>, WriteSignal<i32>)>>(cx, vec![]);
|
||||
// we can really only run this in SSR mode, so just ignore if we're in CSR or hydrate
|
||||
if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
create_scope(|cx| {
|
||||
let (rows, set_rows) =
|
||||
create_signal::<Vec<(usize, ReadSignal<i32>, WriteSignal<i32>)>>(cx, vec![]);
|
||||
|
||||
let keyed = map_keyed(
|
||||
cx,
|
||||
rows,
|
||||
|cx, row| {
|
||||
let read = row.1;
|
||||
create_effect(cx, move |_| println!("row value = {}", read.get()));
|
||||
},
|
||||
|row| row.0,
|
||||
);
|
||||
let keyed = map_keyed(
|
||||
cx,
|
||||
rows,
|
||||
|cx, row| {
|
||||
let read = row.1;
|
||||
create_effect(cx, move |_| println!("row value = {}", read.get()));
|
||||
},
|
||||
|row| row.0,
|
||||
);
|
||||
|
||||
create_effect(cx, move |_| println!("keyed = {:#?}", keyed.get()));
|
||||
create_effect(cx, move |_| println!("keyed = {:#?}", keyed.get()));
|
||||
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
set_rows.update(|n| n.push((0, r, w)));
|
||||
})
|
||||
.dispose();
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
set_rows.update(|n| n.push((0, r, w)));
|
||||
})
|
||||
.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ where
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||
pub fn Suspense<F, E, G>(cx: Scope, props: SuspenseProps<F, E, G>) -> impl Fn() -> Child
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
@@ -32,17 +31,7 @@ where
|
||||
render_suspense(cx, context, props.fallback.clone(), child)
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate", feature = "ssr")))]
|
||||
pub fn Suspense<F, E, G>(cx: Scope, props: SuspenseProps<F, E, G>) -> impl Fn() -> Child
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
E: IntoChild,
|
||||
G: Fn() -> E + 'static,
|
||||
{
|
||||
compile_error!("<Suspense/> can only be used when one of the following features is set on the `leptos` package: 'csr', 'ssr', or 'hydrate'");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
fn render_suspense<'a, F, E, G>(
|
||||
cx: Scope,
|
||||
context: SuspenseContext,
|
||||
@@ -69,7 +58,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
fn render_suspense<'a, F, E, G>(
|
||||
cx: Scope,
|
||||
context: SuspenseContext,
|
||||
|
||||
@@ -8,11 +8,12 @@ repository = "https://github.com/gbj/leptos"
|
||||
description = "DOM operations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
futures = { version = "0.3", optional = true }
|
||||
html-escape = { version = "0.2", optional = true }
|
||||
cfg-if = "1"
|
||||
futures = "0.3"
|
||||
html-escape = "0.2"
|
||||
js-sys = "0.3"
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.11" }
|
||||
serde_json = { version = "1", optional = true }
|
||||
serde_json = "1"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4.31"
|
||||
log = "0.4"
|
||||
@@ -51,11 +52,11 @@ features = [
|
||||
"Storage",
|
||||
"Text",
|
||||
"TreeWalker",
|
||||
"Window"
|
||||
"Window",
|
||||
]
|
||||
|
||||
[features]
|
||||
csr = ["leptos_reactive/csr"]
|
||||
hydrate = ["leptos_reactive/hydrate"]
|
||||
ssr = ["leptos_reactive/ssr", "dep:futures", "dep:html-escape", "dep:serde_json"]
|
||||
stable = ["leptos_reactive/stable"]
|
||||
ssr = ["leptos_reactive/ssr"]
|
||||
stable = ["leptos_reactive/stable"]
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use leptos_reactive::Scope;
|
||||
#[cfg(feature = "stable")]
|
||||
use leptos_reactive::{Memo, ReadSignal, RwSignal};
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
use crate::Node;
|
||||
|
||||
@@ -19,7 +16,7 @@ pub enum Child {
|
||||
}
|
||||
|
||||
impl Child {
|
||||
#[cfg(feature = "ssr")]
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
pub fn as_child_string(&self) -> String {
|
||||
match self {
|
||||
Child::Null => String::new(),
|
||||
@@ -83,24 +80,14 @@ impl IntoChild for String {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
impl IntoChild for web_sys::Node {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Node(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
impl IntoChild for web_sys::Text {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Node(self.unchecked_into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
impl IntoChild for web_sys::Element {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Node(self.unchecked_into())
|
||||
impl<T, U> IntoChild for T
|
||||
where
|
||||
T: FnMut() -> U + 'static,
|
||||
U: IntoChild,
|
||||
{
|
||||
fn into_child(mut self, cx: Scope) -> Child {
|
||||
let modified_fn = Rc::new(RefCell::new(move || (self)().into_child(cx)));
|
||||
Child::Fn(modified_fn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,49 +109,6 @@ impl IntoChild for Vec<Node> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
impl IntoChild for Vec<web_sys::Element> {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Nodes(
|
||||
self.into_iter()
|
||||
.map(|el| el.unchecked_into::<web_sys::Node>())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "stable", not(feature = "ssr")))]
|
||||
impl IntoChild for Memo<Vec<web_sys::Element>> {
|
||||
fn into_child(self, cx: Scope) -> Child {
|
||||
(move || self.get()).into_child(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "stable", not(feature = "ssr")))]
|
||||
impl IntoChild for ReadSignal<Vec<web_sys::Element>> {
|
||||
fn into_child(self, cx: Scope) -> Child {
|
||||
(move || self.get()).into_child(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "stable", not(feature = "ssr")))]
|
||||
impl IntoChild for RwSignal<Vec<web_sys::Element>> {
|
||||
fn into_child(self, cx: Scope) -> Child {
|
||||
(move || self.get()).into_child(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> IntoChild for T
|
||||
where
|
||||
T: FnMut() -> U + 'static,
|
||||
U: IntoChild,
|
||||
{
|
||||
fn into_child(mut self, cx: Scope) -> Child {
|
||||
let modified_fn = Rc::new(RefCell::new(move || (self)().into_child(cx)));
|
||||
Child::Fn(modified_fn)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! child_type {
|
||||
($child_type:ty) => {
|
||||
impl IntoChild for $child_type {
|
||||
@@ -193,3 +137,62 @@ child_type!(f32);
|
||||
child_type!(f64);
|
||||
child_type!(char);
|
||||
child_type!(bool);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "hydrate", feature = "csr"))] {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
impl IntoChild for web_sys::Node {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Node(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoChild for web_sys::Text {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Node(self.unchecked_into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoChild for web_sys::Element {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Node(self.unchecked_into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoChild for Vec<web_sys::Element> {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Nodes(
|
||||
self.into_iter()
|
||||
.map(|el| el.unchecked_into::<web_sys::Node>())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `stable` feature
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "stable")] {
|
||||
use leptos_reactive::{Memo, ReadSignal, RwSignal};
|
||||
|
||||
impl IntoChild for Memo<Vec<crate::Element>> {
|
||||
fn into_child(self, cx: Scope) -> Child {
|
||||
(move || self.get()).into_child(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoChild for ReadSignal<Vec<crate::Element>> {
|
||||
fn into_child(self, cx: Scope) -> Child {
|
||||
(move || self.get()).into_child(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoChild for RwSignal<Vec<crate::Element>> {
|
||||
fn into_child(self, cx: Scope) -> Child {
|
||||
(move || self.get()).into_child(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
pub mod attribute;
|
||||
pub mod child;
|
||||
pub mod class;
|
||||
pub mod event_delegation;
|
||||
pub mod logging;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub mod mount;
|
||||
pub mod operations;
|
||||
pub mod property;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub mod reconcile;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub mod render;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod render_to_string;
|
||||
|
||||
cfg_if! {
|
||||
// can only include this if we're *only* enabling SSR, as it's the lowest-priority feature
|
||||
// if either `csr` or `hydrate` is enabled, `Element` is a `web_sys::Element` and can't be rendered
|
||||
if #[cfg(not(any(feature = "hydrate", feature = "csr")))] {
|
||||
pub type Element = String;
|
||||
pub type Node = String;
|
||||
|
||||
pub mod render_to_string;
|
||||
pub use render_to_string::*;
|
||||
|
||||
pub struct Marker { }
|
||||
} else {
|
||||
pub type Element = web_sys::Element;
|
||||
pub type Node = web_sys::Node;
|
||||
|
||||
pub mod reconcile;
|
||||
pub mod render;
|
||||
|
||||
pub use reconcile::*;
|
||||
pub use render::*;
|
||||
}
|
||||
}
|
||||
|
||||
pub use attribute::*;
|
||||
pub use child::*;
|
||||
pub use class::*;
|
||||
pub use logging::*;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub use mount::*;
|
||||
pub use operations::*;
|
||||
pub use property::*;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub use render::*;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use render_to_string::*;
|
||||
|
||||
pub use js_sys;
|
||||
pub use wasm_bindgen;
|
||||
pub use web_sys;
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub type Element = web_sys::Element;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub type Element = String;
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub type Node = web_sys::Node;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub type Node = String;
|
||||
|
||||
use leptos_reactive::Scope;
|
||||
pub use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
pub fn create_component<F, T>(cx: Scope, f: F) -> T
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
cx.untrack(f)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "csr"))]
|
||||
pub fn create_component<F, T>(cx: Scope, f: F) -> T
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
cx.with_next_context(f)
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "csr")] {
|
||||
cx.untrack(f)
|
||||
} else {
|
||||
cx.with_next_context(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{document, Element};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::Scope;
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
@@ -8,14 +9,24 @@ pub trait Mountable {
|
||||
|
||||
impl Mountable for Element {
|
||||
fn mount(&self, parent: &web_sys::Element) {
|
||||
parent.append_child(self).unwrap_throw();
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
parent.append_child(self).unwrap_throw();
|
||||
} else {
|
||||
let _ = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable for Vec<Element> {
|
||||
fn mount(&self, parent: &web_sys::Element) {
|
||||
for element in self {
|
||||
parent.append_child(element).unwrap_throw();
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
for element in self {
|
||||
parent.append_child(element).unwrap_throw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,6 @@
|
||||
use cfg_if::cfg_if;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use leptos_reactive::*;
|
||||
|
||||
use crate::Element;
|
||||
use futures::{stream::FuturesUnordered, Stream, StreamExt};
|
||||
|
||||
pub fn render_to_stream(view: impl Fn(Scope) -> Element + 'static) -> impl Stream<Item = String> {
|
||||
let ((shell, pending_resources, pending_fragments, serializers), _, disposer) =
|
||||
run_scope_undisposed({
|
||||
move |cx| {
|
||||
// the actual app body/template code
|
||||
// this does NOT contain any of the data being loaded asynchronously in resources
|
||||
let shell = view(cx);
|
||||
|
||||
let resources = cx.all_resources();
|
||||
let pending_resources = serde_json::to_string(&resources).unwrap();
|
||||
|
||||
(
|
||||
shell,
|
||||
pending_resources,
|
||||
cx.pending_fragments(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let fragments = FuturesUnordered::new();
|
||||
for (fragment_id, fut) in pending_fragments {
|
||||
fragments.push(async move { (fragment_id, fut.await) })
|
||||
}
|
||||
|
||||
// HTML for the view function and script to store resources
|
||||
futures::stream::once(async move {
|
||||
format!(
|
||||
r#"
|
||||
{shell}
|
||||
<script>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
})
|
||||
// stream data for each Resource as it resolves
|
||||
.chain(serializers.map(|(id, json)| {
|
||||
let id = serde_json::to_string(&id).unwrap();
|
||||
format!(
|
||||
r#"<script>
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
console.log("(create_resource) calling resolver");
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
|
||||
}} else {{
|
||||
console.log("(create_resource) saving data for resource creation");
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
|
||||
}}
|
||||
</script>"#,
|
||||
)
|
||||
}))
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
.chain(fragments.map(|(fragment_id, html)| {
|
||||
format!(
|
||||
r#"
|
||||
<template id="{fragment_id}">{html}</template>
|
||||
<script>
|
||||
var frag = document.querySelector(`[data-fragment-id="{fragment_id}"]`);
|
||||
var tpl = document.getElementById("{fragment_id}");
|
||||
console.log("replace", frag, "with", tpl.content.cloneNode(true));
|
||||
frag.replaceWith(tpl.content.cloneNode(true));
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
}))
|
||||
// dispose of Scope
|
||||
.chain(futures::stream::once(async {
|
||||
disposer.dispose();
|
||||
Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn escape_text(text: &str) -> Cow<'_, str> {
|
||||
html_escape::encode_text(text)
|
||||
}
|
||||
@@ -86,3 +8,86 @@ pub fn escape_text(text: &str) -> Cow<'_, str> {
|
||||
pub fn escape_attr(text: &str) -> Cow<'_, str> {
|
||||
html_escape::encode_double_quoted_attribute(text)
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos_reactive::*;
|
||||
|
||||
use crate::Element;
|
||||
use futures::{stream::FuturesUnordered, Stream, StreamExt};
|
||||
|
||||
pub fn render_to_stream(view: impl Fn(Scope) -> Element + 'static) -> impl Stream<Item = String> {
|
||||
let ((shell, pending_resources, pending_fragments, serializers), _, disposer) =
|
||||
run_scope_undisposed({
|
||||
move |cx| {
|
||||
// the actual app body/template code
|
||||
// this does NOT contain any of the data being loaded asynchronously in resources
|
||||
let shell = view(cx);
|
||||
|
||||
let resources = cx.all_resources();
|
||||
let pending_resources = serde_json::to_string(&resources).unwrap();
|
||||
|
||||
(
|
||||
shell,
|
||||
pending_resources,
|
||||
cx.pending_fragments(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let fragments = FuturesUnordered::new();
|
||||
for (fragment_id, fut) in pending_fragments {
|
||||
fragments.push(async move { (fragment_id, fut.await) })
|
||||
}
|
||||
|
||||
// HTML for the view function and script to store resources
|
||||
futures::stream::once(async move {
|
||||
format!(
|
||||
r#"
|
||||
{shell}
|
||||
<script>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
})
|
||||
// stream data for each Resource as it resolves
|
||||
.chain(serializers.map(|(id, json)| {
|
||||
let id = serde_json::to_string(&id).unwrap();
|
||||
format!(
|
||||
r#"<script>
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
console.log("(create_resource) calling resolver");
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
|
||||
}} else {{
|
||||
console.log("(create_resource) saving data for resource creation");
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
|
||||
}}
|
||||
</script>"#,
|
||||
)
|
||||
}))
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
.chain(fragments.map(|(fragment_id, html)| {
|
||||
format!(
|
||||
r#"
|
||||
<template id="{fragment_id}">{html}</template>
|
||||
<script>
|
||||
var frag = document.querySelector(`[data-fragment-id="{fragment_id}"]`);
|
||||
var tpl = document.getElementById("{fragment_id}");
|
||||
console.log("replace", frag, "with", tpl.content.cloneNode(true));
|
||||
frag.replaceWith(tpl.content.cloneNode(true));
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
}))
|
||||
// dispose of Scope
|
||||
.chain(futures::stream::once(async {
|
||||
disposer.dispose();
|
||||
Default::default()
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,7 @@ default = ["ssr"]
|
||||
csr = ["leptos_dom/csr", "leptos_reactive/csr"]
|
||||
hydrate = ["leptos_dom/hydrate", "leptos_reactive/hydrate"]
|
||||
ssr = ["leptos_dom/ssr", "leptos_reactive/ssr"]
|
||||
stable = ["leptos_dom/stable", "leptos_reactive/stable"]
|
||||
stable = ["leptos_dom/stable", "leptos_reactive/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
|
||||
@@ -13,14 +13,17 @@ pub(crate) enum Mode {
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
if cfg!(feature = "ssr") {
|
||||
Mode::Ssr
|
||||
} else if cfg!(feature = "hydrate") {
|
||||
// what's the deal with this order of priority?
|
||||
// basically, it's fine for the server to compile wasm-bindgen, but it will panic if it runs it
|
||||
// for the sake of testing, we need to fall back to `ssr` if no flags are enabled
|
||||
// if you have `hydrate` enabled, you definitely want that rather than `csr`
|
||||
// if you have both `csr` and `ssr` we assume you want the browser
|
||||
if cfg!(feature = "hydrate") {
|
||||
Mode::Hydrate
|
||||
} else if cfg!(feature = "csr") {
|
||||
Mode::Client
|
||||
} else {
|
||||
panic!("one of the features leptos/ssr, leptos/hydrate, or leptos/csr needs to be set")
|
||||
Mode::Ssr
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,35 +39,46 @@ mod server;
|
||||
/// same rules as HTML, with the following differences:
|
||||
/// 1. Text content should be provided as a Rust string, i.e., double-quoted:
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
|
||||
/// # run_scope(|cx| {
|
||||
/// view! { cx, <p>"Here’s some text"</p> }
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// view! { cx, <p>"Here’s some text"</p> };
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 2. Self-closing tags need an explicit `/` as in XML/XHTML
|
||||
/// ```rust,compile_fail
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
|
||||
/// # run_scope(|cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// // ❌ not like this
|
||||
/// view! { cx, <input type="text" name="name"> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
|
||||
/// # run_scope(|cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// // ✅ add that slash
|
||||
/// view! { cx, <input type="text" name="name" /> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 3. Components (functions annotated with `#[component]`) can be inserted as camel-cased tags
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::*; use typed_builder::TypedBuilder;
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::*; use typed_builder::TypedBuilder; use leptos_dom::wasm_bindgen::JsCast; use leptos_dom as leptos; use leptos_dom::Marker;
|
||||
/// # #[derive(TypedBuilder)] struct CounterProps { initial_value: i32 }
|
||||
/// # fn Counter(cx: Scope, props: CounterProps) -> Element { view! { cx, <p></p>} }
|
||||
/// # run_scope(|cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// view! { cx, <div><Counter initial_value=3 /></div> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
@@ -73,8 +87,9 @@ mod server;
|
||||
/// *(“Signal” here means `Fn() -> T` where `T` is the appropriate type for that node: a `String` in case
|
||||
/// of text nodes, a `bool` for `class:` attributes, etc.)*
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast; use leptos_dom as leptos; use leptos_dom::Marker;
|
||||
/// # run_scope(|cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (count, set_count) = create_signal(cx, 0);
|
||||
///
|
||||
/// view! {
|
||||
@@ -85,13 +100,16 @@ mod server;
|
||||
/// "Double Count: " {move || count() % 2} // or derive a signal inline
|
||||
/// </div>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 5. Event handlers can be added with `on:` attributes
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
|
||||
/// # run_scope(|cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <button on:click=|ev: web_sys::Event| {
|
||||
@@ -100,14 +118,17 @@ mod server;
|
||||
/// "Click me"
|
||||
/// </button>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 6. DOM properties can be set with `prop:` attributes, which take any primitive type or `JsValue` (or a signal
|
||||
/// that returns a primitive or JsValue).
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
|
||||
/// # run_scope(|cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
|
||||
///
|
||||
/// view! {
|
||||
@@ -120,22 +141,28 @@ mod server;
|
||||
/// on:click=move |ev| set_name(event_target_value(&ev)) // `event_target_value` is a useful little Leptos helper
|
||||
/// />
|
||||
/// }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 7. Classes can be toggled with `class:` attributes, which take a `bool` (or a signal that returns a `bool`).
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
|
||||
/// # run_scope(|cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (count, set_count) = create_signal(cx, 2);
|
||||
/// view! { cx, <div class:hidden={move || count() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// Here’s a simple example that shows off several of these features, put together
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::*;
|
||||
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::*; use leptos_dom as leptos; use leptos_dom::Marker; use leptos_dom::wasm_bindgen::JsCast;
|
||||
///
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// pub fn SimpleCounter(cx: Scope) -> Element {
|
||||
/// // create a reactive signal with the initial value
|
||||
/// let (value, set_value) = create_signal(cx, 0);
|
||||
@@ -157,6 +184,8 @@ mod server;
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// ```
|
||||
#[proc_macro]
|
||||
pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Credit to Dioxus: https://github.com/DioxusLabs/dioxus/blob/master/packages/core-macro/src/Server.rs
|
||||
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use proc_macro2::{TokenStream as TokenStream2};
|
||||
use quote::{quote};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
*, token::Type,
|
||||
*
|
||||
};
|
||||
|
||||
pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Result<TokenStream2> {
|
||||
@@ -21,7 +21,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
quote! { pub #f }
|
||||
quote! { pub #typed_arg }
|
||||
});
|
||||
|
||||
let fn_args = body.inputs.iter().map(|f| {
|
||||
@@ -29,7 +29,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
quote! { #f }
|
||||
quote! { #typed_arg }
|
||||
});
|
||||
let fn_args_2 = fn_args.clone();
|
||||
|
||||
@@ -75,6 +75,8 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
|
||||
let field_names_2 = field_names.clone();
|
||||
let field_names_3 = field_names.clone();
|
||||
let field_names_4 = field_names.clone();
|
||||
let field_names_5 = field_names.clone();
|
||||
|
||||
let output_arrow = body.output_arrow;
|
||||
let return_ty = body.return_ty;
|
||||
@@ -94,11 +96,11 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
};
|
||||
|
||||
Ok(quote::quote! {
|
||||
#[derive(Clone)]
|
||||
pub struct #struct_name {
|
||||
#(#fields),*
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ServerFn for #struct_name {
|
||||
type Output = #output_ty;
|
||||
|
||||
@@ -120,9 +122,15 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn call_fn(self) -> Result<Self::Output, ServerFnError> {
|
||||
fn call_fn(self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>> + Send>> {
|
||||
let #struct_name { #(#field_names),* } = self;
|
||||
#fn_name( #(#field_names_2),*).await
|
||||
Box::pin(async move { #fn_name( #(#field_names_2),*).await })
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn call_fn_client(self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names_3),* } = self;
|
||||
Box::pin(async move { #fn_name( #(#field_names_4),*).await })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,10 +140,9 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
#vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty {
|
||||
::leptos::call_server_fn(#struct_name::url(), #struct_name { #(#field_names_3),* }).await
|
||||
::leptos::call_server_fn(#struct_name::url(), #struct_name { #(#field_names_5),* }).await
|
||||
}
|
||||
}
|
||||
.into())
|
||||
})
|
||||
}
|
||||
|
||||
pub struct ServerFnName {
|
||||
|
||||
@@ -10,19 +10,25 @@ description = "Reactive system for the Leptos web framework."
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
slotmap = { version = "1", features = ["serde"] }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde-lite = { version = "0.3", optional = true }
|
||||
futures = { version = "0.3" }
|
||||
js-sys = { version = "0.3", optional = true }
|
||||
js-sys = "0.3"
|
||||
miniserde = { version = "0.1", optional = true }
|
||||
serde-wasm-bindgen = { version = "0.4", optional = true }
|
||||
serde_json = { version = "1" }
|
||||
base64 = { version = "0.13", optional = true }
|
||||
serde-wasm-bindgen = "0.4"
|
||||
serde_json = "1"
|
||||
base64 = "0.13"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["rt"], optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4", optional = true }
|
||||
web-sys = { version = "0.3", optional = true, features = ["Element"] }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3", features = [
|
||||
"DocumentFragment",
|
||||
"Element",
|
||||
"HtmlTemplateElement",
|
||||
"NodeList",
|
||||
"Window",
|
||||
] }
|
||||
cfg-if = "1.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -30,17 +36,13 @@ tokio-test = "0.4"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
csr = ["dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:web-sys"]
|
||||
hydrate = [
|
||||
"dep:base64",
|
||||
"dep:js-sys",
|
||||
"dep:serde-wasm-bindgen",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:wasm-bindgen-futures",
|
||||
"dep:web-sys",
|
||||
]
|
||||
ssr = ["dep:base64", "dep:tokio"]
|
||||
csr = []
|
||||
hydrate = []
|
||||
ssr = ["dep:tokio"]
|
||||
stable = []
|
||||
serde = ["dep:serde"]
|
||||
serde = []
|
||||
serde-lite = ["dep:serde-lite"]
|
||||
miniserde = ["dep:miniserde"]
|
||||
miniserde = ["dep:miniserde"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
#[cfg(any(feature = "hydrate", feature = "ssr"))]
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
use std::collections::HashSet;
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
#[cfg(any(feature = "hydrate"))]
|
||||
use crate::ResourceId;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SharedContext {
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub completed: Vec<web_sys::Element>,
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub events: Vec<()>,
|
||||
pub context: Option<HydrationContext>,
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub registry: HashMap<String, web_sys::Element>,
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub pending_resources: HashSet<ResourceId>,
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub resolved_resources: HashMap<ResourceId, String>,
|
||||
#[cfg(feature = "ssr")]
|
||||
pub pending_fragments: HashMap<String, Pin<Box<dyn Future<Output = String>>>>,
|
||||
}
|
||||
|
||||
@@ -32,7 +22,6 @@ impl std::fmt::Debug for SharedContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hydrate", not(feature = "ssr")))]
|
||||
impl PartialEq for SharedContext {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.completed == other.completed
|
||||
@@ -44,24 +33,10 @@ impl PartialEq for SharedContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl PartialEq for SharedContext {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.context == other.context
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "ssr", feature = "hydrate")))]
|
||||
impl PartialEq for SharedContext {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.context == other.context
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for SharedContext {}
|
||||
|
||||
impl SharedContext {
|
||||
#[cfg(all(feature = "hydrate", not(feature = "ssr")))]
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn new_with_registry(registry: HashMap<String, web_sys::Element>) -> Self {
|
||||
let pending_resources = js_sys::Reflect::get(
|
||||
&web_sys::window().unwrap(),
|
||||
@@ -91,6 +66,7 @@ impl SharedContext {
|
||||
registry,
|
||||
pending_resources,
|
||||
resolved_resources,
|
||||
pending_fragments: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +84,6 @@ impl SharedContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn current_fragment_key(&self) -> String {
|
||||
if let Some(context) = &self.context {
|
||||
format!("{}{}f", context.id, context.count)
|
||||
|
||||
@@ -100,6 +100,7 @@ where
|
||||
.try_with(|n| f(n.as_ref().expect("Memo is missing its initial value")))
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub(crate) fn subscribe(&self) {
|
||||
self.0.subscribe()
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ use crate::{
|
||||
/// let (how_many_cats, set_how_many_cats) = create_signal(cx, 1);
|
||||
///
|
||||
/// // create a resource that will refetch whenever `how_many_cats` changes
|
||||
/// # // `csr`, `hydrate`, and `ssr` all have issues here
|
||||
/// # // because we're not running in a browser or in Tokio. Let's just ignore it.
|
||||
/// # if false {
|
||||
/// let cats = create_resource(cx, how_many_cats, fetch_cat_picture_urls);
|
||||
///
|
||||
/// // when we read the signal, it contains either
|
||||
@@ -52,6 +55,7 @@ use crate::{
|
||||
/// // when the signal's value changes, the `Resource` will generate and run a new `Future`
|
||||
/// set_how_many_cats(2);
|
||||
/// assert_eq!(cats(), Some(vec!["2".to_string()]));
|
||||
/// # }
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
pub fn create_resource<S, T, Fu>(
|
||||
@@ -160,8 +164,10 @@ where
|
||||
/// ComplicatedUnserializableStruct { }
|
||||
/// }
|
||||
///
|
||||
/// // create the resource that will
|
||||
/// // create the resource; it will run but not be serialized
|
||||
/// # if cfg!(not(any(feature = "csr", feature = "hydrate"))) {
|
||||
/// let result = create_local_resource(cx, move || (), |_| setup_complicated_struct());
|
||||
/// # }
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
pub fn create_local_resource<S, T, Fu>(
|
||||
@@ -555,7 +561,6 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn resource_to_serialization_resolver(
|
||||
&self,
|
||||
id: ResourceId,
|
||||
@@ -579,7 +584,6 @@ pub(crate) enum AnyResource {
|
||||
pub(crate) trait SerializableResource {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn to_serialization_resolver(
|
||||
&self,
|
||||
id: ResourceId,
|
||||
@@ -595,7 +599,6 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn to_serialization_resolver(
|
||||
&self,
|
||||
id: ResourceId,
|
||||
|
||||
@@ -3,16 +3,21 @@ use crate::{
|
||||
EffectId, Memo, ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
|
||||
ScopeProperty, SignalId, WriteSignal,
|
||||
};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell},
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Runtime {
|
||||
pub shared_context: RefCell<Option<SharedContext>>,
|
||||
@@ -177,7 +182,7 @@ impl Runtime {
|
||||
.insert(AnyResource::Serializable(state))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hydrate", not(feature = "ssr")))]
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn start_hydration(&self, element: &web_sys::Element) {
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
@@ -245,13 +250,10 @@ impl Runtime {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ssr"))]
|
||||
pub(crate) fn serialization_resolvers(
|
||||
&self,
|
||||
) -> futures::stream::futures_unordered::FuturesUnordered<
|
||||
std::pin::Pin<Box<dyn futures::Future<Output = (ResourceId, String)>>>,
|
||||
> {
|
||||
let f = futures::stream::futures_unordered::FuturesUnordered::new();
|
||||
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
|
||||
let f = FuturesUnordered::new();
|
||||
for (id, resource) in self.resources.borrow().iter() {
|
||||
if let AnyResource::Serializable(resource) = resource {
|
||||
f.push(resource.to_serialization_resolver(id));
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::SuspenseContext;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
use crate::{hydration::SharedContext, EffectId, ResourceId, Runtime, SignalId};
|
||||
use crate::{PinnedFuture, SuspenseContext};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::{collections::HashMap, future::Future, pin::Pin};
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
#[must_use = "Scope will leak memory if the disposer function is never called"]
|
||||
/// Creates a child reactive scope and runs the function within it. This is useful for applications
|
||||
@@ -184,58 +186,58 @@ impl ScopeDisposer {
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn is_hydrating(&self) -> bool {
|
||||
self.runtime.shared_context.borrow().is_some()
|
||||
}
|
||||
// hydration-specific code
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
pub fn is_hydrating(&self) -> bool {
|
||||
self.runtime.shared_context.borrow().is_some()
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hydrate", not(feature = "ssr")))]
|
||||
pub fn start_hydration(&self, element: &web_sys::Element) {
|
||||
self.runtime.start_hydration(element);
|
||||
}
|
||||
pub fn start_hydration(&self, element: &web_sys::Element) {
|
||||
self.runtime.start_hydration(element);
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn end_hydration(&self) {
|
||||
self.runtime.end_hydration();
|
||||
}
|
||||
pub fn end_hydration(&self) {
|
||||
self.runtime.end_hydration();
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn get_next_element(&self, template: &web_sys::Element) -> web_sys::Element {
|
||||
//log::debug!("get_next_element");
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
pub fn get_next_element(&self, template: &web_sys::Element) -> web_sys::Element {
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
let cloned_template = |t: &web_sys::Element| {
|
||||
let t = t
|
||||
.unchecked_ref::<web_sys::HtmlTemplateElement>()
|
||||
.content()
|
||||
.clone_node_with_deep(true)
|
||||
.expect_throw("(get_next_element) could not clone template")
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.first_element_child()
|
||||
.expect_throw("(get_next_element) could not get first child of template");
|
||||
t
|
||||
};
|
||||
let cloned_template = |t: &web_sys::Element| {
|
||||
let t = t
|
||||
.unchecked_ref::<web_sys::HtmlTemplateElement>()
|
||||
.content()
|
||||
.clone_node_with_deep(true)
|
||||
.expect_throw("(get_next_element) could not clone template")
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.first_element_child()
|
||||
.expect_throw("(get_next_element) could not get first child of template");
|
||||
t
|
||||
};
|
||||
|
||||
if let Some(ref mut shared_context) = &mut *self.runtime.shared_context.borrow_mut() {
|
||||
if shared_context.context.is_some() {
|
||||
let key = shared_context.next_hydration_key();
|
||||
let node = shared_context.registry.remove(&key.to_string());
|
||||
if let Some(ref mut shared_context) = &mut *self.runtime.shared_context.borrow_mut() {
|
||||
if shared_context.context.is_some() {
|
||||
let key = shared_context.next_hydration_key();
|
||||
let node = shared_context.registry.remove(&key);
|
||||
|
||||
//log::debug!("(hy) searching for {key}");
|
||||
//log::debug!("(hy) searching for {key}");
|
||||
|
||||
if let Some(node) = node {
|
||||
//log::debug!("(hy) found {key}");
|
||||
shared_context.completed.push(node.clone());
|
||||
node
|
||||
if let Some(node) = node {
|
||||
//log::debug!("(hy) found {key}");
|
||||
shared_context.completed.push(node.clone());
|
||||
node
|
||||
} else {
|
||||
//log::debug!("(hy) did NOT find {key}");
|
||||
cloned_template(template)
|
||||
}
|
||||
} else {
|
||||
cloned_template(template)
|
||||
}
|
||||
} else {
|
||||
//log::debug!("(hy) did NOT find {key}");
|
||||
cloned_template(template)
|
||||
}
|
||||
} else {
|
||||
cloned_template(template)
|
||||
}
|
||||
} else {
|
||||
cloned_template(template)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,17 +329,6 @@ impl Scope {
|
||||
self.runtime.all_resources()
|
||||
}
|
||||
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn serialization_resolvers(
|
||||
&self,
|
||||
) -> futures::stream::futures_unordered::FuturesUnordered<
|
||||
std::pin::Pin<Box<dyn futures::Future<Output = (ResourceId, String)>>>,
|
||||
> {
|
||||
self.runtime.serialization_resolvers()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn current_fragment_key(&self) -> String {
|
||||
self.runtime
|
||||
.shared_context
|
||||
@@ -347,7 +338,11 @@ impl Scope {
|
||||
.unwrap_or_else(|| String::from("0f"))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
|
||||
pub fn serialization_resolvers(&self) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
|
||||
self.runtime.serialization_resolvers()
|
||||
}
|
||||
|
||||
pub fn register_suspense(
|
||||
&self,
|
||||
context: SuspenseContext,
|
||||
@@ -377,10 +372,9 @@ impl Scope {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn pending_fragments(&self) -> HashMap<String, Pin<Box<dyn Future<Output = String>>>> {
|
||||
if let Some(ref mut shared_context) = *self.runtime.shared_context.borrow_mut() {
|
||||
std::mem::replace(&mut shared_context.pending_fragments, HashMap::new())
|
||||
std::mem::take(&mut shared_context.pending_fragments)
|
||||
} else {
|
||||
HashMap::new()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSig
|
||||
/// because it reduces them from `O(n)` to `O(1)`.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::{create_effect, create_scope, create_selector, create_signal};
|
||||
/// # use leptos_reactive::{create_isomorphic_effect, create_scope, create_selector, create_signal};
|
||||
/// # use std::rc::Rc;
|
||||
/// # use std::cell::RefCell;
|
||||
/// # create_scope(|cx| {
|
||||
@@ -19,7 +19,7 @@ use crate::{create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSig
|
||||
/// let is_selected = create_selector(cx, a);
|
||||
/// let total_notifications = Rc::new(RefCell::new(0));
|
||||
/// let not = Rc::clone(&total_notifications);
|
||||
/// create_effect(cx, {let is_selected = is_selected.clone(); move |_| {
|
||||
/// create_isomorphic_effect(cx, {let is_selected = is_selected.clone(); move |_| {
|
||||
/// if is_selected(5) {
|
||||
/// *not.borrow_mut() += 1;
|
||||
/// }
|
||||
@@ -91,7 +91,7 @@ where
|
||||
let (read, _) = subs
|
||||
.entry(key.clone())
|
||||
.or_insert_with(|| create_signal(cx, false));
|
||||
_ = read.try_with(|n| n.clone());
|
||||
_ = read.try_with(|n| *n);
|
||||
f(&key, v.borrow().as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use cfg_if::cfg_if;
|
||||
use std::rc::Rc;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -18,82 +19,63 @@ where
|
||||
fn from_json(json: &str) -> Result<Self, SerializationError>;
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
feature = "serde",
|
||||
not(feature = "miniserde"),
|
||||
not(feature = "serde-lite")
|
||||
))]
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
cfg_if! {
|
||||
// prefer miniserde if it's chosen
|
||||
if #[cfg(feature = "miniserde")] {
|
||||
use miniserde::{json, Deserialize, Serialize};
|
||||
|
||||
impl<T> Serializable for T
|
||||
where
|
||||
T: Serialize + Deserialize,
|
||||
{
|
||||
fn to_json(&self) -> Result<String, SerializationError> {
|
||||
Ok(json::to_string(&self))
|
||||
}
|
||||
|
||||
fn from_json(json: &str) -> Result<Self, SerializationError> {
|
||||
json::from_str(&json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
feature = "serde",
|
||||
not(feature = "miniserde"),
|
||||
not(feature = "serde-lite")
|
||||
))]
|
||||
impl<T> Serializable for T
|
||||
where
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
fn to_json(&self) -> Result<String, SerializationError> {
|
||||
serde_json::to_string(&self).map_err(|e| SerializationError::Serialize(Rc::new(e)))
|
||||
}
|
||||
// use serde-lite if enabled
|
||||
else if #[cfg(feature = "serde-lite")] {
|
||||
use serde_lite::{Deserialize, Serialize};
|
||||
|
||||
fn from_json(json: &str) -> Result<Self, SerializationError> {
|
||||
serde_json::from_str(&json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
feature = "serde-lite",
|
||||
not(feature = "serde"),
|
||||
not(feature = "miniserde")
|
||||
))]
|
||||
use serde_lite::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(all(
|
||||
feature = "serde-lite",
|
||||
not(feature = "serde"),
|
||||
not(feature = "miniserde")
|
||||
))]
|
||||
impl<T> Serializable for T
|
||||
where
|
||||
T: Serialize + Deserialize,
|
||||
{
|
||||
fn to_json(&self) -> Result<String, SerializationError> {
|
||||
let intermediate = self
|
||||
.serialize()
|
||||
.map_err(|e| SerializationError::Serialize(Rc::new(e)))?;
|
||||
serde_json::to_string(&intermediate).map_err(|e| SerializationError::Serialize(Rc::new(e)))
|
||||
}
|
||||
|
||||
fn from_json(json: &str) -> Result<Self, SerializationError> {
|
||||
let intermediate =
|
||||
serde_json::from_str(&json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))?;
|
||||
Self::deserialize(&intermediate).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
feature = "miniserde",
|
||||
not(feature = "serde-lite"),
|
||||
not(feature = "serde")
|
||||
))]
|
||||
use miniserde::{json, Deserialize, Serialize};
|
||||
|
||||
#[cfg(all(
|
||||
feature = "miniserde",
|
||||
not(feature = "serde-lite"),
|
||||
not(feature = "serde")
|
||||
))]
|
||||
impl<T> Serializable for T
|
||||
where
|
||||
T: Serialize + Deserialize,
|
||||
{
|
||||
fn to_json(&self) -> Result<String, SerializationError> {
|
||||
Ok(json::to_string(&self))
|
||||
}
|
||||
|
||||
fn from_json(json: &str) -> Result<Self, SerializationError> {
|
||||
json::from_str(&json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
|
||||
impl<T> Serializable for T
|
||||
where
|
||||
T: Serialize + Deserialize,
|
||||
{
|
||||
fn to_json(&self) -> Result<String, SerializationError> {
|
||||
let intermediate = self
|
||||
.serialize()
|
||||
.map_err(|e| SerializationError::Serialize(Rc::new(e)))?;
|
||||
serde_json::to_string(&intermediate).map_err(|e| SerializationError::Serialize(Rc::new(e)))
|
||||
}
|
||||
|
||||
fn from_json(json: &str) -> Result<Self, SerializationError> {
|
||||
let intermediate =
|
||||
serde_json::from_str(&json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))?;
|
||||
Self::deserialize(&intermediate).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// otherwise, or if serde is chosen, default to serde
|
||||
else {
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
impl<T> Serializable for T
|
||||
where
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
fn to_json(&self) -> Result<String, SerializationError> {
|
||||
serde_json::to_string(&self).map_err(|e| SerializationError::Serialize(Rc::new(e)))
|
||||
}
|
||||
|
||||
fn from_json(json: &str) -> Result<Self, SerializationError> {
|
||||
serde_json::from_str(json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ where
|
||||
self.id.with_no_subscription(self.runtime, f)
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub(crate) fn subscribe(&self) {
|
||||
self.id.subscribe(self.runtime);
|
||||
}
|
||||
@@ -477,14 +478,6 @@ where
|
||||
self.id.with(self.runtime, f)
|
||||
}
|
||||
|
||||
pub(crate) fn with_no_subscription<U>(&self, f: impl FnOnce(&T) -> U) -> U {
|
||||
self.id.with_no_subscription(self.runtime, f)
|
||||
}
|
||||
|
||||
pub(crate) fn subscribe(&self) {
|
||||
self.id.subscribe(self.runtime);
|
||||
}
|
||||
|
||||
/// Clones and returns the current value of the signal, and subscribes
|
||||
/// the running effect to this signal.
|
||||
/// ```
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
use cfg_if::cfg_if;
|
||||
use std::future::Future;
|
||||
|
||||
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// in the browser, and simply runs the given function when on the server.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn queue_microtask(task: impl FnOnce()) {
|
||||
task();
|
||||
}
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// in the browser, and simply runs the given function when on the server.
|
||||
pub fn queue_microtask(task: impl FnOnce() + 'static) {
|
||||
microtask(wasm_bindgen::closure::Closure::once_into_js(task));
|
||||
}
|
||||
|
||||
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// in the browser, and simply runs the given function when on the server.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn queue_microtask(task: impl FnOnce() + 'static) {
|
||||
microtask(wasm_bindgen::closure::Closure::once_into_js(task));
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(
|
||||
inline_js = "export function microtask(f) { queueMicrotask(f); }"
|
||||
)]
|
||||
extern "C" {
|
||||
fn microtask(task: wasm_bindgen::JsValue);
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(
|
||||
inline_js = "export function microtask(f) { queueMicrotask(f); }"
|
||||
)]
|
||||
extern "C" {
|
||||
fn microtask(task: wasm_bindgen::JsValue);
|
||||
}
|
||||
} else {
|
||||
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// in the browser, and simply runs the given function when on the server.
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
pub fn queue_microtask(task: impl FnOnce()) {
|
||||
task();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_local<F>(fut: F)
|
||||
@@ -29,11 +33,12 @@ where
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
wasm_bindgen_futures::spawn_local(fut)
|
||||
}
|
||||
else if #[cfg(any(test, doctest))] {
|
||||
tokio_test::block_on(fut);
|
||||
} else if #[cfg(feature = "ssr")] {
|
||||
tokio::task::spawn_local(fut);
|
||||
} else if #[cfg(any(test, doctest))] {
|
||||
tokio_test::block_on(fut);
|
||||
} else {
|
||||
} else {
|
||||
futures::executor::block_on(fut)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
[package]
|
||||
name = "leptos_server"
|
||||
version = "0.0.10"
|
||||
version = "0.0.11"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.10" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.10" }
|
||||
async-trait = "0.1"
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.11" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.11" }
|
||||
form_urlencoded = "1"
|
||||
gloo-net = { version = "0.2", optional = true }
|
||||
gloo-net = "0.2"
|
||||
lazy_static = "1"
|
||||
linear-map = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
|
||||
[features]
|
||||
csr = ["leptos_dom/csr", "leptos_reactive/csr", "dep:gloo-net"]
|
||||
hydrate = ["leptos_dom/hydrate", "leptos_reactive/hydrate", "dep:gloo-net"]
|
||||
csr = ["leptos_dom/csr", "leptos_reactive/csr"]
|
||||
hydrate = ["leptos_dom/hydrate", "leptos_reactive/hydrate"]
|
||||
ssr = ["leptos_dom/ssr", "leptos_reactive/ssr"]
|
||||
stable = ["leptos_dom/stable", "leptos_reactive/stable"]
|
||||
stable = ["leptos_dom/stable", "leptos_reactive/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
|
||||
@@ -67,10 +67,9 @@
|
||||
//! signal. This is very useful, as it can be used to invalidate a [Resource](leptos_reactive::Resource)
|
||||
//! that reads from the same data.
|
||||
|
||||
pub use async_trait::async_trait;
|
||||
pub use form_urlencoded;
|
||||
use leptos_reactive::*;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{future::Future, pin::Pin, rc::Rc};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -97,7 +96,6 @@ pub fn server_fn_by_path(path: &str) -> Option<Arc<ServerFnTraitObj>> {
|
||||
.and_then(|fns| fns.get(path).cloned())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ServerFn
|
||||
where
|
||||
Self: Sized + 'static,
|
||||
@@ -111,7 +109,10 @@ where
|
||||
fn from_form_data(data: &[u8]) -> Result<Self, ServerFnError>;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn call_fn(self) -> Result<Self::Output, ServerFnError>;
|
||||
fn call_fn(self) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>> + Send>>;
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn call_fn_client(self) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn register() -> Result<(), ServerFnError> {
|
||||
@@ -119,7 +120,7 @@ where
|
||||
// takes a String -> returns its async value
|
||||
let run_server_fn = Arc::new(|data: &[u8]| {
|
||||
// decode the args
|
||||
let value = Self::from_form_data(&data);
|
||||
let value = Self::from_form_data(data);
|
||||
Box::pin(async move {
|
||||
let value = match value {
|
||||
Ok(v) => v,
|
||||
@@ -173,15 +174,13 @@ pub enum ServerFnError {
|
||||
MissingArg(String),
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub async fn call_server_fn<T>(url: &str, args: impl ServerFn) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: Serializable + Sized,
|
||||
{
|
||||
use leptos_dom::*;
|
||||
|
||||
let window = window();
|
||||
|
||||
let args_form_data = web_sys::FormData::new().expect_throw("could not create FormData");
|
||||
for (field_name, value) in args.as_form_data().into_iter() {
|
||||
args_form_data
|
||||
@@ -202,7 +201,7 @@ where
|
||||
|
||||
// check for error status
|
||||
let status = resp.status();
|
||||
if status >= 500 && status <= 599 {
|
||||
if (500..=599).contains(&status) {
|
||||
return Err(ServerFnError::ServerError(resp.status_text()));
|
||||
}
|
||||
|
||||
@@ -215,22 +214,28 @@ where
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Action<T>
|
||||
pub struct Action<I, O>
|
||||
where
|
||||
T: 'static,
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
{
|
||||
pub version: RwSignal<usize>,
|
||||
value: RwSignal<Option<T>>,
|
||||
input: RwSignal<Option<I>>,
|
||||
value: RwSignal<Option<O>>,
|
||||
pending: RwSignal<bool>,
|
||||
action_fn: Rc<dyn Fn() -> Pin<Box<dyn Future<Output = T>>>>,
|
||||
url: Option<&'static str>,
|
||||
#[allow(clippy::complexity)]
|
||||
action_fn: Rc<dyn Fn(&I) -> Pin<Box<dyn Future<Output = O>>>>,
|
||||
}
|
||||
|
||||
impl<T> Action<T>
|
||||
impl<I, O> Action<I, O>
|
||||
where
|
||||
T: 'static,
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
{
|
||||
pub fn invalidator(&self) {
|
||||
_ = self.version.get();
|
||||
pub fn using_server_fn<T: ServerFn>(mut self) -> Self {
|
||||
self.url = Some(T::url());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pending(&self) -> impl Fn() -> bool {
|
||||
@@ -238,12 +243,21 @@ where
|
||||
move || value.with(|val| val.is_some())
|
||||
}
|
||||
|
||||
pub fn value(&self) -> ReadSignal<Option<T>> {
|
||||
pub fn input(&self) -> ReadSignal<Option<I>> {
|
||||
self.input.read_only()
|
||||
}
|
||||
|
||||
pub fn value(&self) -> ReadSignal<Option<O>> {
|
||||
self.value.read_only()
|
||||
}
|
||||
|
||||
pub fn dispatch(&self) {
|
||||
let fut = (self.action_fn)();
|
||||
pub fn url(&self) -> Option<&str> {
|
||||
self.url
|
||||
}
|
||||
|
||||
pub fn dispatch(&self, input: I) {
|
||||
let fut = (self.action_fn)(&input);
|
||||
self.input.set(Some(input));
|
||||
let version = self.version;
|
||||
let pending = self.pending;
|
||||
let value = self.value;
|
||||
@@ -257,24 +271,39 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_action<T, F, Fu>(cx: Scope, action_fn: F) -> Action<T>
|
||||
pub fn create_action<I, O, F, Fu>(cx: Scope, action_fn: F) -> Action<I, O>
|
||||
where
|
||||
T: 'static,
|
||||
F: Fn() -> Fu + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
let version = create_rw_signal(cx, 0);
|
||||
let input = create_rw_signal(cx, None);
|
||||
let value = create_rw_signal(cx, None);
|
||||
let pending = create_rw_signal(cx, false);
|
||||
let action_fn = Rc::new(move || {
|
||||
let fut = action_fn();
|
||||
Box::pin(async move { fut.await }) as Pin<Box<dyn Future<Output = T>>>
|
||||
let action_fn = Rc::new(move |input: &I| {
|
||||
let fut = action_fn(input);
|
||||
Box::pin(async move { fut.await }) as Pin<Box<dyn Future<Output = O>>>
|
||||
});
|
||||
|
||||
Action {
|
||||
version,
|
||||
url: None,
|
||||
input,
|
||||
value,
|
||||
pending,
|
||||
action_fn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_server_action<S>(cx: Scope) -> Action<S, Result<S::Output, ServerFnError>>
|
||||
where
|
||||
S: Clone + ServerFn,
|
||||
{
|
||||
#[cfg(feature = "ssr")]
|
||||
let c = |args: &S| S::call_fn(args.clone());
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let c = |args: &S| S::call_fn_client(args.clone());
|
||||
create_action(cx, c).using_server_fn::<S>()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ description = "Router for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../leptos", version = "0.0", default-features = false }
|
||||
cfg-if = "1"
|
||||
common_macros = "0.1"
|
||||
gloo-net = "0.2"
|
||||
itertools = "0.10"
|
||||
lazy_static = "1"
|
||||
linear-map = "1"
|
||||
@@ -20,9 +22,9 @@ url = { version = "2", optional = true }
|
||||
urlencoding = "2"
|
||||
thiserror = "1"
|
||||
typed-builder = "0.10"
|
||||
js-sys = { version = "0.3", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4", optional = true }
|
||||
js-sys = { version = "0.3" }
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
wasm-bindgen-futures = { version = "0.4" }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
@@ -50,12 +52,11 @@ features = [
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "dep:js-sys", "dep:wasm-bindgen"]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:js-sys",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:wasm-bindgen-futures",
|
||||
]
|
||||
default = []
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr", "dep:url", "dep:regex"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
# No need to test optional dependencies as they are enabled by the ssr feature
|
||||
denylist = ["url", "regex"]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::error::Error;
|
||||
|
||||
use leptos::*;
|
||||
use typed_builder::TypedBuilder;
|
||||
use wasm_bindgen::JsCast;
|
||||
@@ -10,11 +12,15 @@ where
|
||||
A: ToHref + 'static,
|
||||
{
|
||||
#[builder(default, setter(strip_option))]
|
||||
method: Option<String>,
|
||||
method: Option<&'static str>,
|
||||
action: A,
|
||||
#[builder(default, setter(strip_option))]
|
||||
enctype: Option<String>,
|
||||
children: Box<dyn Fn() -> Vec<Element>>,
|
||||
#[builder(default, setter(strip_option))]
|
||||
version: Option<RwSignal<usize>>,
|
||||
#[builder(default, setter(strip_option))]
|
||||
error: Option<RwSignal<Option<Box<dyn Error>>>>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
@@ -27,8 +33,11 @@ where
|
||||
action,
|
||||
enctype,
|
||||
children,
|
||||
version,
|
||||
error,
|
||||
} = props;
|
||||
|
||||
let action_version = version;
|
||||
let action = use_resolved_path(cx, move || action.to_href()());
|
||||
|
||||
let on_submit = move |ev: web_sys::Event| {
|
||||
@@ -124,22 +133,51 @@ where
|
||||
},
|
||||
};
|
||||
|
||||
if method == "get" {
|
||||
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||
let params =
|
||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
|
||||
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||
let params =
|
||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
|
||||
let action = use_resolved_path(cx, move || action.clone())
|
||||
.get()
|
||||
.unwrap_or_default();
|
||||
// POST
|
||||
if method == "post" {
|
||||
spawn_local(async move {
|
||||
let res = gloo_net::http::Request::post(&action)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", &enctype)
|
||||
.body(params)
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
log::error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(error) = error {
|
||||
error.set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
if let Some(version) = action_version {
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
|
||||
if resp.status() == 303 {
|
||||
if let Some(redirect_url) = resp.headers().get("Location") {
|
||||
navigate(&redirect_url, Default::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// otherwise, GET
|
||||
else {
|
||||
let params = params.to_string().as_string().unwrap_or_default();
|
||||
let action = use_resolved_path(cx, move || action.clone())
|
||||
.get()
|
||||
.unwrap_or_default();
|
||||
navigate(&format!("{action}?{params}"), Default::default());
|
||||
} else {
|
||||
// TODO POST
|
||||
leptos_dom::debug_warn!("<Form/> component: POST not yet implemented");
|
||||
todo!()
|
||||
}
|
||||
};
|
||||
|
||||
let children = children();
|
||||
|
||||
view! { cx,
|
||||
<form
|
||||
method=method
|
||||
@@ -151,3 +189,38 @@ where
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct ActionFormProps<I, O>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
{
|
||||
action: Action<I, O>,
|
||||
children: Box<dyn Fn() -> Vec<Element>>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ActionForm<I, O>(cx: Scope, props: ActionFormProps<I, O>) -> Element
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
{
|
||||
let action = if let Some(url) = props.action.url() {
|
||||
format!("/{url}")
|
||||
} else {
|
||||
debug_warn!("<ActionForm/> action needs a URL. Either use create_server_action() or Action::using_server_fn().");
|
||||
"".to_string()
|
||||
};
|
||||
let version = props.action.version;
|
||||
|
||||
Form(
|
||||
cx,
|
||||
FormProps::builder()
|
||||
.action(action)
|
||||
.version(version)
|
||||
.method("post")
|
||||
.children(props.children)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::leptos_dom::IntoChild;
|
||||
use leptos::*;
|
||||
use typed_builder::TypedBuilder;
|
||||
@@ -89,14 +90,27 @@ where
|
||||
}
|
||||
let child = children.remove(0);
|
||||
|
||||
view! { cx,
|
||||
<a
|
||||
href=move || href().unwrap_or_default()
|
||||
prop:state={props.state.map(|s| s.to_js_value())}
|
||||
prop:replace={props.replace}
|
||||
aria-current=move || if is_active() { Some("page") } else { None }
|
||||
>
|
||||
{child}
|
||||
</a>
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
view! { cx,
|
||||
<a
|
||||
href=move || href().unwrap_or_default()
|
||||
prop:state={props.state.map(|s| s.to_js_value())}
|
||||
prop:replace={props.replace}
|
||||
aria-current=move || if is_active() { Some("page") } else { None }
|
||||
>
|
||||
{child}
|
||||
</a>
|
||||
}
|
||||
} else {
|
||||
view! { cx,
|
||||
<a
|
||||
href=move || href().unwrap_or_default()
|
||||
aria-current=move || if is_active() { Some("page") } else { None }
|
||||
>
|
||||
{child}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use cfg_if::cfg_if;
|
||||
use std::ops::IndexMut;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
@@ -72,11 +73,14 @@ impl std::fmt::Debug for RouterContextInner {
|
||||
|
||||
impl RouterContext {
|
||||
pub fn new(cx: Scope, base: Option<&'static str>, fallback: Option<fn() -> Element>) -> Self {
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
let history = use_context::<RouterIntegrationContext>(cx)
|
||||
.unwrap_or_else(|| RouterIntegrationContext(Rc::new(crate::BrowserIntegration {})));
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
let history = use_context::<RouterIntegrationContext>(cx).expect("You must call provide_context::<RouterIntegrationContext>(cx, ...) somewhere above the <Router/>.");
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
let history = use_context::<RouterIntegrationContext>(cx)
|
||||
.unwrap_or_else(|| RouterIntegrationContext(Rc::new(crate::BrowserIntegration {})));
|
||||
} else {
|
||||
let history = use_context::<RouterIntegrationContext>(cx).expect("You must call provide_context::<RouterIntegrationContext>(cx, ...) somewhere above the <Router/>.");
|
||||
}
|
||||
};
|
||||
|
||||
// Any `History` type gives a way to get a reactive signal of the current location
|
||||
// in the browser context, this is drawn from the `popstate` event
|
||||
@@ -143,7 +147,7 @@ impl RouterContext {
|
||||
});
|
||||
|
||||
// handle all click events on anchor tags
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
leptos_dom::window_event_listener("click", {
|
||||
let inner = Rc::clone(&inner);
|
||||
move |ev| inner.clone().handle_anchor_click(ev)
|
||||
@@ -253,7 +257,7 @@ impl RouterContextInner {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub(crate) fn handle_anchor_click(self: Rc<Self>, ev: web_sys::Event) {
|
||||
let ev = ev.unchecked_into::<web_sys::MouseEvent>();
|
||||
if ev.default_prevented()
|
||||
|
||||
@@ -184,7 +184,7 @@ impl std::fmt::Debug for Loader {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[cfg(all(feature = "ssr", not(feature = "hydrate")))]
|
||||
pub async fn loader_to_json(view: impl Fn(Scope) -> String + 'static) -> Option<String> {
|
||||
let (data, _, disposer) = run_scope_undisposed(move |cx| async move {
|
||||
let _shell = view(cx);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use leptos::*;
|
||||
|
||||
mod location;
|
||||
@@ -22,11 +24,9 @@ pub trait History {
|
||||
fn navigate(&self, loc: &LocationChange);
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct BrowserIntegration {}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
impl BrowserIntegration {
|
||||
fn current() -> LocationChange {
|
||||
let loc = leptos_dom::location();
|
||||
@@ -41,7 +41,6 @@ impl BrowserIntegration {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
impl History for BrowserIntegration {
|
||||
fn location(&self, cx: Scope) -> ReadSignal<LocationChange> {
|
||||
use crate::{NavigateOptions, RouterContext};
|
||||
@@ -107,7 +106,13 @@ impl History for BrowserIntegration {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RouterIntegrationContext(pub std::rc::Rc<dyn History>);
|
||||
pub struct RouterIntegrationContext(pub Rc<dyn History>);
|
||||
|
||||
impl RouterIntegrationContext {
|
||||
pub fn new(history: impl History + 'static) -> Self {
|
||||
Self(Rc::new(history))
|
||||
}
|
||||
}
|
||||
|
||||
impl History for RouterIntegrationContext {
|
||||
fn location(&self, cx: Scope) -> ReadSignal<LocationChange> {
|
||||
@@ -118,3 +123,25 @@ impl History for RouterIntegrationContext {
|
||||
self.0.navigate(loc)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ServerIntegration {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl History for ServerIntegration {
|
||||
fn location(&self, cx: leptos::Scope) -> ReadSignal<LocationChange> {
|
||||
create_signal(
|
||||
cx,
|
||||
LocationChange {
|
||||
value: self.path.clone(),
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state: State(None),
|
||||
},
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
fn navigate(&self, _loc: &LocationChange) {}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use leptos::wasm_bindgen::JsValue;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub struct State(pub Option<JsValue>);
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub struct State(pub Option<()>);
|
||||
|
||||
impl State {
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn to_js_value(&self) -> JsValue {
|
||||
match &self.0 {
|
||||
Some(v) => v.clone(),
|
||||
@@ -19,7 +12,6 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
impl<T> From<T> for State
|
||||
where
|
||||
T: Into<JsValue>,
|
||||
|
||||
@@ -36,7 +36,7 @@ pub fn unescape(s: &str) -> String {
|
||||
.replace('+', " ")
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn unescape(s: &str) -> String {
|
||||
js_sys::decode_uri(s).unwrap().into()
|
||||
}
|
||||
@@ -46,12 +46,12 @@ pub fn escape(s: &str) -> String {
|
||||
urlencoding::encode(s).into()
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn escape(s: &str) -> String {
|
||||
js_sys::encode_uri(s).as_string().unwrap()
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
impl TryFrom<&str> for Url {
|
||||
type Error = String;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ fn has_scheme(path: &str) -> bool {
|
||||
HAS_SCHEME_RE.is_match(path)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn has_scheme(path: &str) -> bool {
|
||||
let re = js_sys::RegExp::new(HAS_SCHEME, "");
|
||||
re.test(path)
|
||||
@@ -75,7 +75,7 @@ const BEGINS_WITH_QUERY_OR_HASH: &str = r#"^[?#]"#;
|
||||
const HAS_SCHEME: &str = r#"^(?:[a-z0-9]+:)?//"#;
|
||||
const QUERY: &str = r#"/*(\*.*)?$"#;
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn replace_trim_path<'a>(text: &'a str, replace: &str) -> Cow<'a, str> {
|
||||
let re = js_sys::RegExp::new(TRIM_PATH, "g");
|
||||
js_sys::JsString::from(text)
|
||||
@@ -85,13 +85,13 @@ fn replace_trim_path<'a>(text: &'a str, replace: &str) -> Cow<'a, str> {
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn begins_with_query_or_hash(text: &str) -> bool {
|
||||
let re = js_sys::RegExp::new(BEGINS_WITH_QUERY_OR_HASH, "");
|
||||
re.test(text)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn replace_query(text: &str) -> String {
|
||||
let re = js_sys::RegExp::new(QUERY, "g");
|
||||
js_sys::JsString::from(text)
|
||||
|
||||
Reference in New Issue
Block a user