Search code examples
typescriptformsperformancevalidationsveltekit

Is there a shorter / more concise way to validate fields in SvelteKit?


Currently, I am using a bunch of utility functions, as well as the SvelteKit $ operator to determine whether a form is valid or not as follows:

<script lang="ts">
    const required = (str: string) => str.length < 1;
    const length = (str: string, minLength: number, maxLength: number) => str.length < minLength || str.length >= maxLength;

    $: errors = {
        name: 
            required(fields.name) ? "Name is a required field." : 
            length(fields.name, 3, 5) ? "Must be between 3-5 chars." : ""
    }

    $: valid = !errors.name;

    const onSubmit = () => {
        if (valid) {
           // ...
        }
    }
</script>

My question is: Is this an efficient way of validating fields? If I have many fields, will a change re-calculate the whole errors object, or only the required field?


Solution

  • How about using native validation?

    <form onsubmit="event.preventDefault(); alert('submitted');">
      <input name="Name" required minlength=3 maxlength=5 />
      <button>Submit</button>
    </form>

    If you have anything more custom, there are also functions for that.

    • setCustomValidity - Specify any error message, clear via setting '', should be done on any input/change.
    • reportValidity - Manually report validation errors (exists for inputs and forms).
    • checkValidity - Get whether a form/input is valid without reporting the errors.

    There are also properties on the inputs which give you a message (validationMessage) or a detailed state with various flags that describe what is wrong (validity).

    To output a native validation message instead of the popups, you can use the invalid event. If your structure is rigid you can e.g. add an action to the input that automatically outputs the error in its next sibling:

    <script>
        function onSubmitClick(event) {
            const form = event.target.closest('form');
            const valid = form.checkValidity();
    
            if (valid == false) {
                event.preventDefault(); // Prevents error popups
                [...form].find(e => e.validity.valid == false)?.focus();
            }
        }
        
        function validationErrors(node) {
            const output = node.nextElementSibling;
            node.addEventListener('invalid', onInvalid);
            node.addEventListener('input', onInput);
            
            function onInvalid() {
                output.textContent = node.validationMessage;
            }
            
            function onInput() {
                if (node.validationMessage == '')
                    output.textContent = '';
            }
            
            return {
                destroy() {
                    node.removeEventListener('invalid', onInvalid);
                    node.removeEventListener('input', onInput);
                }
            }
        }
    </script>
    
    <form on:submit|preventDefault={() => alert('submitted')}>
        <input name="name" required minlength=3 maxlength=5 use:validationErrors />
        <div class="error"></div>
    
        <div><button on:click={onSubmitClick}>Submit</button></div>
    </form>
    

    REPL

    This only shows the errors when the user tries to submit the form which is what I would recommend. It is not polite to berate the user when they have not even finished writing.

    If the DOM structure is not as rigid, one could use bind:this to get element references or use something like data- attributes to associate the elements with each other.

    It may also be useful to extracts things to separate components. The action itself can be extracted to any JS file.