Core Concepts
Forms
Forms are an essential part of any application.
Introduction
By default, Radonis applications work like a traditional monolythic application: Forms are submitted and trigger a page reload, no JavaScript required. With the help of client-side hydration and flash messages, this "old school" way of handling user input comes really close to the modern UX known from Single Page Applications. But there are cases, being some small interaction like a "Add to favorites" button or a form inside a modal, where communicating with the backend via fetch delivers a better UX for the user. Radonis ships with a form component that can do both, and switching between submit and fetch is as simple as adding the noReload
prop to the form component.
The different ways of using forms
The following chapter demonstrates the two ways in which the <Form>
component can be used to submit user input to the AdonisJS backend.
Via fetch
The following snippet shows how a form is created that submits data via fetch.
import { Form } from '@microeinhundert/radonis'
type Data = {
title: string
description: string
}
type Error = { errors: { field: keyof Data, message: string, rule: string }[] }
// Helper for extracting the error message for a specific field
function extractFieldErrorMessage(field: keyof Data, { errors }: Error): string | undefined {
return errors.find((error) => error.field === field)?.message
}
function FetchFormDemo() {
return (
<Form<Data, Error>
action$="YourController.store"
method="post" // or `get`, `put`, `delete`, `patch`
params={{ someParam: 'hello' }}
queryParams={{ someQueryParam: 'world' }}
noReload // Use fetch instead of reloading the page
hooks={{
onMutate: ({ input }) => {
// Do something before the mutation runs
return () => {
// Rollback changes if the mutation failed
}
},
onSuccess: ({ data, input }) => {
// Do something once the mutation succeeded
},
onFailure: ({ error, rollback, input }) => {
// Do something once the mutation failed,
// like executing the rollback
rollback?.()
},
onSettled: ({ status, error, data, rollback, input }) => {
switch (status) {
case 'success': {
// Do something if the mutation succeeded
}
case 'failure': {
// Do something if the mutation failed
}
}
},
}}
>
{({ status, error }) => {
const isSubmitting = status === 'loading'
const titleErrorMessage = extractFieldErrorMessage('title', error)
const descriptionErrorMessage = extractFieldErrorMessage('description', error)
return (
<>
<label>
<span>Title</span>
<input name="title" type="text" required />
{titleErrorMessage && <span>{titleErrorMessage}</span>}
</label>
<label>
<span>Description</span>
<textarea name="description" required />
{descriptionErrorMessage && <span>{descriptionErrorMessage}</span>}
</label>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</>
)
}}
</Form>
)
}
Via submission
The following snippet shows how a form is created that submits the form traditionally.
import { useFlashMessages, Form } from '@microeinhundert/radonis'
function TraditionalFormDemo() {
const flashMessages = useFlashMessages()
return (
<Form
method="post" // or `get`, `put`, `delete`, `patch`
action$="YourController.store"
params={{ someParam: 'hello' }}
queryParams={{ someQueryParam: 'world' }}
>
<label>
<span>Title</span>
<input name="title" type="text" required />
{flashMessages.has$('errors.title') && <span>{flashMessages.get$('errors.title')}</span>}
</label>
<label>
<span>Description</span>
<textarea name="description" required />
{flashMessages.has$('errors.description') && <span>{flashMessages.get$('errors.description')}</span>}
</label>
<button type="submit">Submit</button>
</Form>
)
}