Skip to content

open-sky-dev/opensky-remotes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 

Repository files navigation

@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) - Returns onblur and oninput handlers for a field path
  • issues(path: string) - Returns validation issues for a field path
  • allIssues() - Returns all validation issues
  • addIssue(path: string, issue: string) - Adds a custom validation error to a field
  • reset() - Clears all validation issues
  • validateAll() - 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 handler
  • reset() - 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 if delayMs was provided)
  • timeout - Boolean getter (only available if timeoutMs was provided)

Creation Options:

  • validation? - Optional validation instance from createValidation
  • delayMs? - Milliseconds to wait before transitioning to 'delayed' state
  • timeoutMs? - Milliseconds to wait before transitioning to 'timeout' state

Callbacks (all optional):

  • onSubmit - Called when form submission begins. Receives cancel() and updates() functions:
    • cancel(state?) - Cancel submission and set state to 'idle' (default), 'error', or 'issues'
    • updates(...queries) - Provide queries/overrides for optimistic updates via submit().updates(...)
  • onDelay - Called when delayed state is reached (only allowed if delayMs is set)
  • onTimeout - Called when timeout state is reached (only allowed if timeoutMs is set)
  • onReturn - Called when form submission returns successfully
  • onIssues - Called when form submission returns with validation issues
  • onError - 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}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •