Skip to main content

dioform_fullstack/
lib.rs

1//! Dioxus Fullstack submit adapters for Dioform.
2//!
3//! The adapter keeps Dioform's submission lifecycle local to the form. Server functions return
4//! application-defined transport payloads, and callers map those payloads into structured
5//! [`SubmitErrors`]. Transport failures stay on a separate explicit mapping path so network,
6//! serialization, or server-function invocation failures do not become submit errors by accident.
7//!
8//! ```rust,no_run
9//! #[cfg(feature = "server")]
10//! mod fullstack_submit_example {
11//!     extern crate dioxus_server;
12//!
13//!     use dioxus_core::Event;
14//!     use dioform::{Form, FormHandle, SubmitError};
15//!     use dioform_fullstack::{ServerSubmitOutcome, SubmitBindingFullstackExt};
16//!     use dioxus_fullstack::{server, ServerFnError, ServerFnResult};
17//!     use serde::{Deserialize, Serialize};
18//!
19//!     #[derive(Clone, Debug, Eq, Form, PartialEq)]
20//!     struct SignupForm {
21//!         email: String,
22//!     }
23//!
24//!     #[derive(Clone, Copy, Debug, Eq, PartialEq)]
25//!     enum SignupError {
26//!         EmailTaken,
27//!         TryAgain,
28//!     }
29//!
30//!     #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
31//!     enum SignupRejection {
32//!         EmailTaken,
33//!     }
34//!
35//!     #[server]
36//!     async fn submit_signup(
37//!         email: String,
38//!     ) -> ServerFnResult<ServerSubmitOutcome<SignupRejection>> {
39//!         if email == "taken@example.com" {
40//!             Ok(ServerSubmitOutcome::rejected(SignupRejection::EmailTaken))
41//!         } else {
42//!             Ok(ServerSubmitOutcome::accepted())
43//!         }
44//!     }
45//!
46//!     fn submit_from_form<EventData: ?Sized + 'static>(
47//!         form: FormHandle<SignupForm, SignupError>,
48//!         event: Event<EventData>,
49//!     ) {
50//!         form.managed_submit().on_submit_server_fn(
51//!             event,
52//!             |submitted| async move { submit_signup(submitted.value().email.clone()).await },
53//!             |rejection| match rejection {
54//!                 SignupRejection::EmailTaken => {
55//!                     SubmitError::field(SignupForm::fields().email(), SignupError::EmailTaken).into()
56//!                 }
57//!             },
58//!             |_failure: ServerFnError| SubmitError::form(SignupError::TryAgain).into(),
59//!         );
60//!     }
61//! }
62//! ```
63
64use std::future::Future;
65
66use dioform::{IntentSubmitBinding, SubmissionSnapshot, SubmitBinding, SubmitErrors, SubmitResult};
67use dioxus_core::Event;
68
69/// Server-side outcome for one submitted form value.
70///
71/// Transport failures are kept outside this type. A server function can return this as its
72/// successful transport payload so application rejections map into structured submit errors while
73/// operational failures stay on the explicit failure mapping path.
74#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
75pub enum ServerSubmitOutcome<Rejection> {
76    /// The server accepted the submitted value.
77    Accepted,
78    /// The server rejected the submitted value with application-defined rejection details.
79    Rejected(Rejection),
80}
81
82impl<Rejection> ServerSubmitOutcome<Rejection> {
83    /// Creates an accepted server submit outcome.
84    pub const fn accepted() -> Self {
85        Self::Accepted
86    }
87
88    /// Creates a rejected server submit outcome.
89    pub const fn rejected(rejection: Rejection) -> Self {
90        Self::Rejected(rejection)
91    }
92}
93
94impl<Success, Rejection> From<Result<Success, Rejection>> for ServerSubmitOutcome<Rejection> {
95    fn from(value: Result<Success, Rejection>) -> Self {
96        match value {
97            Ok(_) => Self::Accepted,
98            Err(rejection) => Self::Rejected(rejection),
99        }
100    }
101}
102
103fn map_server_submit_result<Model, Error, Outcome, Rejection, Failure, MapRejection, MapFailure>(
104    result: Result<Outcome, Failure>,
105    map_rejection: MapRejection,
106    map_failure: MapFailure,
107) -> SubmitErrors<Model, Error>
108where
109    Outcome: Into<ServerSubmitOutcome<Rejection>>,
110    MapRejection: FnOnce(Rejection) -> SubmitErrors<Model, Error>,
111    MapFailure: FnOnce(Failure) -> SubmitErrors<Model, Error>,
112{
113    match result {
114        Ok(outcome) => match outcome.into() {
115            ServerSubmitOutcome::Accepted => SubmitErrors::none(),
116            ServerSubmitOutcome::Rejected(rejection) => map_rejection(rejection),
117        },
118        Err(failure) => map_failure(failure),
119    }
120}
121
122/// Dioxus Fullstack-oriented submit helpers for ordinary managed submit bindings.
123pub trait SubmitBindingFullstackExt<Model, Error> {
124    /// Applies a Dioxus `onsubmit` event and starts a managed async submit that calls a
125    /// server-function-shaped async operation.
126    fn on_submit_server_fn<
127        EventData,
128        Call,
129        Fut,
130        Outcome,
131        Rejection,
132        Failure,
133        MapRejection,
134        MapFailure,
135    >(
136        &self,
137        event: Event<EventData>,
138        call: Call,
139        map_rejection: MapRejection,
140        map_failure: MapFailure,
141    ) -> SubmitResult
142    where
143        EventData: ?Sized + 'static,
144        Call: FnOnce(SubmissionSnapshot<Model>) -> Fut + 'static,
145        Fut: Future<Output = Result<Outcome, Failure>> + 'static,
146        Outcome: Into<ServerSubmitOutcome<Rejection>> + 'static,
147        MapRejection: FnOnce(Rejection) -> SubmitErrors<Model, Error> + 'static,
148        MapFailure: FnOnce(Failure) -> SubmitErrors<Model, Error> + 'static,
149        Model: 'static,
150        Error: 'static;
151}
152
153impl<Model, Error> SubmitBindingFullstackExt<Model, Error> for SubmitBinding<Model, Error>
154where
155    Model: Clone + 'static,
156    Error: 'static,
157{
158    fn on_submit_server_fn<
159        EventData,
160        Call,
161        Fut,
162        Outcome,
163        Rejection,
164        Failure,
165        MapRejection,
166        MapFailure,
167    >(
168        &self,
169        event: Event<EventData>,
170        call: Call,
171        map_rejection: MapRejection,
172        map_failure: MapFailure,
173    ) -> SubmitResult
174    where
175        EventData: ?Sized + 'static,
176        Call: FnOnce(SubmissionSnapshot<Model>) -> Fut + 'static,
177        Fut: Future<Output = Result<Outcome, Failure>> + 'static,
178        Outcome: Into<ServerSubmitOutcome<Rejection>> + 'static,
179        MapRejection: FnOnce(Rejection) -> SubmitErrors<Model, Error> + 'static,
180        MapFailure: FnOnce(Failure) -> SubmitErrors<Model, Error> + 'static,
181    {
182        self.on_submit_async(event, move |submitted| async move {
183            map_server_submit_result(call(submitted).await, map_rejection, map_failure)
184        })
185    }
186}
187
188/// Dioxus Fullstack-oriented submit helpers for intent-scoped managed submit bindings.
189pub trait IntentSubmitBindingFullstackExt<Model, Intent, Error> {
190    /// Applies a Dioxus `onsubmit` event and starts a managed async submit that calls a
191    /// server-function-shaped async operation with the binding's submit intent.
192    fn on_submit_server_fn<
193        EventData,
194        Call,
195        Fut,
196        Outcome,
197        Rejection,
198        Failure,
199        MapRejection,
200        MapFailure,
201    >(
202        &self,
203        event: Event<EventData>,
204        call: Call,
205        map_rejection: MapRejection,
206        map_failure: MapFailure,
207    ) -> SubmitResult
208    where
209        EventData: ?Sized + 'static,
210        Call: FnOnce(SubmissionSnapshot<Model, Intent>) -> Fut + 'static,
211        Fut: Future<Output = Result<Outcome, Failure>> + 'static,
212        Outcome: Into<ServerSubmitOutcome<Rejection>> + 'static,
213        MapRejection: FnOnce(Rejection) -> SubmitErrors<Model, Error> + 'static,
214        MapFailure: FnOnce(Failure) -> SubmitErrors<Model, Error> + 'static,
215        Model: 'static,
216        Intent: 'static,
217        Error: 'static;
218}
219
220impl<Model, Intent, Error> IntentSubmitBindingFullstackExt<Model, Intent, Error>
221    for IntentSubmitBinding<Model, Intent, Error>
222where
223    Model: Clone + 'static,
224    Intent: Clone + PartialEq + 'static,
225    Error: 'static,
226{
227    fn on_submit_server_fn<
228        EventData,
229        Call,
230        Fut,
231        Outcome,
232        Rejection,
233        Failure,
234        MapRejection,
235        MapFailure,
236    >(
237        &self,
238        event: Event<EventData>,
239        call: Call,
240        map_rejection: MapRejection,
241        map_failure: MapFailure,
242    ) -> SubmitResult
243    where
244        EventData: ?Sized + 'static,
245        Call: FnOnce(SubmissionSnapshot<Model, Intent>) -> Fut + 'static,
246        Fut: Future<Output = Result<Outcome, Failure>> + 'static,
247        Outcome: Into<ServerSubmitOutcome<Rejection>> + 'static,
248        MapRejection: FnOnce(Rejection) -> SubmitErrors<Model, Error> + 'static,
249        MapFailure: FnOnce(Failure) -> SubmitErrors<Model, Error> + 'static,
250    {
251        self.on_submit_async(event, move |submitted| async move {
252            map_server_submit_result(call(submitted).await, map_rejection, map_failure)
253        })
254    }
255}