diff --git a/examples/01-app-demos/bluetooth-scanner/src/main.rs b/examples/01-app-demos/bluetooth-scanner/src/main.rs index 662449f42..6843c65e4 100644 --- a/examples/01-app-demos/bluetooth-scanner/src/main.rs +++ b/examples/01-app-demos/bluetooth-scanner/src/main.rs @@ -43,11 +43,11 @@ fn app() -> Element { h2 { class: "text-2xl font-bold", "Scan for Bluetooth Devices" } button { class: "inline-block w-full md:w-auto px-6 py-3 font-medium text-white bg-indigo-500 hover:bg-indigo-600 rounded transition duration-200", - disabled: scan.is_pending(), + disabled: scan.pending(), onclick: move |_| { scan.call(); }, - if scan.is_pending() { "Scanning" } else { "Scan" } + if scan.pending() { "Scanning" } else { "Scan" } } } } @@ -64,8 +64,8 @@ fn app() -> Element { th { class: "pb-3 px-2 font-medium", "Security" } } } - match scan.result() { - None if scan.is_pending() => rsx! { "Scanning..." }, + match scan.value() { + None if scan.pending() => rsx! { "Scanning..." }, None => rsx! { "Press Scan to start scanning" }, Some(Err(_err)) => rsx! { "Failed to scan" }, Some(Ok(peripherals)) => rsx! { diff --git a/examples/01-app-demos/dog_app.rs b/examples/01-app-demos/dog_app.rs index 020a17842..d60075568 100644 --- a/examples/01-app-demos/dog_app.rs +++ b/examples/01-app-demos/dog_app.rs @@ -53,7 +53,7 @@ fn app() -> Element { } } div { - match breed.result() { + match breed.value() { None => rsx! { div { "Click the button to fetch a dog!" } }, Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } }, Some(Ok(res)) => rsx! { diff --git a/examples/01-app-demos/image_generator_openai.rs b/examples/01-app-demos/image_generator_openai.rs index 99dc7bc3d..360757021 100644 --- a/examples/01-app-demos/image_generator_openai.rs +++ b/examples/01-app-demos/image_generator_openai.rs @@ -69,13 +69,13 @@ fn app() -> Element { } button { class: "button is-primary", - class: if image.is_pending() { "is-loading" }, + class: if image.pending() { "is-loading" }, onclick: move |_| { image.call(); }, "Generate image" } - if let Some(image) = image.value() { + if let Some(Ok(image)) = image.value() { for image in image.read().data.as_slice() { section { class: "is-flex", div { class: "container is-fluid", diff --git a/examples/01-app-demos/weather_app.rs b/examples/01-app-demos/weather_app.rs index 70b09b8b6..2d0069bbc 100644 --- a/examples/01-app-demos/weather_app.rs +++ b/examples/01-app-demos/weather_app.rs @@ -20,7 +20,7 @@ fn app() -> Element { let current_weather = use_loader(move || get_weather(country())); rsx! { - Stylesheet { href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" } + Stylesheet { href: asset!("/examples/assets/weatherapp.css") } div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center", div { class: "flex items-center justify-center flex-row", div { class: "flex items-start justify-center flex-row", @@ -35,6 +35,11 @@ fn app() -> Element { weather: weather.cloned(), } Forecast { weather: weather.cloned() } + div { height: "20px", margin_top: "10px", + if weather.loading() { + "Fetching weather data..." + } + } }, Err(Loading::Pending(_)) => rsx! { div { "Loading weather data..." } @@ -56,8 +61,8 @@ fn app() -> Element { #[component] fn CountryData(weather: WeatherResponse, country: WeatherLocation) -> Element { let today = "Today"; - let max_temp = weather.daily.temperature_2m_max.first().unwrap(); - let min_temp = weather.daily.temperature_2m_min.first().unwrap(); + let max_temp = weather.daily.temperature_2m_max[0]; + let min_temp = weather.daily.temperature_2m_min[0]; rsx! { div { class: "flex mb-4 justify-between items-center", @@ -82,15 +87,11 @@ fn CountryData(weather: WeatherResponse, country: WeatherLocation) -> Element { #[allow(non_snake_case)] #[component] fn Forecast(weather: WeatherResponse) -> Element { - let today = (weather.daily.temperature_2m_max.first().unwrap() - + weather.daily.temperature_2m_max.first().unwrap()) - / 2.0; - let tomorrow = (weather.daily.temperature_2m_max.get(1).unwrap() - + weather.daily.temperature_2m_max.get(1).unwrap()) - / 2.0; - let past_tomorrow = (weather.daily.temperature_2m_max.get(2).unwrap() - + weather.daily.temperature_2m_max.get(2).unwrap()) - / 2.0; + let today = (weather.daily.temperature_2m_max[0] + weather.daily.temperature_2m_max[0]) / 2.0; + let tomorrow = + (weather.daily.temperature_2m_max[1] + weather.daily.temperature_2m_max[1]) / 2.0; + let past_tomorrow = + (weather.daily.temperature_2m_max[2] + weather.daily.temperature_2m_max[2]) / 2.0; rsx! { div { class: "px-6 pt-4 relative", @@ -117,7 +118,7 @@ fn Forecast(weather: WeatherResponse) -> Element { #[component] fn SearchBox(mut country: WriteSignal) -> Element { let mut input = use_signal(|| "".to_string()); - let locations = use_resource(move || get_locations(input())); + let locations = use_loader(move || get_locations(input())); rsx! { div { @@ -145,17 +146,32 @@ fn SearchBox(mut country: WriteSignal) -> Element { } } ul { class: "bg-white border border-gray-100 w-full mt-2 max-h-72 overflow-auto", - if let Some(Ok(locs)) = locations.read().as_ref() { - for wl in locs.iter().take(5).cloned() { - li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900", - onclick: move |_| country.set(wl.clone()), - MapIcon {} - b { "{wl.name}" } - " · {wl.country}" + match locations { + Ok(locs) if locs.is_empty() => rsx! { + li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative", + "No locations found" + } + }, + Ok(locs) => rsx! { + for wl in locs.read().iter().take(5).cloned() { + li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900", + onclick: move |_| country.set(wl.clone()), + MapIcon {} + b { "{wl.name}" } + " · {wl.country}" + } + } + }, + Err(Loading::Pending(_)) => rsx! { + li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative", + "Searching..." + } + }, + Err(Loading::Failed(handle)) => rsx! { + li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative", + "Failed to search: {handle.error():?}" } } - } else { - "loading locations..." } } } @@ -200,10 +216,11 @@ type WeatherLocations = Vec; #[derive(Debug, Default, Serialize, Deserialize)] struct SearchResponse { + #[serde(default)] results: WeatherLocations, } -async fn get_locations(input: impl Display) -> reqwest::Result { +async fn get_locations(input: impl Display) -> Result { let res = reqwest::get(&format!( "https://geocoding-api.open-meteo.com/v1/search?name={input}" )) diff --git a/examples/05-using-async/suspense.rs b/examples/05-using-async/suspense.rs index dfa8aee32..cf8811bbe 100644 --- a/examples/05-using-async/suspense.rs +++ b/examples/05-using-async/suspense.rs @@ -43,8 +43,7 @@ fn Doggo() -> Element { } reqwest::get("https://dog.ceo/api/breeds/image/random/") - .await - .unwrap() + .await? .json::() .await })?; diff --git a/examples/07-fullstack/auth/src/main.rs b/examples/07-fullstack/auth/src/main.rs index 30d473dcb..8092f1cc3 100644 --- a/examples/07-fullstack/auth/src/main.rs +++ b/examples/07-fullstack/auth/src/main.rs @@ -105,9 +105,9 @@ fn app() -> Element { "Fetch User Info" } - pre { "Logged in: {login.result():?}" } - pre { "User name: {user_name.result():?}" } - pre { "Permissions: {permissions.result():?}" } + pre { "Logged in: {login.value():?}" } + pre { "User name: {user_name.value():?}" } + pre { "Permissions: {permissions.value():?}" } } } } diff --git a/examples/07-fullstack/dog_app_self_hosted.rs b/examples/07-fullstack/dog_app_self_hosted.rs index 31062ae46..36f91f83c 100644 --- a/examples/07-fullstack/dog_app_self_hosted.rs +++ b/examples/07-fullstack/dog_app_self_hosted.rs @@ -22,7 +22,7 @@ fn app() -> Element { } } div { - match breed.result() { + match breed.value() { None => rsx! { div { "Click the button to fetch a dog!" } }, Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } }, Some(Ok(res)) => rsx! { img { max_width: "500px", max_height: "500px", src: "{res}" } }, diff --git a/examples/07-fullstack/handling_errors.rs b/examples/07-fullstack/handling_errors.rs index b1bc6431f..703f6e7bc 100644 --- a/examples/07-fullstack/handling_errors.rs +++ b/examples/07-fullstack/handling_errors.rs @@ -72,12 +72,12 @@ fn main() { pre { "Dog data: {dog_data.value():#?}" } pre { "IP data: {ip_data.value():#?}" } pre { "Custom encoded data: {custom_data.value():#?}" } - pre { "Error data: {error_data.result():#?}" } - pre { "Typed error data: {typed_error_data.result():#?}" } - pre { "HTTP 400 data: {http_error_data.result():#?}" } - pre { "HTTP 400 (context) data: {http_error_context_data.result():#?}" } - pre { "Dog error data: {dog_data_err.result():#?}" } - pre { "Throws ok data: {throws_ok_data.result():#?}" } + pre { "Error data: {error_data.value():#?}" } + pre { "Typed error data: {typed_error_data.value():#?}" } + pre { "HTTP 400 data: {http_error_data.value():#?}" } + pre { "HTTP 400 (context) data: {http_error_context_data.value():#?}" } + pre { "Dog error data: {dog_data_err.value():#?}" } + pre { "Throws ok data: {throws_ok_data.value():#?}" } } } }); diff --git a/examples/07-fullstack/header_map.rs b/examples/07-fullstack/header_map.rs index cba415b7f..3f187635a 100644 --- a/examples/07-fullstack/header_map.rs +++ b/examples/07-fullstack/header_map.rs @@ -21,7 +21,7 @@ fn app() -> Element { rsx! { h1 { "Header Map Example" } button { onclick: move |_| headers.call(), "Get Headers" } - if let Some(Ok(headers)) = headers.result() { + if let Some(Ok(headers)) = headers.value() { p { "Response from server:" } pre { "{headers}" } } else { diff --git a/examples/07-fullstack/hello-world/src/main.rs b/examples/07-fullstack/hello-world/src/main.rs index 97eeb9182..9a17f879f 100644 --- a/examples/07-fullstack/hello-world/src/main.rs +++ b/examples/07-fullstack/hello-world/src/main.rs @@ -38,7 +38,7 @@ fn app() -> Element { div { style: "margin: 1rem 0;", h2 { "Server Greeting" } button { onclick: move |_| message.call("World".to_string(), 30), "Get Server Greeting" } - if message.is_pending() { + if message.pending() { p { "Loading..." } } p { "{message:#?}" } diff --git a/examples/07-fullstack/login_form.rs b/examples/07-fullstack/login_form.rs index d553d7c18..935326d10 100644 --- a/examples/07-fullstack/login_form.rs +++ b/examples/07-fullstack/login_form.rs @@ -34,7 +34,7 @@ fn app() -> Element { }, "Get Sensitive Data", } - pre { "Response from locked API: {fetch_sensitive.result():?}"} + pre { "Response from locked API: {fetch_sensitive.value():?}"} form { onsubmit: move |evt: FormEvent| async move { // Prevent the browser from navigating away. diff --git a/examples/07-fullstack/middleware.rs b/examples/07-fullstack/middleware.rs index 0bd05f187..2f76e31e3 100644 --- a/examples/07-fullstack/middleware.rs +++ b/examples/07-fullstack/middleware.rs @@ -37,7 +37,7 @@ fn app() -> Element { rsx! { h1 { "Fullstack Middleware Example" } button { onclick: move |_| per_route.call(), "Fetch Data" } - pre { "{per_route.result():#?}" } + pre { "{per_route.value():#?}" } } } diff --git a/examples/07-fullstack/server_functions.rs b/examples/07-fullstack/server_functions.rs index 99ead625d..9811376da 100644 --- a/examples/07-fullstack/server_functions.rs +++ b/examples/07-fullstack/server_functions.rs @@ -174,13 +174,13 @@ fn app() -> Element { "Clear results" } - pre { "Echo result: {echo_action.result():#?}" } - pre { "Chat result: {chat_action.result():#?}" } - pre { "Dog data: {dog_data.result():#?}" } - pre { "Custom data: {custom_data.result():#?}" } - pre { "Anonymous: {anonymous_action.result():#?}" } - pre { "Custom anonymous: {custom_anonymous_action.result():#?}" } - pre { "Custom response: {custom_response_action.result():#?}" } + pre { "Echo result: {echo_action.value():#?}" } + pre { "Chat result: {chat_action.value():#?}" } + pre { "Dog data: {dog_data.value():#?}" } + pre { "Custom data: {custom_data.value():#?}" } + pre { "Anonymous: {anonymous_action.value():#?}" } + pre { "Custom anonymous: {custom_anonymous_action.value():#?}" } + pre { "Custom response: {custom_response_action.value():#?}" } } } } diff --git a/examples/07-fullstack/server_state.rs b/examples/07-fullstack/server_state.rs index 5c5ffac48..96ba6e42c 100644 --- a/examples/07-fullstack/server_state.rs +++ b/examples/07-fullstack/server_state.rs @@ -108,13 +108,13 @@ fn app() -> Element { rsx! { div { button { onclick: move |_| users.call(), "Get Users" } - pre { "{users.result():?}" } + pre { "{users.value():?}" } button { onclick: move |_| messages.call(), "Get Messages" } - pre { "{messages.result():?}" } + pre { "{messages.value():?}" } button { onclick: move |_| broadcast.call(), "Broadcast Message" } - pre { "{broadcast.result():?}" } + pre { "{broadcast.value():?}" } button { onclick: move |_| add.call(), "Add Message" } - pre { "{add.result():?}" } + pre { "{add.value():?}" } } } } diff --git a/examples/07-fullstack/ssr-only/src/main.rs b/examples/07-fullstack/ssr-only/src/main.rs index b04100612..f2f950315 100644 --- a/examples/07-fullstack/ssr-only/src/main.rs +++ b/examples/07-fullstack/ssr-only/src/main.rs @@ -36,12 +36,12 @@ fn Home() -> Element { } #[component] -fn Post(id: u32) -> Element { +fn Post(id: ReadSignal) -> Element { // You can return `HttpError` to return a specific HTTP status code and message. // `404 Not Found` will cause the server to return a 404 status code. // // `use_loader` will suspend the server-side rendering until the future resolves. - let post_data = use_loader(move || get_post(id))?; + let post_data = use_loader(move || get_post(id()))?; rsx! { h1 { "Post {id}" } diff --git a/examples/07-fullstack/streaming_file_upload.rs b/examples/07-fullstack/streaming_file_upload.rs index 1aaadeff0..3c9560e32 100644 --- a/examples/07-fullstack/streaming_file_upload.rs +++ b/examples/07-fullstack/streaming_file_upload.rs @@ -80,7 +80,7 @@ fn app() -> Element { }, "Drop files here" } - pre { "{upload_as_file_upload.result():?}" } + pre { "{upload_as_file_upload.value():?}" } } div { @@ -99,9 +99,9 @@ fn app() -> Element { div { h3 { "Download a file from the server" } button { onclick: move |_| download_file.call(), "Download file" } - if let Some(Ok(content)) = &download_file.result() { + if let Some(Ok(content)) = &download_file.value() { pre { "{content}" } - } else if let Some(Err(e)) = &download_file.result() { + } else if let Some(Err(e)) = &download_file.value() { pre { "Error downloading file: {e}" } } } diff --git a/examples/assets/weatherapp.css b/examples/assets/weatherapp.css new file mode 100644 index 000000000..e9e01796c --- /dev/null +++ b/examples/assets/weatherapp.css @@ -0,0 +1,1523 @@ +/*! tailwindcss v4.1.5 | MIT License | https://tailwindcss.com */ +@layer properties; +@layer theme, base, components, utilities; + +@layer theme { + + :root, + :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + monospace; + --color-orange-300: oklch(83.7% 0.128 66.29); + --color-orange-400: oklch(75% 0.183 55.934); + --color-yellow-50: oklch(98.7% 0.026 102.212); + --color-yellow-600: oklch(68.1% 0.162 75.834); + --color-green-500: oklch(72.3% 0.219 149.579); + --color-blue-300: oklch(80.9% 0.105 251.813); + --color-indigo-500: oklch(58.5% 0.233 277.117); + --color-indigo-600: oklch(51.1% 0.262 276.966); + --color-purple-50: oklch(97.7% 0.014 308.299); + --color-purple-500: oklch(62.7% 0.265 303.9); + --color-gray-50: oklch(98.5% 0.002 247.839); + --color-gray-100: oklch(96.7% 0.003 264.542); + --color-gray-200: oklch(92.8% 0.006 264.531); + --color-gray-400: oklch(70.7% 0.022 261.325); + --color-gray-500: oklch(55.1% 0.027 264.364); + --color-gray-600: oklch(44.6% 0.03 256.802); + --color-gray-700: oklch(37.3% 0.034 259.733); + --color-gray-800: oklch(27.8% 0.033 256.848); + --color-gray-900: oklch(21% 0.034 264.665); + --color-white: #fff; + --spacing: 0.25rem; + --container-sm: 24rem; + --container-md: 28rem; + --container-xl: 36rem; + --container-2xl: 42rem; + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); + --text-5xl: 3rem; + --text-5xl--line-height: 1; + --text-6xl: 3.75rem; + --text-6xl--line-height: 1; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } +} + +@layer base { + + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + + html, + :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: inherit; + font-weight: inherit; + } + + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + + b, + strong { + font-weight: bolder; + } + + code, + kbd, + samp, + pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + + small { + font-size: 80%; + } + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + + :-moz-focusring { + outline: auto; + } + + progress { + vertical-align: baseline; + } + + summary { + display: list-item; + } + + ol, + ul, + menu { + list-style: none; + } + + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; + vertical-align: middle; + } + + img, + video { + max-width: 100%; + height: auto; + } + + button, + input, + select, + optgroup, + textarea, + ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + + ::file-selector-button { + margin-inline-end: 4px; + } + + ::placeholder { + opacity: 1; + } + + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + + textarea { + resize: vertical; + } + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + + ::-webkit-datetime-edit { + display: inline-flex; + } + + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + + ::-webkit-datetime-edit, + ::-webkit-datetime-edit-year-field, + ::-webkit-datetime-edit-month-field, + ::-webkit-datetime-edit-day-field, + ::-webkit-datetime-edit-hour-field, + ::-webkit-datetime-edit-minute-field, + ::-webkit-datetime-edit-second-field, + ::-webkit-datetime-edit-millisecond-field, + ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + + :-moz-ui-invalid { + box-shadow: none; + } + + button, + input:where([type='button'], [type='reset'], [type='submit']), + ::file-selector-button { + appearance: button; + } + + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + + [hidden]:where(:not([hidden='until-found'])) { + display: none !important; + } +} + +@layer utilities { + .visible { + visibility: visible; + } + + .absolute { + position: absolute; + } + + .fixed { + position: fixed; + } + + .relative { + position: relative; + } + + .static { + position: static; + } + + .inset-0 { + inset: calc(var(--spacing) * 0); + } + + .top-0 { + top: calc(var(--spacing) * 0); + } + + .top-1\/2 { + top: calc(1/2 * 100%); + } + + .top-2 { + top: calc(var(--spacing) * 2); + } + + .top-3\.5 { + top: calc(var(--spacing) * 3.5); + } + + .right-0 { + right: calc(var(--spacing) * 0); + } + + .bottom-0 { + bottom: calc(var(--spacing) * 0); + } + + .left-0 { + left: calc(var(--spacing) * 0); + } + + .left-2 { + left: calc(var(--spacing) * 2); + } + + .left-2\.5 { + left: calc(var(--spacing) * 2.5); + } + + .isolate { + isolation: isolate; + } + + .z-50 { + z-index: 50; + } + + .container { + width: 100%; + + @media (width >=40rem) { + max-width: 40rem; + } + + @media (width >=48rem) { + max-width: 48rem; + } + + @media (width >=64rem) { + max-width: 64rem; + } + + @media (width >=80rem) { + max-width: 80rem; + } + + @media (width >=96rem) { + max-width: 96rem; + } + } + + .m-0 { + margin: calc(var(--spacing) * 0); + } + + .m-2 { + margin: calc(var(--spacing) * 2); + } + + .-mx-4 { + margin-inline: calc(var(--spacing) * -4); + } + + .mx-2 { + margin-inline: calc(var(--spacing) * 2); + } + + .mx-4 { + margin-inline: calc(var(--spacing) * 4); + } + + .mx-8 { + margin-inline: calc(var(--spacing) * 8); + } + + .mx-auto { + margin-inline: auto; + } + + .my-1 { + margin-block: calc(var(--spacing) * 1); + } + + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + + .mr-1 { + margin-right: calc(var(--spacing) * 1); + } + + .mr-2 { + margin-right: calc(var(--spacing) * 2); + } + + .mr-3 { + margin-right: calc(var(--spacing) * 3); + } + + .mr-6 { + margin-right: calc(var(--spacing) * 6); + } + + .mr-8 { + margin-right: calc(var(--spacing) * 8); + } + + .mr-10 { + margin-right: calc(var(--spacing) * 10); + } + + .mr-12 { + margin-right: calc(var(--spacing) * 12); + } + + .mr-14 { + margin-right: calc(var(--spacing) * 14); + } + + .mr-16 { + margin-right: calc(var(--spacing) * 16); + } + + .mr-auto { + margin-right: auto; + } + + .mb-0 { + margin-bottom: calc(var(--spacing) * 0); + } + + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + + .mb-6 { + margin-bottom: calc(var(--spacing) * 6); + } + + .mb-8 { + margin-bottom: calc(var(--spacing) * 8); + } + + .mb-10 { + margin-bottom: calc(var(--spacing) * 10); + } + + .mb-12 { + margin-bottom: calc(var(--spacing) * 12); + } + + .mb-14 { + margin-bottom: calc(var(--spacing) * 14); + } + + .mb-16 { + margin-bottom: calc(var(--spacing) * 16); + } + + .mb-24 { + margin-bottom: calc(var(--spacing) * 24); + } + + .ml-8 { + margin-left: calc(var(--spacing) * 8); + } + + .block { + display: block; + } + + .contents { + display: contents; + } + + .flex { + display: flex; + } + + .hidden { + display: none; + } + + .inline-block { + display: inline-block; + } + + .inline-flex { + display: inline-flex; + } + + .table { + display: table; + } + + .h-2 { + height: calc(var(--spacing) * 2); + } + + .h-4 { + height: calc(var(--spacing) * 4); + } + + .h-6 { + height: calc(var(--spacing) * 6); + } + + .h-8 { + height: calc(var(--spacing) * 8); + } + + .h-9 { + height: calc(var(--spacing) * 9); + } + + .h-40 { + height: calc(var(--spacing) * 40); + } + + .h-full { + height: 100%; + } + + .h-px { + height: 1px; + } + + .h-screen { + height: 100vh; + } + + .max-h-72 { + max-height: calc(var(--spacing) * 72); + } + + .w-1\/2 { + width: calc(1/2 * 100%); + } + + .w-1\/4 { + width: calc(1/4 * 100%); + } + + .w-1\/6 { + width: calc(1/6 * 100%); + } + + .w-2 { + width: calc(var(--spacing) * 2); + } + + .w-4 { + width: calc(var(--spacing) * 4); + } + + .w-5\/6 { + width: calc(5/6 * 100%); + } + + .w-6 { + width: calc(var(--spacing) * 6); + } + + .w-8 { + width: calc(var(--spacing) * 8); + } + + .w-12 { + width: calc(var(--spacing) * 12); + } + + .w-16 { + width: calc(var(--spacing) * 16); + } + + .w-30 { + width: calc(var(--spacing) * 30); + } + + .w-full { + width: 100%; + } + + .max-w-2xl { + max-width: var(--container-2xl); + } + + .max-w-md { + max-width: var(--container-md); + } + + .max-w-sm { + max-width: var(--container-sm); + } + + .max-w-xl { + max-width: var(--container-xl); + } + + .min-w-0 { + min-width: calc(var(--spacing) * 0); + } + + .shrink-0 { + flex-shrink: 0; + } + + .table-auto { + table-layout: auto; + } + + .translate-1\/2 { + --tw-translate-x: calc(1/2 * 100%); + --tw-translate-y: calc(1/2 * 100%); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .transform { + transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, ); + } + + .cursor-pointer { + cursor: pointer; + } + + .flex-col { + flex-direction: column; + } + + .flex-row { + flex-direction: row; + } + + .flex-wrap { + flex-wrap: wrap; + } + + .place-items-center { + place-items: center; + } + + .items-center { + align-items: center; + } + + .items-start { + align-items: flex-start; + } + + .justify-between { + justify-content: space-between; + } + + .justify-center { + justify-content: center; + } + + .self-center { + align-self: center; + } + + .overflow-auto { + overflow: auto; + } + + .overflow-hidden { + overflow: hidden; + } + + .overflow-x-auto { + overflow-x: auto; + } + + .overflow-y-auto { + overflow-y: auto; + } + + .rounded { + border-radius: 0.25rem; + } + + .rounded-full { + border-radius: calc(infinity * 1px); + } + + .rounded-lg { + border-radius: var(--radius-lg); + } + + .rounded-md { + border-radius: var(--radius-md); + } + + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + + .border-0 { + border-style: var(--tw-border-style); + border-width: 0px; + } + + .border-r { + border-right-style: var(--tw-border-style); + border-right-width: 1px; + } + + .border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + + .border-b-2 { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 2px; + } + + .border-l { + border-left-style: var(--tw-border-style); + border-left-width: 1px; + } + + .border-gray-100 { + border-color: var(--color-gray-100); + } + + .border-gray-200 { + border-color: var(--color-gray-200); + } + + .border-transparent { + border-color: transparent; + } + + .bg-gray-50 { + background-color: var(--color-gray-50); + } + + .bg-gray-100 { + background-color: var(--color-gray-100); + } + + .bg-gray-200 { + background-color: var(--color-gray-200); + } + + .bg-gray-800 { + background-color: var(--color-gray-800); + } + + .bg-gray-900 { + background-color: var(--color-gray-900); + } + + .bg-green-500 { + background-color: var(--color-green-500); + } + + .bg-indigo-500 { + background-color: var(--color-indigo-500); + } + + .bg-orange-300 { + background-color: var(--color-orange-300); + } + + .bg-purple-50 { + background-color: var(--color-purple-50); + } + + .bg-white { + background-color: var(--color-white); + } + + .stroke-current { + stroke: currentcolor; + } + + .object-cover { + object-fit: cover; + } + + .object-scale-down { + object-fit: scale-down; + } + + .p-2 { + padding: calc(var(--spacing) * 2); + } + + .p-4 { + padding: calc(var(--spacing) * 4); + } + + .p-10 { + padding: calc(var(--spacing) * 10); + } + + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + + .px-6 { + padding-inline: calc(var(--spacing) * 6); + } + + .px-8 { + padding-inline: calc(var(--spacing) * 8); + } + + .px-10 { + padding-inline: calc(var(--spacing) * 10); + } + + .px-12 { + padding-inline: calc(var(--spacing) * 12); + } + + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + + .py-3 { + padding-block: calc(var(--spacing) * 3); + } + + .py-4 { + padding-block: calc(var(--spacing) * 4); + } + + .py-5 { + padding-block: calc(var(--spacing) * 5); + } + + .py-6 { + padding-block: calc(var(--spacing) * 6); + } + + .py-8 { + padding-block: calc(var(--spacing) * 8); + } + + .py-20 { + padding-block: calc(var(--spacing) * 20); + } + + .pt-4 { + padding-top: calc(var(--spacing) * 4); + } + + .pr-2 { + padding-right: calc(var(--spacing) * 2); + } + + .pr-10 { + padding-right: calc(var(--spacing) * 10); + } + + .pb-3 { + padding-bottom: calc(var(--spacing) * 3); + } + + .pb-10 { + padding-bottom: calc(var(--spacing) * 10); + } + + .pl-4 { + padding-left: calc(var(--spacing) * 4); + } + + .pl-6 { + padding-left: calc(var(--spacing) * 6); + } + + .pl-8 { + padding-left: calc(var(--spacing) * 8); + } + + .text-center { + text-align: center; + } + + .text-left { + text-align: left; + } + + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + + .text-3xl { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } + + .text-5xl { + font-size: var(--text-5xl); + line-height: var(--tw-leading, var(--text-5xl--line-height)); + } + + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + + .break-words { + overflow-wrap: break-word; + } + + .text-ellipsis { + text-overflow: ellipsis; + } + + .text-blue-300 { + color: var(--color-blue-300); + } + + .text-gray-400 { + color: var(--color-gray-400); + } + + .text-gray-500 { + color: var(--color-gray-500); + } + + .text-gray-600 { + color: var(--color-gray-600); + } + + .text-purple-500 { + color: var(--color-purple-500); + } + + .text-white { + color: var(--color-white); + } + + .uppercase { + text-transform: uppercase; + } + + .placeholder-gray-400 { + &::placeholder { + color: var(--color-gray-400); + } + } + + .opacity-25 { + opacity: 25%; + } + + .shadow { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .shadow-2xl { + --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .shadow-lg { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .shadow-sm { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .ring-1 { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .filter { + filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); + } + + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .duration-200 { + --tw-duration: 200ms; + transition-duration: 200ms; + } + + .hover\:bg-indigo-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-indigo-600); + } + } + } + + .hover\:bg-orange-400 { + &:hover { + @media (hover: hover) { + background-color: var(--color-orange-400); + } + } + } + + .hover\:bg-yellow-50 { + &:hover { + @media (hover: hover) { + background-color: var(--color-yellow-50); + } + } + } + + .hover\:text-gray-600 { + &:hover { + @media (hover: hover) { + color: var(--color-gray-600); + } + } + } + + .hover\:text-gray-700 { + &:hover { + @media (hover: hover) { + color: var(--color-gray-700); + } + } + } + + .hover\:text-gray-900 { + &:hover { + @media (hover: hover) { + color: var(--color-gray-900); + } + } + } + + .hover\:shadow-2xl { + &:hover { + @media (hover: hover) { + --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } + + .hover\:ring-4 { + &:hover { + @media (hover: hover) { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } + + .focus\:border-blue-300 { + &:focus { + border-color: var(--color-blue-300); + } + } + + .focus\:border-transparent { + &:focus { + border-color: transparent; + } + } + + .focus\:bg-white { + &:focus { + background-color: var(--color-white); + } + } + + .focus\:ring-2 { + &:focus { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + + .focus\:ring-blue-300 { + &:focus { + --tw-ring-color: var(--color-blue-300); + } + } + + .focus\:ring-transparent { + &:focus { + --tw-ring-color: transparent; + } + } + + .focus\:ring-yellow-600 { + &:focus { + --tw-ring-color: var(--color-yellow-600); + } + } + + .focus\:outline-hidden { + &:focus { + --tw-outline-style: none; + outline-style: none; + + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + } + + .focus\:outline-none { + &:focus { + --tw-outline-style: none; + outline-style: none; + } + } + + .md\:mb-0 { + @media (width >=48rem) { + margin-bottom: calc(var(--spacing) * 0); + } + } + + .md\:w-1\/2 { + @media (width >=48rem) { + width: calc(1/2 * 100%); + } + } + + .md\:w-auto { + @media (width >=48rem) { + width: auto; + } + } + + .md\:text-right { + @media (width >=48rem) { + text-align: right; + } + } + + .md\:text-6xl { + @media (width >=48rem) { + font-size: var(--text-6xl); + line-height: var(--tw-leading, var(--text-6xl--line-height)); + } + } + + .lg\:pl-20 { + @media (width >=64rem) { + padding-left: calc(var(--spacing) * 20); + } + } + + .xl\:mx-auto { + @media (width >=80rem) { + margin-inline: auto; + } + } + + .xl\:mb-0 { + @media (width >=80rem) { + margin-bottom: calc(var(--spacing) * 0); + } + } + + .xl\:block { + @media (width >=80rem) { + display: block; + } + } + + .xl\:flex { + @media (width >=80rem) { + display: flex; + } + } + + .xl\:hidden { + @media (width >=80rem) { + display: none; + } + } + + .xl\:inline-block { + @media (width >=80rem) { + display: inline-block; + } + } + + .xl\:w-2\/3 { + @media (width >=80rem) { + width: calc(2/3 * 100%); + } + } + + .dark\:bg-gray-600 { + @media (prefers-color-scheme: dark) { + background-color: var(--color-gray-600); + } + } +} + +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} + +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} + +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} + +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} + +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} + +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} + +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} + +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} + +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} + +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} + +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} + +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} + +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} + +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} + +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} + +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} + +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} + +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} + +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} + +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} + +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} + +@property --tw-blur { + syntax: "*"; + inherits: false; +} + +@property --tw-brightness { + syntax: "*"; + inherits: false; +} + +@property --tw-contrast { + syntax: "*"; + inherits: false; +} + +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} + +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} + +@property --tw-invert { + syntax: "*"; + inherits: false; +} + +@property --tw-opacity { + syntax: "*"; + inherits: false; +} + +@property --tw-saturate { + syntax: "*"; + inherits: false; +} + +@property --tw-sepia { + syntax: "*"; + inherits: false; +} + +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} + +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} + +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} + +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} + +@property --tw-duration { + syntax: "*"; + inherits: false; +} + +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + + *, + ::before, + ::after, + ::backdrop { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; + --tw-border-style: solid; + --tw-font-weight: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-duration: initial; + } + } +} diff --git a/packages/fullstack-core/src/loader.rs b/packages/fullstack-core/src/loader.rs index e8e802934..dccb80878 100644 --- a/packages/fullstack-core/src/loader.rs +++ b/packages/fullstack-core/src/loader.rs @@ -1,17 +1,14 @@ -use dioxus_core::{ - spawn, use_hook, Callback, IntoAttributeValue, IntoDynNode, ReactiveContext, Subscribers, Task, -}; +use dioxus_core::{use_hook, IntoAttributeValue, IntoDynNode, Subscribers}; use dioxus_core::{CapturedError, RenderError, Result, SuspendedFuture}; -use dioxus_hooks::{use_callback, use_signal}; +use dioxus_hooks::{use_resource, use_signal, Resource}; use dioxus_signals::{ read_impls, ReadSignal, Readable, ReadableBoxExt, ReadableExt, ReadableRef, Signal, Writable, WritableExt, WriteLock, }; -use futures_util::{future, pin_mut, FutureExt, StreamExt}; use generational_box::{BorrowResult, UnsyncStorage}; use serde::{de::DeserializeOwned, Serialize}; -use std::future::Future; -use std::{cell::Cell, ops::Deref, rc::Rc}; +use std::ops::Deref; +use std::{cmp::PartialEq, future::Future}; /// A hook to create a resource that loads data asynchronously. /// @@ -35,105 +32,118 @@ use std::{cell::Cell, ops::Deref, rc::Rc}; pub fn use_loader(mut future: impl FnMut() -> F + 'static) -> Result, Loading> where F: Future> + 'static, - T: 'static + std::cmp::PartialEq + Serialize + DeserializeOwned, + T: 'static + PartialEq + Serialize + DeserializeOwned, E: Into + 'static, { - let location = std::panic::Location::caller(); + let serialize_context = use_hook(crate::transport::serialize_context); - // todo: wire up serialize context into loader - // let serialize_context = use_hook(crate::transport::serialize_context); - // // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client - // let storage_entry: SerializeContextEntry = use_hook(|| serialize_context.create_entry()); - // use crate::{transport::SerializeContextEntry, use_server_future, use_server_future_unsuspended}; + // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client + #[allow(unused)] + let storage_entry: crate::transport::SerializeContextEntry> = + use_hook(|| serialize_context.create_entry()); - let mut err = use_signal(|| None as Option); + #[cfg(feature = "server")] + let caller = std::panic::Location::caller(); + + // If this is the first run and we are on the web client, the data might be cached + #[cfg(feature = "web")] + let initial_web_result = + use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get())))); + + let mut error = use_signal(|| None as Option); let mut value = use_signal(|| None as Option); - let mut state = use_signal(|| LoaderState::Pending); - let (rc, changed) = use_hook(|| { - let (rc, changed) = ReactiveContext::new_with_origin(location); - (rc, Rc::new(Cell::new(Some(changed)))) - }); + let mut loader_state = use_signal(|| LoaderState::Pending); - let callback = use_callback(move |_| { - // Set the state to Pending when the task is restarted - state.set(LoaderState::Pending); + let resource = use_resource(move || { + #[cfg(feature = "server")] + let storage_entry = storage_entry.clone(); - // Create the user's task - let fut = rc.reset_and_run_in(&mut future); + let user_fut = future(); - // Spawn a wrapper task that polls the inner future and watches its dependencies - spawn(async move { - // Move the future here and pin it so we can poll it - let fut = fut; - pin_mut!(fut); + #[cfg(feature = "web")] + let initial_web_result = initial_web_result.clone(); - // Run each poll in the context of the reactive scope - // This ensures the scope is properly subscribed to the future's dependencies - let res = future::poll_fn(|cx| { - rc.run_in(|| { - tracing::trace_span!("polling resource", location = %location) - .in_scope(|| fut.poll_unpin(cx)) - }) - }) - .await; + #[allow(clippy::let_and_return)] + async move { + // If this is the first run and we are on the web client, the data might be cached + #[cfg(feature = "web")] + match initial_web_result.take() { + // The data was deserialized successfully from the server + Some(Ok(o)) => { + match o { + Ok(v) => { + value.set(Some(v)); + loader_state.set(LoaderState::Ready); + } + Err(e) => { + error.set(Some(e)); + loader_state.set(LoaderState::Failed); + } + }; + return; + } - // Map the error to the captured error type so it's cheap to clone and pass out - // We go through the anyhow::Error first since it's more useful. - let res: Result = res.map_err(|e| { - let res: dioxus_core::Error = e.into(); - res.into() + // The data is still pending from the server. Don't try to resolve it on the client + Some(Err(crate::transport::TakeDataError::DataPending)) => { + std::future::pending::<()>().await + } + + // The data was not available on the server, rerun the future + Some(Err(_)) => {} + + // This isn't the first run, so we don't need do anything + None => {} + } + + // Otherwise just run the future itself + let out = user_fut.await; + + // Remap the error to the captured error type so it's cheap to clone and pass out, just + // slightly more cumbersome to access the inner error. + let out = out.map_err(|e| { + let anyhow_err: anyhow::Error = e.into(); + anyhow_err.into() }); - match res { + // If this is the first run and we are on the server, cache the data in the slot we reserved for it + #[cfg(feature = "server")] + storage_entry.insert(&out, caller); + + match out { Ok(v) => { - err.set(None); value.set(Some(v)); - state.set(LoaderState::Ready); + loader_state.set(LoaderState::Ready); } Err(e) => { - err.set(Some(e)); - state.set(LoaderState::Failed); + error.set(Some(e)); + loader_state.set(LoaderState::Failed); } - } - }) + }; + } }); - let mut task = use_hook(|| Signal::new(callback(()))); - + // On the first run, force this task to be polled right away in case its value is ready use_hook(|| { - let mut changed = changed.take().unwrap(); - spawn(async move { - loop { - // Wait for the dependencies to change - let _ = changed.next().await; - - // Stop the old task - task.write().cancel(); - - // Start a new task - task.set(callback(())); - } - }) + let _ = resource.task().poll_now(); }); let read_value = use_hook(|| value.map(|f| f.as_ref().unwrap()).boxed()); let handle = LoaderHandle { - task, - err, - callback, - state, + resource, + error, + state: loader_state, + _marker: std::marker::PhantomData, }; - match &*state.read_unchecked() { + match &*loader_state.read_unchecked() { LoaderState::Pending => Err(Loading::Pending(handle)), LoaderState::Failed => Err(Loading::Failed(handle)), LoaderState::Ready => Ok(Loader { real_value: value, read_value, - error: err, - state, - task, + error, + state: loader_state, handle, }), } @@ -153,7 +163,6 @@ pub struct Loader { real_value: Signal>, error: Signal>, state: Signal, - task: Signal, handle: LoaderHandle, } @@ -178,8 +187,12 @@ impl Loader { } /// Cancel the current loading task. - pub fn cancel(&self) { - self.task.read().cancel(); + pub fn cancel(&mut self) { + self.handle.resource.cancel(); + } + + pub fn loading(&self) -> bool { + !self.handle.resource.finished() } } @@ -294,25 +307,27 @@ pub enum LoaderState { } #[derive(PartialEq)] -pub struct LoaderHandle { - callback: Callback<(), Task>, - task: Signal, - err: Signal>, +pub struct LoaderHandle { + resource: Resource<()>, + error: Signal>, state: Signal, + _marker: std::marker::PhantomData, } impl LoaderHandle { /// Restart the loading task. pub fn restart(&mut self) { - self.task.write().cancel(); - let new_task = self.callback.call(()); - self.task.set(new_task); + self.resource.restart(); } /// Get the current state of the loader. pub fn state(&self) -> LoaderState { *self.state.read() } + + pub fn error(&self) -> Option { + self.error.read().as_ref().cloned() + } } impl Clone for LoaderHandle { @@ -354,9 +369,9 @@ impl std::fmt::Display for Loading { impl From for RenderError { fn from(val: Loading) -> Self { match val { - Loading::Pending(t) => RenderError::Suspended(SuspendedFuture::new(t.task.cloned())), + Loading::Pending(t) => RenderError::Suspended(SuspendedFuture::new(t.resource.task())), Loading::Failed(err) => RenderError::Error( - err.err + err.error .cloned() .expect("LoaderHandle in Failed state should always have an error"), ), diff --git a/packages/fullstack-server/README.md b/packages/fullstack-server/README.md index 0bd366f76..4ff42efa3 100644 --- a/packages/fullstack-server/README.md +++ b/packages/fullstack-server/README.md @@ -89,7 +89,7 @@ async fn main() { let router = axum::Router::new() // You can add a dioxus application to the router with the `serve_dioxus_application` method // This will add a fallback route to the router that will serve your component and server functions - .serve_dioxus_application(dioxus_server::ServeConfig::new().unwrap(), App); + .serve_dioxus_application(dioxus_server::ServeConfig::new(), App); // Finally, we can launch the server let router = router.into_make_service(); diff --git a/packages/fullstack-server/docs/request_origin.md b/packages/fullstack-server/docs/request_origin.md deleted file mode 100644 index 07208b904..000000000 --- a/packages/fullstack-server/docs/request_origin.md +++ /dev/null @@ -1,48 +0,0 @@ -This method interacts with information from the current request. The request may come from: - -1. The initial SSR render if this method called from a [`Component`](dioxus_core_macro::component) or a [`server`](dioxus_server) function that is called during the initial render - -```rust -# use dioxus::prelude::*; -#[component] -fn PrintHtmlRequestInfo() -> Element { - // The server context only exists on the server, so we need to put it behind a server_only! config - server_only! { - // Since we are calling this from a component, the server context that is returned will be from - // the html request for ssr rendering - let context = server_context(); - let request_parts = context.request_parts(); - println!("headers are {:?}", request_parts.headers); - } - rsx! {} -} -``` - -2. A request to a [`server`](dioxus_server) function called directly from the client (either on desktop/mobile or on the web frontend after the initial render) - -```rust -# use dioxus::prelude::*; -#[server] -async fn read_headers() -> ServerFnResult { - // Since we are calling this from a server function, the server context that is may be from the - // initial request or a request from the client - let context = server_context(); - let request_parts = context.request_parts(); - println!("headers are {:?}", request_parts.headers); - Ok(()) -} - -#[component] -fn CallServerFunction() -> Element { - rsx! { - button { - // If you click the button, the server function will be called and the server context will be - // from the client request - onclick: move |_| async { - _ = read_headers().await - }, - "Call server function" - } - } -} -``` diff --git a/packages/fullstack-server/src/config.rs b/packages/fullstack-server/src/config.rs index 0dc46d845..d27e89c6c 100644 --- a/packages/fullstack-server/src/config.rs +++ b/packages/fullstack-server/src/config.rs @@ -3,37 +3,51 @@ use dioxus_core::LaunchConfig; use std::any::Any; -use std::fs::File; -use std::io::Read; -use std::path::PathBuf; use std::sync::Arc; -use crate::IncrementalRendererConfig; +use crate::{IncrementalRendererConfig, IndexHtml}; #[allow(unused)] pub(crate) type ContextProviders = Arc Box + Send + Sync + 'static>>>; /// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus_ssr`]. #[derive(Clone, Default)] -pub struct ServeConfigBuilder { +pub struct ServeConfig { pub(crate) root_id: Option<&'static str>, - pub(crate) index_html: Option, - pub(crate) index_path: Option, + pub(crate) index: IndexHtml, + pub(crate) index_html_override: Option, pub(crate) incremental: Option, pub(crate) context_providers: Vec Box + Send + Sync + 'static>>, pub(crate) streaming_mode: StreamingMode, } -impl LaunchConfig for ServeConfigBuilder {} +/// The streaming mode to use while rendering the page +#[derive(Clone, Copy, Default, PartialEq)] +pub enum StreamingMode { + /// Streaming is disabled; all server futures should be resolved before hydrating the page on the client + #[default] + Disabled, -impl ServeConfigBuilder { - /// Create a new ServeConfigBuilder with incremental static generation disabled and the default index.html settings + /// Out of order streaming is enabled; server futures are resolved out of order and streamed to the client + /// as they resolve + OutOfOrder, +} + +impl LaunchConfig for ServeConfig {} + +impl ServeConfig { + /// Create a new ServeConfig with incremental static generation disabled and the default index.html settings. + pub fn builder() -> Self { + Self::new() + } + + /// Create a new ServeConfig with incremental static generation disabled and the default index.html settings pub fn new() -> Self { Self { root_id: None, - index_html: None, - index_path: None, incremental: None, + index: IndexHtml::default(), + index_html_override: None, context_providers: Default::default(), streaming_mode: StreamingMode::default(), } @@ -49,7 +63,7 @@ impl ServeConfigBuilder { /// // Finally, launch the app with the config /// LaunchBuilder::new() /// // Only set the server config if the server feature is enabled - /// .with_cfg(server_only!(dioxus_server::ServeConfigBuilder::default().incremental(dioxus_server::IncrementalRendererConfig::default()))) + /// .with_cfg(server_only!(dioxus_server::ServeConfig::default().incremental(dioxus_server::IncrementalRendererConfig::default()))) /// .launch(app); /// ``` pub fn incremental(mut self, cfg: IncrementalRendererConfig) -> Self { @@ -57,15 +71,9 @@ impl ServeConfigBuilder { self } - /// Set the contents of the index.html file to be served. (precedence over index_path) + /// Set the contents of the index.html file to be served. pub fn index_html(mut self, index_html: String) -> Self { - self.index_html = Some(index_html); - self - } - - /// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html) - pub fn index_path(mut self, index_path: PathBuf) -> Self { - self.index_path = Some(index_path); + self.index_html_override = Some(index_html); self } @@ -96,7 +104,7 @@ impl ServeConfigBuilder { /// LaunchBuilder::new() /// // Only set the server config if the server feature is enabled /// .with_cfg(server_only! { - /// dioxus_server::ServeConfigBuilder::default().root_id("app") + /// dioxus_server::ServeConfig::default().root_id("app") /// }) /// // You also need to set the root id in your web config /// .with_cfg(web! { @@ -134,12 +142,11 @@ impl ServeConfigBuilder { /// /// #[cfg(feature = "server")] /// dioxus_server::serve(|| async move { - /// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt}; + /// use dioxus_server::{axum, ServeConfig, DioxusRouterExt}; /// - /// let config = ServeConfigBuilder::default() + /// let config = ServeConfig::default() /// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder - /// .context_providers(Arc::new(vec![Box::new(|| Box::new(1234u32) as Box) as Box Box + Send + Sync>])) - /// .build()?; + /// .context_providers(Arc::new(vec![Box::new(|| Box::new(1234u32) as Box) as Box Box + Send + Sync>])); /// /// Ok( /// axum::Router::new() @@ -192,12 +199,11 @@ impl ServeConfigBuilder { /// /// #[cfg(feature = "server")] /// dioxus_server::serve(|| async move { - /// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt}; + /// use dioxus_server::{axum, ServeConfig, DioxusRouterExt}; /// - /// let config = ServeConfigBuilder::default() + /// let config = ServeConfig::default() /// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder - /// .context_provider(|| 1234u32) - /// .build()?; + /// .context_provider(|| 1234u32); /// /// Ok( /// axum::Router::new() @@ -243,12 +249,12 @@ impl ServeConfigBuilder { /// // Run a custom server with axum on the server /// #[cfg(feature = "server")] /// dioxus_server::serve(|| async move { - /// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt}; + /// use dioxus_server::{axum, ServeConfig, DioxusRouterExt}; + /// + /// let config = ServeConfig::default() + /// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder + /// .context(1234u32); /// - /// let config = ServeConfigBuilder::default() - /// .context(1234u32) - /// .build()?; - // /// Ok( /// axum::Router::new() /// .serve_dioxus_application(config, app) @@ -307,188 +313,4 @@ impl ServeConfigBuilder { self.streaming_mode = StreamingMode::OutOfOrder; self } - - /// Build the ServeConfig. This may fail if the index.html file is not found. - /// - /// ## WASM compatibility - /// In the context of WASM the file system normally can't be read so this always requires - /// the `index_html` field to be set. - pub fn build(self) -> Result { - let root_id = self.root_id.unwrap_or("main"); - - let index_html = match self.index_html { - Some(index) => index, - None => { - // The CLI always bundles static assets into the exe/public directory - let public_path = public_path(); - - let index_path = self - .index_path - .unwrap_or_else(|| public_path.join("index.html")); - load_index_path(index_path).unwrap_or_else(|_| { - r#" - - - - -
- - - "# - .to_string() - }) - } - }; - - let index = load_index_html(index_html, root_id); - let context_providers = Arc::new( - self.context_providers - .into_iter() - .map(|f| { - Box::new(move || f()) as Box Box + Send + Sync> - }) - .collect(), - ); - - Ok(ServeConfig { - index, - incremental: self.incremental, - context_providers, - streaming_mode: self.streaming_mode, - }) - } -} - -impl TryInto for ServeConfigBuilder { - type Error = UnableToLoadIndex; - - fn try_into(self) -> Result { - self.build() - } -} - -/// Get the path to the public assets directory to serve static files from -pub(crate) fn public_path() -> PathBuf { - if let Ok(path) = std::env::var("DIOXUS_PUBLIC_PATH") { - return PathBuf::from(path); - } - - // The CLI always bundles static assets into the exe/public directory - std::env::current_exe() - .expect("Failed to get current executable path") - .parent() - .unwrap() - .join("public") -} - -/// An error that can occur when loading the index.html file -#[derive(Debug, Clone)] -pub struct UnableToLoadIndex(PathBuf); - -impl std::fmt::Display for UnableToLoadIndex { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built. Tried to open file at path: {:?}", self.0) - } -} - -impl std::error::Error for UnableToLoadIndex {} - -fn load_index_path(path: PathBuf) -> Result { - let mut file = File::open(&path).map_err(|_| UnableToLoadIndex(path))?; - - let mut contents = String::new(); - file.read_to_string(&mut contents) - .expect("Failed to read index.html"); - Ok(contents) -} - -fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml { - let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\"")).unwrap_or_else(|| panic!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page.")); - - let post_main = post_main.split_once('>').unwrap_or_else(|| { - panic!("Failed to find closing > after id=\"{root_id}\" in index.html.") - }); - - let (pre_main, post_main) = ( - pre_main.to_string() + &format!("id=\"{root_id}\"") + post_main.0 + ">", - post_main.1.to_string(), - ); - - let (head, close_head) = pre_main.split_once("").unwrap_or_else(|| { - panic!("Failed to find closing tag after id=\"{root_id}\" in index.html.") - }); - let (head, close_head) = (head.to_string(), "".to_string() + close_head); - - let (post_main, after_closing_body_tag) = - post_main.split_once("").unwrap_or_else(|| { - panic!("Failed to find closing tag after id=\"{root_id}\" in index.html.") - }); - - // Strip out the head if it exists - let mut head_before_title = String::new(); - let mut head_after_title = head; - let mut title = String::new(); - if let Some((new_head_before_title, new_title)) = head_after_title.split_once("") { - let (new_title, new_head_after_title) = new_title - .split_once("") - .expect("Failed to find closing tag after in index.html."); - title = format!("<title>{new_title}"); - head_before_title = new_head_before_title.to_string(); - head_after_title = new_head_after_title.to_string(); - } - - IndexHtml { - head_before_title, - head_after_title, - title, - close_head, - post_main: post_main.to_string(), - after_closing_body_tag: "".to_string() + after_closing_body_tag, - } -} - -#[derive(Clone, Debug)] -pub(crate) struct IndexHtml { - pub(crate) head_before_title: String, - pub(crate) head_after_title: String, - pub(crate) title: String, - pub(crate) close_head: String, - pub(crate) post_main: String, - pub(crate) after_closing_body_tag: String, -} - -/// The streaming mode to use while rendering the page -#[derive(Clone, Copy, Default, PartialEq)] -pub enum StreamingMode { - /// Streaming is disabled; all server futures should be resolved before hydrating the page on the client - #[default] - Disabled, - /// Out of order streaming is enabled; server futures are resolved out of order and streamed to the client - /// as they resolve - OutOfOrder, -} - -/// Used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus_ssr`]. -/// See [`ServeConfigBuilder`] to create a ServeConfig -#[derive(Clone)] -pub struct ServeConfig { - pub(crate) index: IndexHtml, - pub(crate) incremental: Option, - pub(crate) context_providers: ContextProviders, - pub(crate) streaming_mode: StreamingMode, -} - -impl LaunchConfig for ServeConfig {} - -impl ServeConfig { - /// Create a new ServeConfig - #[cfg(not(target_family = "wasm"))] - pub fn new() -> Result { - ServeConfigBuilder::new().build() - } - - /// Create a new builder for a ServeConfig - pub fn builder() -> ServeConfigBuilder { - ServeConfigBuilder::new() - } } diff --git a/packages/fullstack-server/src/index_html.rs b/packages/fullstack-server/src/index_html.rs new file mode 100644 index 000000000..0e867d5e4 --- /dev/null +++ b/packages/fullstack-server/src/index_html.rs @@ -0,0 +1,83 @@ +use anyhow::Context; + +/// An `IndexHtml` represents the contents of an `index.html` file used to serve a web application. +/// +/// This defines the static portion of your web application, typically generated by a tool like `dx` +/// in conjunction with wasm-bindgen. +#[derive(Clone, Debug)] +pub(crate) struct IndexHtml { + pub(crate) head_before_title: String, + pub(crate) head_after_title: String, + pub(crate) title: String, + pub(crate) close_head: String, + pub(crate) post_main: String, + pub(crate) after_closing_body_tag: String, +} + +impl IndexHtml { + /// Create a new `IndexHtml` from the raw contents of an `index.html` file. + /// + /// This function will parse the `index.html` and split it into sections for easier manipulation. + /// The `root_id` parameter specifies the id of the main application container (e.g., "main") + /// which your app will render into. + pub(crate) fn new(contents: &str, root_id: &'static str) -> Result { + let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\"")) + .with_context(|| format!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page."))?; + + let post_main = post_main.split_once('>').with_context(|| { + format!("Failed to find closing > after id=\"{root_id}\" in index.html.") + })?; + + let (pre_main, post_main) = ( + pre_main.to_string() + &format!("id=\"{root_id}\"") + post_main.0 + ">", + post_main.1.to_string(), + ); + + let (head, close_head) = pre_main.split_once("").with_context(|| { + format!("Failed to find closing tag after id=\"{root_id}\" in index.html.") + })?; + + let (head, close_head) = (head.to_string(), "".to_string() + close_head); + + let (post_main, after_closing_body_tag) = + post_main.split_once("").with_context(|| { + format!("Failed to find closing tag after id=\"{root_id}\" in index.html.") + })?; + + // Strip out the head if it exists + let mut head_before_title = String::new(); + let mut head_after_title = head; + let mut title = String::new(); + if let Some((new_head_before_title, new_title)) = head_after_title.split_once("") { + let (new_title, new_head_after_title) = new_title + .split_once("") + .context("Failed to find closing tag after in index.html.")?; + title = format!("<title>{new_title}"); + head_before_title = new_head_before_title.to_string(); + head_after_title = new_head_after_title.to_string(); + } + + Ok(IndexHtml { + head_before_title, + head_after_title, + title, + close_head, + post_main: post_main.to_string(), + after_closing_body_tag: "".to_string() + after_closing_body_tag, + }) + } +} + +impl Default for IndexHtml { + fn default() -> Self { + const DEFAULT: &str = r#" + + + +
+ + "#; + + Self::new(DEFAULT, "main").expect("Failed to load default index.html") + } +} diff --git a/packages/fullstack-server/src/launch.rs b/packages/fullstack-server/src/launch.rs index c4e97a797..5a263f7b7 100644 --- a/packages/fullstack-server/src/launch.rs +++ b/packages/fullstack-server/src/launch.rs @@ -1,11 +1,13 @@ //! A launch function that creates an axum router for the LaunchBuilder -use crate::{server::DioxusRouterExt, RenderHandleState, ServeConfig, ServeConfigBuilder}; +use crate::IndexHtml; +use crate::{server::DioxusRouterExt, RenderHandleState, ServeConfig}; +use anyhow::Context; use axum::{ body::Body, extract::{Request, State}, - response::IntoResponse, routing::IntoMakeService, + Router, }; use dioxus_cli_config::base_path; use dioxus_core::Element; @@ -19,11 +21,11 @@ use hyper_util::{ rt::{TokioExecutor, TokioIo}, service::TowerToHyperService, }; +use std::sync::Arc; use std::{any::Any, collections::HashMap, net::SocketAddr, prelude::rust_2024::Future}; use tokio::net::TcpStream; use tokio_util::task::LocalPoolHandle; -use tower::Service; -use tower::ServiceExt as _; +use tower::{Service, ServiceExt as _}; type ContextList = Vec Box + Send + Sync>>; @@ -55,34 +57,32 @@ async fn serve_server( dioxus_devtools::connect(move |msg| _ = devtools_tx.unbounded_send(msg)); - let platform_config = platform_config + let mut cfg = platform_config .into_iter() - .find_map(|cfg| { - cfg.downcast::() - .map(|cfg| Result::Ok(*cfg)) - .or_else(|cfg| { - cfg.downcast::() - .map(|builder| builder.build()) - }) - .ok() - }) + .find_map(|cfg| cfg.downcast::().ok().map(|b| *b)) .unwrap_or_else(ServeConfig::new); // Extend the config's context providers with the context providers from the launch builder - let cfg = platform_config - .map(|mut cfg| { - let mut contexts = contexts; - let cfg_context_providers = cfg.context_providers.clone(); - for i in 0..cfg_context_providers.len() { - contexts.push(Box::new({ - let cfg_context_providers = cfg_context_providers.clone(); - move || (cfg_context_providers[i])() - })); - } - cfg.context_providers = std::sync::Arc::new(contexts); - cfg - }) - .unwrap(); + for ctx in contexts { + let arced = Arc::new(ctx) as Arc Box + Send + Sync>; + cfg.context_providers.push(arced); + } + + // Create the IndexHtml object if the user has specified an override + if let Some(raw_index_html) = cfg.index_html_override.as_ref() { + cfg.index = IndexHtml::new(raw_index_html, cfg.root_id.unwrap_or("main")) + .expect("Failed to parse custom index.html"); + } else if !cfg!(target_arch = "wasm32") { + let public_path = crate::public_path(); + let index_html_path = public_path.join("index.html"); + + if index_html_path.exists() { + let index_html = std::fs::read_to_string(index_html_path) + .expect("Failed to read index.html from public directory"); + cfg.index = IndexHtml::new(&index_html, cfg.root_id.unwrap_or("main")) + .expect("Failed to parse index.html from public directory"); + } + } // Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address // and we use the generated address the CLI gives us @@ -90,7 +90,7 @@ async fn serve_server( // Create the router and register the server functions under the basepath. let router = apply_base_path( - axum::Router::new().serve_dioxus_application(cfg.clone(), original_root), + Router::new().serve_dioxus_application(cfg.clone(), original_root), original_root, cfg.clone(), base_path().map(|s| s.to_string()), @@ -101,6 +101,7 @@ async fn serve_server( .map(usize::from) .unwrap_or(1), ); + let mut make_service = router.into_make_service(); let listener = tokio::net::TcpListener::bind(address).await.unwrap(); @@ -136,7 +137,7 @@ async fn serve_server( let tcp_stream = TokioIo::new(tcp_stream); std::future::poll_fn(|cx| { - as tower::Service>::poll_ready( + as tower::Service>::poll_ready( &mut make_service, cx, ) @@ -189,8 +190,8 @@ async fn serve_server( unsafe { dioxus_devtools::subsecond::apply_patch(table).unwrap() }; - let mut new_router = axum::Router::new().serve_static_assets(); - let new_cfg = ServeConfig::new().unwrap(); + let mut new_router = Router::new().serve_static_assets(); + let new_cfg = ServeConfig::new(); let server_fn_iter = ServerFunction::collect(); @@ -250,58 +251,69 @@ async fn serve_server( } fn apply_base_path( - mut router: axum::Router, - root: fn() -> Result, + mut router: Router, + root: fn() -> Element, cfg: ServeConfig, base_path: Option, -) -> axum::Router { +) -> Router { if let Some(base_path) = base_path { let base_path = base_path.trim_matches('/'); + // If there is a base path, nest the router under it and serve the root route manually // Nesting a route in axum only serves /base_path or /base_path/ not both - router = axum::Router::new().nest(&format!("/{base_path}/"), router); - - async fn root_render_handler( - state: State, - mut request: Request, - ) -> impl IntoResponse { - // The root of the base path always looks like the root from dioxus fullstack - *request.uri_mut() = "/".parse().unwrap(); - RenderHandleState::render_handler(state, request).await - } - - router = router.route( + router = Router::new().nest(&format!("/{base_path}/"), router).route( &format!("/{base_path}"), - axum::routing::method_routing::get(root_render_handler) - .with_state(RenderHandleState::new(cfg, root)), + axum::routing::method_routing::get( + |state: State, mut request: Request| async move { + // The root of the base path always looks like the root from dioxus fullstack + *request.uri_mut() = "/".parse().unwrap(); + RenderHandleState::render_handler(state, request).await + }, + ) + .with_state(RenderHandleState::new(cfg, root)), ) } router } -pub fn serve(mut serve_it: impl FnMut() -> F) +/// Serve a fullstack dioxus application with a custom axum router. +/// +/// This function sets up an async runtime, enables the default dioxus logger, runs the provided initializer, +/// and then starts an axum server with the returned router. +/// +/// The axum router will be bound to the address specified by the `IP` and `PORT` environment variables, +/// defaulting to `127.0.0.1:8080` if not set. +pub fn serve(mut serve_it: impl FnMut() -> F) -> ! where - F: Future>, + F: Future>, { dioxus_logger::initialize_default(); - tokio::runtime::Runtime::new() - .unwrap() - .block_on(async move { - let router = serve_it().await.unwrap(); + let app_future = async move { + let router = serve_it().await.expect("Failed to create axum router"); + let address = dioxus_cli_config::fullstack_address_or_localhost(); + let listener = tokio::net::TcpListener::bind(address) + .await + .with_context(|| format!("Failed to bind app to given address: {address}")) + .unwrap(); + tracing::trace!("Listening on {address}"); + axum::serve::serve(listener, router) + .await + .expect("Failed to serve axum app"); + }; - let address = dioxus_cli_config::fullstack_address_or_localhost(); - let listener = tokio::net::TcpListener::bind(address).await.unwrap(); + if let Ok(handle) = tokio::runtime::Handle::try_current() { + handle.block_on(app_future); + } else { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(app_future); + } - tracing::trace!("Listening on {address}"); - - axum::serve::serve(listener, router.into_make_service()) - .await - .unwrap(); - }); - - // unreachable!("Serving a fullstack app should never return") + unreachable!("Serving a fullstack app should never return") } /// Create a router that serves the dioxus application at the appropriate base path. @@ -315,10 +327,10 @@ where /// - Hot-reloading /// - Async Runtime /// - Logging -pub fn router(app: fn() -> Element) -> axum::Router { - let cfg = ServeConfig::new().unwrap(); +pub fn router(app: fn() -> Element) -> Router { + let cfg = ServeConfig::new(); apply_base_path( - axum::Router::new().serve_dioxus_application(cfg.clone(), app), + Router::new().serve_dioxus_application(cfg.clone(), app), app, cfg, base_path().map(|s| s.to_string()), diff --git a/packages/fullstack-server/src/lib.rs b/packages/fullstack-server/src/lib.rs index 1ef221fd1..67ae758f0 100644 --- a/packages/fullstack-server/src/lib.rs +++ b/packages/fullstack-server/src/lib.rs @@ -11,9 +11,8 @@ pub use bytes::Bytes; pub use dioxus_fullstack_core::{ServerFnError, ServerFnResult}; pub use axum; -pub(crate) use config::*; +pub use config::ServeConfig; pub use config::*; -pub use config::{ServeConfig, ServeConfigBuilder}; pub use document::ServerDocument; pub use http; pub use inventory; @@ -46,3 +45,6 @@ pub use serverfn::*; pub mod isrg; pub use isrg::*; + +mod index_html; +pub(crate) use index_html::IndexHtml; diff --git a/packages/fullstack-server/src/server.rs b/packages/fullstack-server/src/server.rs index 89addd57b..202591eee 100644 --- a/packages/fullstack-server/src/server.rs +++ b/packages/fullstack-server/src/server.rs @@ -2,20 +2,17 @@ use crate::{ ssr::{SSRError, SsrRendererPool}, ServeConfig, ServerFunction, }; -use crate::{IncrementalRendererError, RenderFreshness}; use axum::{ - body, body::Body, extract::State, - http::{Request, Response, StatusCode}, + http::{Request, StatusCode}, response::IntoResponse, + response::Response, routing::*, }; use dioxus_core::{Element, VirtualDom}; -use dioxus_fullstack_core::HttpError; -use futures::Stream; -use http::{header::*, request::Parts}; -use std::path::Path; +use http::header::*; +use std::path::{Path, PathBuf}; use std::sync::Arc; use tower::util::MapResponse; use tower::ServiceExt; @@ -46,10 +43,9 @@ use tower_http::services::fs::ServeFileSystemResponseBody; /// // Note you can use `register_server_functions_with_context` /// // to inject the context into server functions running outside /// // of an SSR render context. -/// .fallback(get(render_handler) -/// .with_state(RenderHandleState::new(ServeConfig::new().unwrap(), app)) -/// ) -/// .into_make_service(); +/// .fallback(get(render_handler)) +/// .with_state(RenderHandleState::new(ServeConfig::new(), app)); +/// /// let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); /// axum::serve(listener, router).await.unwrap(); /// } @@ -103,7 +99,7 @@ pub trait DioxusRouterExt: DioxusRouterFnExt { /// let addr = dioxus::cli_config::fullstack_address_or_localhost(); /// let router = axum::Router::new() /// // Server side render the application, serve static assets, and register server functions - /// .serve_dioxus_application(ServeConfig::new().unwrap(), app) + /// .serve_dioxus_application(ServeConfig::new(), app) /// .into_make_service(); /// let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); /// axum::serve(listener, router).await.unwrap(); @@ -124,7 +120,7 @@ where S: Send + Sync + Clone + 'static, { fn serve_static_assets(self) -> Self { - let public_path = crate::public_path(); + let public_path = public_path(); if !public_path.exists() { return self; @@ -166,6 +162,7 @@ pub trait DioxusRouterFnExt { fn register_server_functions(self) -> Self where Self: Sized; + /// Serves a Dioxus application without static assets. /// Sets up server function routes and rendering endpoints only. /// @@ -179,7 +176,7 @@ pub trait DioxusRouterFnExt { /// #[tokio::main] /// async fn main() { /// let router = axum::Router::new() - /// .serve_api_application(ServeConfig::new().unwrap(), app) + /// .serve_api_application(ServeConfig::new(), app) /// .into_make_service(); /// // ... /// } @@ -195,8 +192,14 @@ pub trait DioxusRouterFnExt { impl DioxusRouterFnExt for Router { fn register_server_functions(mut self) -> Self { - for f in ServerFunction::collect() { - self = f.register_server_fn_on_router(self); + for func in ServerFunction::collect() { + tracing::info!( + "Registering server function: {} {}", + func.method(), + func.path() + ); + + self = func.register_server_fn_on_router(self); } self } @@ -229,7 +232,8 @@ impl RenderHandleState { } } - /// Create a new [`RenderHandleState`] with a custom [`VirtualDom`] factory. This method can be used to pass context into the root component of your application. + /// Create a new [`RenderHandleState`] with a custom [`VirtualDom`] factory. This method can be + /// used to pass context into the root component of your application. pub fn new_with_virtual_dom_factory( config: ServeConfig, build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static, @@ -272,36 +276,36 @@ impl RenderHandleState { /// // Note you can use `register_server_functions_with_context` /// // to inject the context into server functions running outside /// // of an SSR render context. - /// .fallback(get(render_handler) - /// .with_state(RenderHandleState::new(ServeConfig::new().unwrap(), app)) - /// ) + /// .fallback(get(render_handler)) + /// .with_state(RenderHandleState::new(ServeConfig::new(), app)) /// .into_make_service(); + /// /// let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); /// axum::serve(listener, router).await.unwrap(); /// } /// ``` - pub async fn render_handler( - State(state): State, - request: Request, - ) -> impl IntoResponse { - let cfg = &state.config; - let build_virtual_dom = { - let build_virtual_dom = state.build_virtual_dom.clone(); - let context_providers = state.config.context_providers.clone(); - move || { - let mut vdom = build_virtual_dom(); - for state in context_providers.as_slice() { - vdom.insert_any_root_context(state()); - } - vdom - } - }; - + pub async fn render_handler(State(state): State, request: Request) -> Response { let (parts, _) = request.into_parts(); - match state.render(parts, cfg, build_virtual_dom).await { + let response = state + .renderers + .clone() + .render_to(parts, &state.config, { + let build_virtual_dom = state.build_virtual_dom.clone(); + let context_providers = state.config.context_providers.clone(); + move || { + let mut vdom = build_virtual_dom(); + for state in context_providers.as_slice() { + vdom.insert_any_root_context(state()); + } + vdom + } + }) + .await; + + match response { Ok((status, freshness, rx)) => { - let mut response = axum::response::Response::builder() + let mut response = Response::builder() .status(status.status) .header(CONTENT_TYPE, "text/html; charset=utf-8") .body(Body::from_stream(rx)) @@ -309,15 +313,20 @@ impl RenderHandleState { freshness.write(response.headers_mut()); - Result::, StatusCode>::Ok(response) + response } Err(SSRError::Incremental(e)) => { tracing::error!("Failed to render page: {}", e); - Ok(Self::err_to_axum_response(e).into_response()) + + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(e.to_string()) + .unwrap() + .into_response() } - Err(SSRError::HttpError { status, message }) => Ok(Response::builder() + Err(SSRError::HttpError { status, message }) => Response::builder() .status(status) .body(Body::from(message.unwrap_or_else(|| { status @@ -325,43 +334,26 @@ impl RenderHandleState { .unwrap_or("An unknown error occurred") .to_string() }))) - .unwrap()), + .unwrap(), } } - - fn err_to_axum_response(e: E) -> Response { - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(body::Body::new(format!("Error: {}", e))) - .unwrap() - } - - /// Render the application to HTML. - pub async fn render<'a>( - &'a self, - parts: Parts, - cfg: &'a ServeConfig, - virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static, - ) -> Result< - ( - HttpError, - RenderFreshness, - impl Stream>, - ), - SSRError, - > { - self.renderers - .clone() - .render_to(parts, cfg, virtual_dom_factory) - .await - } } -fn serve_dir_cached( - mut router: Router, - public_path: &std::path::Path, - directory: &std::path::Path, -) -> Router +/// Get the path to the public assets directory to serve static files from +pub(crate) fn public_path() -> PathBuf { + if let Ok(path) = std::env::var("DIOXUS_PUBLIC_PATH") { + return PathBuf::from(path); + } + + // The CLI always bundles static assets into the exe/public directory + std::env::current_exe() + .expect("Failed to get current executable path") + .parent() + .unwrap() + .join("public") +} + +fn serve_dir_cached(mut router: Router, public_path: &Path, directory: &Path) -> Router where S: Send + Sync + Clone + 'static, { @@ -376,8 +368,16 @@ where if path == public_path.join("index.html") { continue; } - let route = path.strip_prefix(public_path).unwrap(); - let route = path_components_to_route_lossy(route); + + let route = format!( + "/{}", + path.strip_prefix(public_path) + .unwrap() + .iter() + .map(|segment| segment.to_string_lossy()) + .collect::>() + .join("/") + ); if path.is_dir() { router = serve_dir_cached(router, public_path, &path); @@ -397,15 +397,6 @@ where router } -fn path_components_to_route_lossy(path: &Path) -> String { - let route = path - .iter() - .map(|segment| segment.to_string_lossy()) - .collect::>() - .join("/"); - format!("/{}", route) -} - type MappedAxumService = MapResponse< S, fn(Response) -> Response, diff --git a/packages/fullstack-server/src/serverfn.rs b/packages/fullstack-server/src/serverfn.rs index 6e1db515d..abfc306be 100644 --- a/packages/fullstack-server/src/serverfn.rs +++ b/packages/fullstack-server/src/serverfn.rs @@ -1,5 +1,4 @@ use axum::body::Body; -use axum::handler::Handler; use axum::routing::MethodRouter; use axum::Router; use dashmap::DashMap; @@ -19,10 +18,6 @@ pub struct ServerFunction { _phantom: PhantomData, } -pub struct MakeRequest { - _phantom: PhantomData, -} - impl ServerFunction { /// Create a new server function object. pub const fn new( @@ -52,56 +47,14 @@ impl ServerFunction { inventory::iter::().collect() } + pub fn handler(&self) -> fn() -> MethodRouter { + self.handler + } + pub fn register_server_fn_on_router(&'static self, router: Router) -> Router where S: Send + Sync + Clone + 'static, { - tracing::info!( - "Registering server function: {} {}", - self.method(), - self.path() - ); - use http::method::Method; - let path = self.path(); - let method = self.method(); - let handler = move |req| self.handle_server_fns_inner(req); - match method { - Method::GET => router.route(path, axum::routing::get(handler)), - Method::POST => router.route(path, axum::routing::post(handler)), - Method::PUT => router.route(path, axum::routing::put(handler)), - Method::DELETE => router.route(path, axum::routing::delete(handler)), - Method::PATCH => router.route(path, axum::routing::patch(handler)), - Method::HEAD => router.route(path, axum::routing::head(handler)), - Method::OPTIONS => router.route(path, axum::routing::options(handler)), - Method::CONNECT => router.route(path, axum::routing::connect(handler)), - Method::TRACE => router.route(path, axum::routing::trace(handler)), - _ => unimplemented!("Unsupported server function method: {}", method), - } - } - - pub async fn handle_server_fns_inner(&self, req: http::Request) -> http::Response { - // todo: jon - // - bring back middleware to serverfns. the new layer system is fine but isn't a full replacement - // - // use axum::body; - // use axum::extract::State; - // use axum::routing::*; - // use axum::{ - // body::Body, - // http::{Request, Response, StatusCode}, - // response::IntoResponse, - // }; - // use http::header::*; - - // let (parts, body) = req.into_parts(); - // let req = Request::from_parts(parts.clone(), body); - - // // Create the server context with info from the request - // let server_context = DioxusServerContext::new(parts); - - // // Provide additional context from the render state - // server_context.add_server_context(&additional_context); - // // store Accepts and Referrer in case we need them for redirect (below) // let referrer = req.headers().get(REFERER).cloned(); // let accepts_html = req @@ -111,13 +64,6 @@ impl ServerFunction { // .map(|v| v.contains("text/html")) // .unwrap_or(false); - // actually run the server fn (which may use the server context) - // let fut = crate::with_server_context(server_context.clone(), || service.run(req)); - // let fut = crate::with_server_context(server_context.clone(), || service.run(req)); - - // let res = ProvideServerContext::new(fut, server_context.clone()).await; - // let mut res = res.res; - // // it it accepts text/html (i.e., is a plain form post) and doesn't already have a // // Location set, then redirect to Referer // if accepts_html { @@ -130,13 +76,10 @@ impl ServerFunction { // } // } - // // apply the response parts from the server context to the response - // server_context.send_response(&mut res); - - let mthd: MethodRouter = - (self.handler)().with_state(DioxusServerState {}); - - mthd.call(req, DioxusServerState {}).await + router.route( + self.path(), + ((self.handler)()).with_state(DioxusServerState {}), + ) } } diff --git a/packages/fullstack-server/src/ssr.rs b/packages/fullstack-server/src/ssr.rs index 481fb7364..a52445a3d 100644 --- a/packages/fullstack-server/src/ssr.rs +++ b/packages/fullstack-server/src/ssr.rs @@ -667,8 +667,6 @@ impl SsrRendererPool { to: &mut R, virtual_dom: &VirtualDom, ) -> Result<(), IncrementalRendererError> { - let ServeConfig { index, .. } = cfg; - let title = { let document: Option> = virtual_dom.in_scope(ScopeId::ROOT, dioxus_core::try_consume_context); @@ -676,13 +674,13 @@ impl SsrRendererPool { document.and_then(|document| document.title()) }; - to.write_str(&index.head_before_title)?; + to.write_str(&cfg.index.head_before_title)?; if let Some(title) = title { to.write_str(&title)?; } else { - to.write_str(&index.title)?; + to.write_str(&cfg.index.title)?; } - to.write_str(&index.head_after_title)?; + to.write_str(&cfg.index.head_after_title)?; let document = virtual_dom.in_scope(ScopeId::ROOT, try_consume_context::>); @@ -704,9 +702,7 @@ impl SsrRendererPool { cfg: &ServeConfig, to: &mut R, ) -> Result<(), IncrementalRendererError> { - let ServeConfig { index, .. } = cfg; - - to.write_str(&index.close_head)?; + to.write_str(&cfg.index.close_head)?; // // #[cfg(feature = "document")] // { @@ -723,8 +719,6 @@ impl SsrRendererPool { to: &mut R, virtual_dom: &VirtualDom, ) -> Result<(), IncrementalRendererError> { - let ServeConfig { index, .. } = cfg; - // Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s. // Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend let resolved_data = SsrRendererPool::serialize_server_data(virtual_dom, ScopeId::ROOT); @@ -749,7 +743,7 @@ impl SsrRendererPool { )?; } write!(to, r#""#,)?; - to.write_str(&index.post_main)?; + to.write_str(&cfg.index.post_main)?; Ok(()) } @@ -759,9 +753,7 @@ impl SsrRendererPool { cfg: &ServeConfig, to: &mut R, ) -> Result<(), IncrementalRendererError> { - let ServeConfig { index, .. } = cfg; - - to.write_str(&index.after_closing_body_tag)?; + to.write_str(&cfg.index.after_closing_body_tag)?; Ok(()) } diff --git a/packages/generational-box/src/references.rs b/packages/generational-box/src/references.rs index 25c3b81fb..7591e34e5 100644 --- a/packages/generational-box/src/references.rs +++ b/packages/generational-box/src/references.rs @@ -104,6 +104,14 @@ impl> GenerationalRef { guard: self.guard, }) } + + /// Clone the inner value. This requires that the inner value implements [`Clone`]. + pub fn cloned(&self) -> T + where + T: Clone, + { + self.inner.deref().clone() + } } impl> Debug for GenerationalRef { diff --git a/packages/hooks/src/use_action.rs b/packages/hooks/src/use_action.rs index 6035710b0..2588db99a 100644 --- a/packages/hooks/src/use_action.rs +++ b/packages/hooks/src/use_action.rs @@ -3,7 +3,7 @@ use dioxus_core::{use_hook, Callback, CapturedError, Result, Task}; use dioxus_signals::{ReadSignal, ReadableBoxExt, ReadableExt, Signal, WritableExt}; use futures_channel::oneshot::Receiver; use futures_util::{future::Shared, FutureExt}; -use std::{marker::PhantomData, prelude::rust_2024::Future}; +use std::{marker::PhantomData, pin::Pin, prelude::rust_2024::Future, task::Poll}; pub fn use_action(mut user_fn: C) -> Action where @@ -82,15 +82,7 @@ pub struct Action { } impl Action { - pub fn value(&self) -> Option> { - if self.value.read().is_none() { - return None; - } - - Some(self.reader) - } - - pub fn result(&self) -> Option, CapturedError>> { + pub fn value(&self) -> Option, CapturedError>> { if !matches!( *self.state.read(), ActionState::Ready | ActionState::Errored @@ -109,7 +101,7 @@ impl Action { Some(Ok(self.reader)) } - pub fn is_pending(&self) -> bool { + pub fn pending(&self) -> bool { *self.state.read() == ActionState::Pending } @@ -162,13 +154,10 @@ impl Dispatching { impl std::future::Future for Dispatching { type Output = (); - fn poll( - mut self: std::pin::Pin<&mut Self>, - _cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { + fn poll(mut self: Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> Poll { match self.receiver.poll_unpin(_cx) { - std::task::Poll::Ready(_) => std::task::Poll::Ready(()), - std::task::Poll::Pending => std::task::Poll::Pending, + Poll::Ready(_) => Poll::Ready(()), + Poll::Pending => Poll::Pending, } } } @@ -281,6 +270,7 @@ impl Action<(A, B), O> { Dispatching::new((self.callback).call((_a, _b))) } } + impl Action<(A, B, C), O> { pub fn call(&mut self, _a: A, _b: B, _c: C) -> Dispatching<()> { Dispatching::new((self.callback).call((_a, _b, _c))) diff --git a/packages/hooks/src/use_resource.rs b/packages/hooks/src/use_resource.rs index 194a24c79..b9f7abf9b 100644 --- a/packages/hooks/src/use_resource.rs +++ b/packages/hooks/src/use_resource.rs @@ -11,8 +11,8 @@ use futures_util::{ future::{self}, pin_mut, FutureExt, StreamExt, }; -use std::ops::Deref; use std::{cell::Cell, future::Future, rc::Rc}; +use std::{fmt::Debug, ops::Deref}; #[doc = include_str!("../docs/use_resource.md")] #[doc = include_str!("../docs/rules_of_hooks.md")] @@ -324,6 +324,11 @@ impl Resource { self.task.cloned() } + /// Is the resource's future currently running? + pub fn pending(&self) -> bool { + matches!(*self.state.peek(), UseResourceState::Pending) + } + /// Is the resource's future currently finished running? /// /// Reading this does not subscribe to the future's state @@ -439,6 +444,35 @@ impl Resource { } } +impl Resource> { + /// Convert the `Resource>` into an `Option, MappedSignal>>` + #[allow(clippy::type_complexity)] + pub fn result( + &self, + ) -> Option< + Result< + MappedSignal>>>, + MappedSignal>>>, + >, + > { + let value: MappedSignal>>> = self.value.map(|v| match v { + Some(Ok(ref res)) => res, + _ => panic!("Resource is not ready"), + }); + + let error: MappedSignal>>> = self.value.map(|v| match v { + Some(Err(ref err)) => err, + _ => panic!("Resource is not ready"), + }); + + match &*self.value.peek() { + Some(Ok(_)) => Some(Ok(value)), + Some(Err(_)) => Some(Err(error)), + None => None, + } + } +} + impl From> for ReadSignal> { fn from(val: Resource) -> Self { val.value.into() @@ -468,6 +502,19 @@ impl Readable for Resource { } } +impl Writable for Resource { + type WriteMetadata = > as Writable>::WriteMetadata; + + fn try_write_unchecked( + &self, + ) -> Result, generational_box::BorrowMutError> + where + Self::Target: 'static, + { + self.value.try_write_unchecked() + } +} + impl IntoAttributeValue for Resource where T: Clone + IntoAttributeValue, diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index b19c0bcdb..fd1b9446d 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -278,6 +278,8 @@ impl_event! [ onmounted ]; +pub use onmounted as onmount; + /// The MountedResult type for the MountedData pub type MountedResult = Result; diff --git a/packages/logger/src/lib.rs b/packages/logger/src/lib.rs index 1bdad935c..3fc954157 100644 --- a/packages/logger/src/lib.rs +++ b/packages/logger/src/lib.rs @@ -101,7 +101,8 @@ pub fn init(level: Level) -> Result<(), SetGlobalDefaultError> { .with_env_filter( tracing_subscriber::EnvFilter::builder() .with_default_directive(level.into()) - .from_env_lossy(), + .from_env_lossy() + .add_directive("hyper_util=warn".parse().unwrap()), // hyper has `debug!` sitting around in some places that are spammy ); if !dioxus_cli_config::is_cli_enabled() { diff --git a/packages/signals/src/memo.rs b/packages/signals/src/memo.rs index 8f2c588e8..a93774472 100644 --- a/packages/signals/src/memo.rs +++ b/packages/signals/src/memo.rs @@ -1,6 +1,6 @@ -use crate::CopyValue; -use crate::{read::Readable, ReadableRef, Signal}; +use crate::{read::Readable, write_impls, ReadableRef, Signal}; use crate::{read_impls, GlobalMemo, ReadableExt, WritableExt}; +use crate::{CopyValue, Writable}; use std::{ cell::RefCell, ops::Deref, @@ -214,6 +214,19 @@ where } } +impl Writable for Memo { + type WriteMetadata = as Writable>::WriteMetadata; + + fn try_write_unchecked( + &self, + ) -> Result, generational_box::BorrowMutError> + where + Self::Target: 'static, + { + self.inner.try_write_unchecked() + } +} + impl IntoAttributeValue for Memo where T: Clone + IntoAttributeValue + PartialEq + 'static, @@ -250,6 +263,7 @@ where } read_impls!(Memo where T: PartialEq); +write_impls!(Memo where T: PartialEq); impl Clone for Memo { fn clone(&self) -> Self {