@opensky/remotes
v0.0.3
Published
SvelteKit remotes helper utilities
Maintainers
Readme
@opensky/remotes
Provides two helpers createValidation and createEnhancedForm that improve the behaviors and experience working with remote form functions.
createValidation
Use this to achieve better user experience around validation issues. The logic for showing issues on fields is: issues only appear when you leave a field (onblur events) but they are cleared on input. This way you don't see issues appear as you type, but if the field shows an issue, as you type, the issue will go away as soon as you modify it to be valid.
This follows the same validation as superforms did and is inspired by several articles. (todo: links)
Usage
import { createValidation } from '@opensky/remotes'
const valid = createValidation(remoteForm)Returns an object with:
fields(path: string)- Returnsonblurandoninputhandlers for a field pathissues(path: string)- Returns validation issues for a field pathallIssues()- Returns all validation issuesaddIssue(path: string, issue: string)- Adds a custom validation error to a fieldreset()- Clears all validation issuesvalidateAll()- Validates all registered fields (with server)updateIssues()- Updates issues for all registered fields (populates from issues)
Use .fields() to add the fields you want validated. Use string access like 'address.state' to reference the fields.
Then use .issues() to get issues by field path the same way. This returns an array of strings or null. So you can easily use it as a check for styling like class:border-red-500={valid.issues('address.state')}
<input
{...remoteForm.fields.address.state.as('text')}
{...valid.fields('address.state')}
class:border-red-500={valid.issues('address.state')}
/>
{#if valid.issues('address.state')}
{#each valid.issues('address.state') as issue}
<p>{issue}</p>
{/each}
{/if}Details
There is currently a lot of change with svelte's remote functions and with form validation. We use a very newly added preflightOnly flag for validation calls to avoid making calls to the server to validate on every keystroke however there seem to be bugs in the current implementation including that we don't get server issues until you submit the form (we should be getting them on blur).
We also have some buggy behavior after a submit if the server sends back validation issues (and doesn't run the remote function), we do show those issues, however they currently all get cleared when you mutate the form. This is not desired. We want to clear only the issues for the field that mutates after server sends back issues.
For these to update you need to call valid.updateIssues() in your form enhance handler when there is no error but also no result. And you need to call valid.validateAll() in your catch block. (Or use alongside createEnhancedForm to get this automatically).
createEnhancedForm
Creates tracked state for the remote form and also semantic callbacks for stages of the request.
This allows you to add callbacks a little more easily (more like superforms) and allows this utility to take care of some things for you. It also exposes state of the form including delayed and timeout. And you can pass in your validation instance from createValidation for the proper calls be added in the callbacks.
Usage
import { createEnhancedForm } from '@opensky/remotes'
const form = createEnhancedForm(remoteForm, {
validation: valid,
delayMs: 500,
timeoutMs: 3500
})Returns an object with:
enhance(params, callbacks)- Form enhancement handlerreset()- Resets the form state back to 'idle'state- Current form state (type-safe based on creation options)idle,pending,issues,error,result- Boolean getters (always available)delayed- Boolean getter (only available ifdelayMswas provided)timeout- Boolean getter (only available iftimeoutMswas provided)
Creation Options:
validation?- Optional validation instance fromcreateValidationdelayMs?- Milliseconds to wait before transitioning to 'delayed' statetimeoutMs?- Milliseconds to wait before transitioning to 'timeout' state
Callbacks (all optional):
onSubmit- Called when form submission begins. Receivescancel()andupdates()functions:cancel(state?)- Cancel submission and set state to 'idle' (default), 'error', or 'issues'updates(...queries)- Provide queries/overrides for optimistic updates viasubmit().updates(...)
onDelay- Called when delayed state is reached (only allowed ifdelayMsis set)onTimeout- Called when timeout state is reached (only allowed iftimeoutMsis set)onReturn- Called when form submission returns successfullyonIssues- Called when form submission returns with validation issuesonError- Called when form submission encounters an error
Use with the remote form's enhance method:
<form {...remoteForm.preflight(schema).enhance(opts =>
form.enhance(opts, {
onSubmit: ({ cancel, updates, data }) => {
// Custom client-side checks before submission
if (!customValidationCheck(data)) {
valid.addIssue('fieldName', 'Custom validation failed')
cancel('issues') // Cancel and set state to 'issues'
return
}
// Optimistic updates
updates(getPosts().withOverride((posts) => [newPost, ...posts]))
},
onDelay: () => {}, // Only allowed if delayMs was set at creation
onTimeout: () => {}, // Only allowed if timeoutMs was set at creation
onReturn: ({ result }) => {},
onIssues: () => {},
onError: ({ error }) => {}
})
)}>
<!-- form fields -->
<button disabled={form.pending || form.delayed}>
{form.delayed ? 'Loading...' : 'Submit'}
</button>
</form>
{#if form.timeout}
<p>Request timed out</p>
{/if}Example
Example of usage of both createValidation and createEnhancedForm
<script lang="ts">
import { createValidation, createEnhancedForm } from '@opensky/remotes'
import { myForm } from './myForm.remote'
import { z } from 'zod'
const schema = z.object({
name: z.string().min(4, 'Too short').max(10, 'Too long'),
address: z.object({
state: z.string()
})
})
const valid = createValidation(myForm)
const form = createEnhancedForm(myForm, {
validation: valid,
delayMs: 500,
timeoutMs: 3500
})
</script>
<p>State: {form.state}</p>
<form {...myForm.preflight(schema).enhance(opts =>
form.enhance(opts, {
onDelay: () => console.log('showing loader'),
onTimeout: () => console.log('request timeout'),
onReturn: ({ result }) => console.log('success', result)
})
)}>
<input
{...myForm.fields.name.as('text')}
{...valid.fields('name')}
class:error={valid.issues('name')}
/>
{#if valid.issues('name')}
{#each valid.issues('name') as issue}
<p class="error">{issue}</p>
{/each}
{/if}
<input
{...myForm.fields.address.state.as('text')}
{...valid.fields('address.state')}
class:error={valid.issues('address.state')}
/>
<button disabled={form.pending || form.delayed}>
{form.delayed ? 'Loading...' : 'Submit'}
</button>
</form>
{#if form.timeout}
<p>Request timed out</p>
{/if}