Skip to content

Luna: Forms ​

Introduction ​

Forms are where a Luna app sends data back to the server. You could do this with a raw router visit, but Luna gives you two purpose-built tools that handle the tedious parts — tracking field state, submitting, wiring up validation errors, showing progress, and resetting:

  • The useForm helper — a stateful form object you drive imperatively.
  • The <Form> component — a declarative wrapper that submits its fields for you.

Both integrate with LaraGram's server-side validation and, optionally, Precognition for live validation.

The useForm Helper ​

useForm returns a form object holding your data, error state, and submission helpers. Give it an initial data shape:

jsx
import { useForm } from '@laraxgram/react'

function CreateUser() {
    const form = useForm({
        name: '',
        email: '',
    })

    function submit(e) {
        e.preventDefault()
        form.post('/users')
    }

    return (
        <form onSubmit={submit}>
            <input
                value={form.data.name}
                onChange={(e) => form.setData('name', e.target.value)}
            />
            {form.errors.name && <div>{form.errors.name}</div>}

            <button type="submit" disabled={form.processing}>
                Create
            </button>
        </form>
    )
}

You can also pass a method and URL up front so the form knows how to submit itself — useful with the Telegram MainButton:

js
const form = useForm('post', '/users', { name: '', email: '' })
form.submit() // Uses the bound method + URL.

Form State & Methods ​

The form object exposes everything you need to render and control the form:

MemberDescription
dataThe current field values.
setData(key, value)Update a field (also accepts an object or updater callback).
errorsValidation errors, keyed by field.
hasErrorsWhether any errors are present.
setError / clearErrorsSet or clear errors manually.
processingtrue while a submit is in flight.
progressUpload progress (for file uploads).
isDirtyWhether data differs from the initial/last-submitted values.
wasSuccessfulThe last submit succeeded.
recentlySuccessfultrue briefly after success — handy for a "Saved ✓" flash.
reset(...fields)Reset fields to their defaults (all fields if none named).
resetAndClearErrors(...fields)Reset and clear errors together.
transform(cb)Transform the data just before submit.
submit / get / post / put / patch / deleteSubmit the form.
cancel()Cancel an in-flight submit.

Submitting ​

Each verb accepts a URL and the same visit options as the router, including lifecycle callbacks:

js
form.post('/users', {
    preserveScroll: true,
    onSuccess: () => form.reset(),
    onError: (errors) => console.log(errors),
    onFinish: () => {},
})

Transforming Data Before Submit ​

Use transform to shape the payload without mutating your form state — for example, combining fields or adding a computed value:

js
form.transform((data) => ({
    ...data,
    name: data.name.trim(),
})).post('/users')

Validation Errors ​

Server-side validation needs no special handling. Validate in the controller as usual; when validation fails, LaraGram redirects back with the errors, and Luna surfaces them on form.errors automatically:

php
public function store(Request $request)
{
    $request->validate([
        'name' => ['required', 'string', 'max:255'],
        'email' => ['required', 'email', 'unique:users'],
    ]);

    // ...
}
jsx
{form.errors.email && <p className="error">{form.errors.email}</p>}

Errors are also shared globally, so you can read them via the page object (usePage().props.errors) even outside a useForm.

File Uploads ​

Include a File anywhere in your form data and Luna automatically encodes the request as multipart/form-data and reports upload progress:

jsx
const form = useForm({
    name: '',
    avatar: null,
})

<input
    type="file"
    onChange={(e) => form.setData('avatar', e.target.files[0])}
/>

{form.progress && (
    <progress value={form.progress.percentage} max="100" />
)}

NOTE

Because HTML forms can't natively send PUT/PATCH/DELETE with files, submit file uploads via post and add a _method field (form.setData('_method', 'put')) for LaraGram's method spoofing.

The Form Component ​

When you don't need imperative control, the <Form> component is the least-boilerplate option. It collects its child inputs by name and submits them to the given action:

jsx
import { Form } from '@laraxgram/react'

<Form action="/users" method="post" resetOnSuccess>
    {({ processing, errors }) => (
        <>
            <input name="name" />
            {errors.name && <span>{errors.name}</span>}

            <input name="email" />
            {errors.email && <span>{errors.email}</span>}

            <button type="submit" disabled={processing}>Create</button>
        </>
    )}
</Form>

The render-prop exposes the same state as useForm (processing, errors, isDirty, …). Useful props:

PropDescription
actionThe submit URL.
methodHTTP verb.
transformTransform data before submit.
optionsVisit options passed through to the request.
onSuccess / onErrorLifecycle callbacks.
resetOnSuccess / resetOnErrorReset fields after the outcome.
disableWhileProcessingDisable the form's controls during submit.

Access the form context from a nested component with useFormContext(). Vue and Svelte export the equivalent <Form> and useFormContext.

Live Validation with Precognition ​

Luna's forms integrate with LaraGram Precognition for real-time, server-authoritative validation — validate a field on blur using the same rules as your final submit, with no duplicated client logic. The form object gains validate, validating, touch, and forgetError:

jsx
const form = useForm({ email: '' })

<input
    value={form.data.email}
    onChange={(e) => form.setData('email', e.target.value)}
    onBlur={() => form.validate('email')}
/>
{form.validating && <span>Checking…</span>}
{form.errors.email && <span>{form.errors.email}</span>}

On the server, add the Precognition middleware to the route and use a form request; Luna sends a precognitive request that runs validation without executing the controller body. See the Precognition documentation for the full setup.

Next Steps ​

Configure your framework, layouts, and build pipeline in Frontend Setup, or start building a Telegram Mini App — where forms drive the native MainButton.

Released under the MIT License.