DEV Community

MartinJ
MartinJ

Posted on • Updated on

2.5 Creating a Serious Svelte Information System (b): Form validation and Exception handling

To create a new post x.y, start with "ngatesystems-post-series-v2-xpy" as your title and save this with /empty/ content. The important thing is not

to put your -- title -- into the post until you've got the URL initialised with your first save.

Introduction

Thus far, this series has concentrated on basic functionality. This post considers the issues you must consider to make your webapp fit to be launched into an eager world.

For example, when an input form is used to create or update a Firestore document it is assumed that the data is valid and compatible with existing records. It also assumes that the web and your Firestore server are both working. Your webapp will be judged harshly if it doesn't provide a cushion to handle or mitigate these issues. This post will describe how to handle both anticipated and unanticipated exceptions.

Similarly, in post ??, a Firestore collection was created that could be read and written by anyone with access to the webapp. Since this will be generally undesirable, this post will describe how you can arrange to:

  • provide read-only access
  • ensure that only "authorised" individuals who can identify themselves with a user-id/password combination can read or write documents.

Form validation

Form validation is one of the most fiddly development tasks - but also one where you must be absolutely on top of your game. Poor design of the form and its error-signalling arrangements will upset your users, and inadequate filtering of error conditions will upset your database.

With the default svelte +page.svelte/+page.server.js arrangement your validation arrangements will be spread across both client and server. On the client, you can use the browser's interactive facilities to deliver excellent UX (user experience) for the data-capture phase of the process. On the server, you can use its secure environment to assure yourself that the data really is valid and complete the database update in complete privacy. This might seem paranoid, but client code is not secure and a determined hacker could bypass rules that you had coded here.

Here's an example of the sort of UX you might build on the client

Let's say you have a simple form that asks for input to a field that should contain only numeric characters. A popular design arrangement would then deploy this alongside a "submit" button styled initially as "disabled" - conventionally through a pale background.

As soon as valid input is detected, the input field's colour will revert to standard and the submit button will be enabled. But, if a non-numeric field is entered, the position will be reversed: the submit button will revert to its disabled state and the input field will display an error state again. This code be coded in many different ways, but here's one solution that works quite well for me:

src/routes/form_validation/page.svelte

<script>

    import { inputFieldIsNumeric } from "$lib/utilities/inputFieldIsNumeric";

    let inputField;

    let inputFieldClass = "inputField";
    let submitButtonClass = "submitButton";

    export let form;
</script>

