githubEdit

clipboard-list-checkValidation

Front-end validation acts as the first line of defense before server-side validation that occurs in domain workflows (see Domain Model β€” Guard clauses and Validation sections).

Shared validation rules

The key design decision is that validation rules are defined once in Domain.Types, alongside the types they protect, and consumed by both the server and the client (via Fable):

// Shopfoo.Domain.Types / Catalog.fs
[<RequireQualifiedAccess>]
module Product =
    module Guard =
        let SKU          = GuardCriteria.Create("SKU", required = true)
        let Name         = GuardCriteria.Create("Name", required = true, maxLength = 128)
        let BookSubtitle = GuardCriteria.Create("BookSubtitle", maxLength = 256)
        let Description  = GuardCriteria.Create("Description", maxLength = 512)
        let ImageUrl     = GuardCriteria.None

Each GuardCriteria bundles the constraints for a single field β€” Required, MinLength, MaxLength β€” so that both layers derive their behavior from the same source of truth, with zero rule duplication.

Field
Required
Max length
Notes

SKU

Yes

β€”

Validated server-side only (not editable in the form)

Name

Yes

128

Description

No

512

BookSubtitle

No

256

Books only

ImageUrl

β€”

β€”

GuardCriteria.None β€” broken-link detection handled separately

GuardProps: bridging criteria to React

On the client side, a GuardProps class (defined in UI.fs) turns a GuardCriteria into ready-to-use React properties:

An extension method makes the call site concise:

What each member produces

Member
Output
Visual effect

textRequired

" (Required)" text + optional text-error class

Label turns red when the field is empty

textCharCount

"len / maxLength" text + optional text-error class

Counter turns red when the limit is exceeded

validation

required, minLength, maxLength attributes + input-error class

DaisyUI input styling + HTML5 constraint validation

value

prop.value binding

Keeps the input in sync with the model

Usage pattern in form fieldsets

Every validated field in CatalogInfo.fs follows the same structure:

circle-info

Daisy.validator.input comes from Feliz.DaisyUIarrow-up-right, the F#/Fable binding for the DaisyUI Validatorarrow-up-right component. It wraps an <input> with the validator CSS class, enabling built-in error styling when HTML5 constraints are violated.

Image URL: custom invalid parameter

The ImageUrl field has no length or required constraints (GuardCriteria.None), but it supports broken-link detection via the optional invalid parameter:

The ImageUrl type carries a Broken flag alongside the URL:

In the image preview, the <img> element listens for the onError event and flips that flag via the Elmish dispatch:

This triggers a model update (ProductChanged), which re-renders the form. The GuardProps then picks up the Broken flag through its invalid parameter and applies the input-error class on the URL input β€” giving the user immediate visual feedback that the image could not be loaded: red solidlink-slash icon.

Read-only mode

The Fieldset class conditionally replaces editable props with read-only attributes depending on the user's catalog access level:

This ensures the same form renders in both edit and view modes without duplicating the layout.

Summary

Aspect
Detail

Single source of truth

Product.Guard.* in Domain.Types

Shared across layers

Same GuardCriteria used by server validation and client UI (via Fable)

Client bridge

GuardProps class turns criteria into React props

Visual feedback

Required indicator, character counter, DaisyUI error styling

HTML5 integration

required, minLength, maxLength attributes for native browser validation

Extensibility

Optional invalid parameter for custom checks (e.g. broken image URL)

Last updated