<!-- display the <form> only if it has not yet been submitted-->
{#if form === null}
    <form method="POST">
        <label>
            Numeric Input Field
            <input
                bind:this={inputField}
                name="inputField"
                class={inputFieldClass}
                on:input={() => {
                    if (inputFieldIsNumeric(inputField.value)) {
                        submitButtonClass = "submitButton validForm";
                        inputFieldClass = "inputField";
                    } else {
                        submitButtonClass = "submitButton error";
                        inputFieldClass = "inputField error";
                    }
                }}
            />
        </label>

        {#if inputFieldClass === "inputField error"}
            <span class="error">Invalid input. Please enter a number.</span>
        {/if}

        <button class={submitButtonClass}>Submit</button>
    </form>
{:else if form?.success}
    <p>Form submitted successfully.</p>
{:else}
    <p>Form submission failed!</p>
{/if}

<style>
    .inputField {
        border: 1px solid black;
        height: 1rem;
        width: 5rem;
        margin: auto;
    }

    .submitButton {
        display: block;
        margin-top: 1rem;
    }

    .error {
        color: red;
    }

    .validForm {
        background: palegreen;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

src/lib/utilities/inputFieldIsNumeric.js

export function inputFieldIsNumeric(valueString) {
    // returns true if "value" contains only numeric characters, false otherwise

    if (valueString.length === 0) return false;
    for (let char of valueString) {
        if (char < '0' || char > '9') {
            return false;
        }
    }
    return true;
};
Enter fullscreen mode Exit fullscreen mode

src/routes/form_validation/+page.server.js

import { inputFieldIsNumeric } from "$lib/utilities/inputFieldIsNumeric";

export const actions = {
    default: async ({ request }) => {
        const input = await request.formData();
        const inputField = input.get("inputField");

        const validationResult =  inputFieldIsNumeric(inputField);

        return { success : validationResult};
    }

};
Enter fullscreen mode Exit fullscreen mode

The +page.svelte and +page.server.js files defined here use a shared inputFieldIsNumeric.js utility function to check input both on the client and on the server. If you start your dev server and direct the browser to http://localhost:5173/form_validation you should see an input form displayed as below:

Try entering a number. Note how the "submit" button turns green to indicate that the client side of the webapp is happy with your input. Click the button. The webapp should respond with a "Form submitted successfully." message.

Now try to enter a letter. The webapp should display an error message and leave the button disabled. It won't let you proceed further until you correct the error.

The +page.svelte code introduces some new features so I'll walk you through it.

The inputFieldIsNumeric function that performs the validation is referenced by both +page.svelte and +page.server.js. To avoid duplicating the code in each of these files, it makes sense to share it by placing it in a utility file. The src/lib/utilities/inputFieldIsNumeric.js file exports an inputFieldIsNumeric function that can be imported wherever required.

The "code template" section of +page.server needs to maintain a state variable to describe the valid/invalid condition of the inputField variable. A flag field with values "valid" or "invalid" might have been used here, but a trick defining the state as display styles provides a neater solution.

When the field is invalid we want its <input> tag to turn red. Previously, you've seen this done by adding a style="color: red" qualifier. In this case, because the <input> needs several styles, I've used an inputField class qualifier declared in the <style> section of the template. A valid <input> field can then be displayed as <input class="inputField" and an erroredfield asfield display as follows:
javascript
<input
bind:this={inputField}
name="inputField"
class={inputFieldClass}

Where you need to see if the "Numeric Input Field" is in an error state (to decide whether you need to display an error message) you simply check if it has been given an error style, as follows:
javascript
{#if inputFieldClass === "inputField error"}
`

Moving on, the export let form; statement both declares a form variable and marks it as a "prop" or "property" of +page.svelte". This means it can be set outside the component - specifically by the "actions" function in the associated +page.server.js file. This is where our server-side validation will be performed and we can now use the prop to return the result. This is, of course, identical to the export let data pattern used in [post 2.3]() to return data from the load() function in a +page.server.js` file.

A little further into +page.svelte, the {#if form === null} statement displays the input form only when it is still in its empty, initial state. Once submitted and validated, the code is designed to display only the validation result - either a success or a failure message. In neither case is there any requirement for the form.

Finally, at {:else if form.success} you can see how, triggered by the return of the state form variable, the template section of +page.js re-renders and uses the form.success property to deliver its success/failure report.

In this case, since the server-side validation can't fail, (since the input was already validated client-side), you'll only see a "Form submitted successfully" message. But in the next section, you'll see how code can fail for other reasons.

Exception handling

Supposing the validation code involved checking input against database content, could client-side and server-side validation reach different conclusions in such a case? Again, unlikely, but not impossible. The reason is that database activity introduces a much greater scope for unpredictable outcomes. For example, you have to consider the possibility of "exceptions" such as network or hardware failures.

When stakes are high enough, software must include "contingency" management arrangements to ensure that users aren't disenchanted by unpredictable behaviour.

Of course, you can't stop these problems from happening, but what you can do is make sure that the consequences are managed gracefully - users are informed of the situation and any necessary clean-up actions taken.

Your questions now should be "How do you know an error has occurred and how do you find out what type of error it is?"

Library functions like the Firestore API calls you used earlier to read and write documents to database collections signal that something has gone wrong by "throwing" an "exception". In Javascript, you can do this by using a "throw" statement:

throw new Error("Structured error message");
Enter fullscreen mode Exit fullscreen mode

A "throw" statement terminates the program and displays a system error message. This is generally unwelcome! However, this situation can be avoided by taking some simple preliminary precautions.

Javascript provides a fundamental arrangement called a "catch .. try" block and it looks like this:

try {
  blockOfVulnerableCode;
} catch (error) {
 handleError
}
Enter fullscreen mode Exit fullscreen mode

This says "Try to run this "blockOfVulnerableCode" and pass control the catch block if anything goes wrong". If the catch block is called, its error parameter will receive the "Structured error message" registered by the exception. The "handleError" code can then inspect this and close the situation down appropriately.

Obviously, it only makes sense to use "try" bocks when there is serious concern that code may fail, but database i/o would certainly be a candidate. Just to be clear about the arrangement, here's the Firestore "register product" code from post 2.3 rigged with a "try" block. With this in place, should any of the API calls error, control will be passed to the catch block where a soft-landing can be engineered.

export const actions = {
    default: async ({ request }) => {
        try {
            const input = await request.formData();
            const newProductNumber = input.get("newProductNumber");
            const productsDocData = { productNumber: newProductNumber }
            const productsCollRef = collection(db, "products");
            const productsDocRef = doc(productsCollRef);
            await setDoc(productsDocRef, productsDocData);
            return { success: true };
        } catch (error) {
            //Email the system administrator with the error message. A firestore error return is an object
            // with a standard structure : eg {code: "permission-denied", message: "Missing or insufficient
            // permissions."}
            return { success: false, error: error.message };
        }

    }
};
Enter fullscreen mode Exit fullscreen mode

Since you are working on the server at this point, you can't communicate directly with the user via a Javascript alert() call. But now that you've caught the error, program execution will continue as normal and back in +page.js you can, if appropriate, use the form.success value returned from the actions() function to trigger a popup and display a suitable message.

Summary

This has been a long post, but data validation and exception handling are serious issues. Good form design is appreciated highly by users and can make or break your system. I hope you've found the designs described here helpful.

What's still needed to complete your system? As things stand, all of your webapp pages are accessible by anyone that knows their URL. In practice, you'll want to restrict access to named individuals. What you need is a "logon" arrangement. This is covered in the next post.

Top comments (0)