Skip to content

Latest commit

 

History

History
480 lines (365 loc) · 16.6 KB

File metadata and controls

480 lines (365 loc) · 16.6 KB

Lesson 5: Form Validation

In this lesson, we will implement comprehensive form validation across all three steps of the trip logging process (Start Trip, Catch Log, and End Trip). This ensures users provide accurate, complete information before proceeding to each subsequent step. We'll add validation logic using React state management and display user-friendly error messages using USWDS ErrorMessage components.

Step 1: Understanding Form Validation in RADFish Applications

Before implementing validation, let's understand the validation patterns used in RADFish applications and how they integrate with React state management.

1.1: Validation Architecture

Form validation in RADFish applications follows a consistent pattern across all form components:

  1. Validation Functions: Pure functions that check field values against business rules
  2. Error State Management: React state to store and display validation errors
  3. Submission Prevention: Block navigation/submission when validation fails
  4. User Experience: Clear, accessible error messages using USWDS components

1.2: Validation Types

Our trip logging application uses several validation types:

  • Required Field Validation: Ensures essential fields are not empty
  • Data Type Validation: Validates numbers, coordinates, and other specific formats
  • Range Validation: Ensures numeric values fall within acceptable bounds
  • Business Logic Validation: Custom rules specific to fishing trip data

Key Validation Concepts:

  • Client-Side Validation: Immediate feedback without server round-trips
  • Accessible Error Messages: Screen reader compatible with proper ARIA attributes
  • Form State Management: Coordinating validation with React's controlled components
  • Progressive Enhancement: Validation works even if JavaScript fails

Step 2: Start Trip Form Validation

Let's begin by implementing validation for the Start Trip form, which collects the trip date, start time, and weather conditions.

2.1: Set Validation Error State

The Start Trip form uses React state to store validation errors. When the form is submitted, we need to validate all fields and update the error state.

Open src/pages/StartTrip.jsx and locate the handleSubmit function.

const newErrors = validateForm();
  const handleSubmit = async (e) => {
    e.preventDefault(); // Prevent default browser form submission
    setSubmitted(true); // Mark form as submitted to show errors

    //diff-add-start
    const newErrors = validateForm();
    setErrors(newErrors);
    //diff-add-end

Explanation:

  • validateForm(): Calls the validation function that checks all form fields
  • setErrors(newErrors): Updates React state with the validation results
  • The form only proceeds if Object.keys(newErrors).length === 0

2.2: Required Field Validation Logic

The Start Trip form validates three required fields using a standardized validation helper:

const validateRequired = (value, fieldName) => {
  if (!value || String(value).trim() === "") {
    return `${fieldName} is required`;
  }
  return null;
};

// Validates all fields in the form
const validateForm = () => {
  const newErrors = {};

  const dateError = validateRequired(formData.tripDate, FIELD_DATE);
  if (dateError) newErrors.tripDate = dateError;

  const weatherError = validateRequired(formData.weather, FIELD_WEATHER);
  if (weatherError) newErrors.weather = weatherError;

  const timeError = validateRequired(formData.startTime, FIELD_START_TIME);
  if (timeError) newErrors.startTime = timeError;

  return newErrors;
};

Understanding the Validation Pattern:

  • Helper Function: validateRequired() provides reusable validation logic
  • Field Constants: FIELD_DATE, FIELD_WEATHER, etc. ensure consistent error messages
  • Error Object: Returned object maps field names to error messages
  • Null Returns: Valid fields return null to indicate no errors

2.3: Display Error Messages

The Start Trip form displays validation errors using USWDS ErrorMessage components. Each form field has an associated error display:

//diff-remove-start
<FormGroup>
//diff-remove-end
//diff-add-start
<FormGroup error={submitted && errors.tripDate}>
//diff-add-end
  <Label
    htmlFor="tripDate"
    //diff-add-start
    error={submitted && errors.tripDate}
    //diff-add-end
    hint=" mm/dd/yyyy"
    requiredMarker
  >
    Date
  </Label>
  <DatePicker
    id="tripDate"
    name="tripDate"
    defaultValue={formData.tripDate}
    onChange={handleDateChange}
    //diff-add-start
    validationStatus={submitted && errors.tripDate ? "error" : undefined}
    //diff-add-end
    aria-describedby="trip-date-hint trip-date-error-message"
    required
  />
  <span id="trip-date-hint" className="usa-sr-only">
    Please enter or select the date of your fishing trip.
  </span>
  //diff-add-start
  {submitted && errors.tripDate && (
         <ErrorMessage id="trip-date-error-message" className="font-sans-2xs">
      {errors.tripDate}
    </ErrorMessage>
  )}
  //diff-add-end
</FormGroup>

Understanding the Error Display Pattern:

The diff shows the transformation from a basic form group to a fully validated one:

  • FormGroup Error State: error={submitted && errors.tripDate} applies USWDS error styling to the entire form group
  • Label Error Integration: error={submitted && errors.tripDate} on the Label component provides visual error indication
  • Input Validation Status: validationStatus={submitted && errors.tripDate ? "error" : undefined} adds error styling to the DatePicker
  • ARIA Accessibility: aria-describedby="trip-date-hint trip-date-error-message" links the input to both hint text and error messages
  • Conditional Error Messages: {submitted && errors.tripDate && (...)} only displays errors after form submission
  • Consistent ID Naming: id="trip-date-error-message" follows a consistent kebab-case pattern for error message identification

This pattern ensures errors are visually prominent, accessible to screen readers, and only appear when users attempt to submit invalid data.

Step 3: Catch Log Form Validation

The Catch Log page presents a more complex validation scenario with both a "new catch" form and a list of existing catches that can be edited.

3.1: New Catch Validation

The "Add Catch" form validates species, weight, length, time, and optional coordinates before adding to the list.

Open src/pages/CatchLog.jsx and locate the handleAddCatch function. Observe how validation is implemented:

const handleAddCatch = async (e) => {
  e.preventDefault();
  setSubmitted(true);

  //diff-add-start
  const formErrors = validateForm();
  setErrors(formErrors);
  //diff-add-end

  // Proceed only if no errors and tripId exists
  if (Object.keys(formErrors).length === 0 && tripId) {
    // Save catch and update UI
  }
};

Complex Validation Rules:

The Catch Log uses multiple validation types beyond simple required field checks:

const validateNumberRange = (value, min, max, fieldName, allowZero = true) => {
  if (value === "" || value === null || value === undefined) return null;
  const numValue = Number(value);
  if (isNaN(numValue)) return `${fieldName} must be a valid number`;
  if (!allowZero && numValue <= min)
    return `${fieldName} must be greater than ${min}`;
  if (allowZero && numValue < min)
    return `${fieldName} must be at least ${min}`;
  if (numValue > max) {
    const minOperator = allowZero ? ">=" : ">";
    return `${fieldName} must be ${minOperator} ${min} and <= ${max}`;
  }
  return null;
};

const validateLatitude = (value) => {
  if (value === "" || value === null || value === undefined) return null;
  const numValue = Number(value);
  if (isNaN(numValue)) return `${FIELD_LATITUDE} must be a valid number`;
  if (numValue < -90 || numValue > 90)
    return `${FIELD_LATITUDE} must be between -90 and 90`;
  return null;
};

Advanced Validation Features:

  • Range Validation: Weight (0-1000 lbs) and length (0-500 inches) must be within realistic bounds
  • Coordinate Validation: Latitude (-90 to 90) and longitude (-180 to 180) follow geographic standards
  • Optional Field Handling: Coordinates are validated only if values are provided
  • Type Coercion: Automatic conversion from string inputs to numbers

3.2: Recorded Catches Validation

Before navigating to the End Trip page, all catches in the "Recorded Catches" list must be validated:

const handleSubmit = async (e) => {
  e.preventDefault();

  //diff-add-start
  const recordedErrors = validateRecordedCatches();
  setRecordedCatchErrors(recordedErrors);
  //diff-add-end

  // Only proceed if there are no errors in the recorded catches list
  if (Object.keys(recordedErrors).length === 0) {
    // Navigate to EndTrip page
    navigate(`/end`, { state: { tripId: tripId } });
  }
};

Multi-Record Validation:

const validateRecordedCatches = () => {
  const allErrors = {};
  catches.forEach((catchItem, index) => {
    const catchErrors = {};
    // Validate each catch using the same rules as new catches
    catchErrors.species = validateRequired(catchItem.species, FIELD_SPECIES);
    catchErrors.weight = validateRequired(catchItem.weight, FIELD_WEIGHT);
    // ... additional validations

    // Only store errors if they exist
    const filteredCatchErrors = Object.entries(catchErrors).reduce(
      (acc, [key, value]) => {
        if (value) acc[key] = value;
        return acc;
      },
      {},
    );

    if (Object.keys(filteredCatchErrors).length > 0) {
      allErrors[index] = filteredCatchErrors;
    }
  });
  return allErrors;
};

3.3: Error Display for Recorded Catches

The recorded catches list displays errors with index-specific identifiers:

catches.map((catchItem, index) => {
  const catchErrors = recordedCatchErrors[index] || {};

  return (
    <div key={catchItem.id || index}>
      //diff-remove-start
      <FormGroup>
      //diff-remove-end
      //diff-add-start
      <FormGroup error={!!catchErrors.species}>
      //diff-add-end
        <Label
          htmlFor={`recorded-species-${index}`}
          //diff-add-start
          error={!!catchErrors.species}
          //diff-add-end
        >
          Species
          <span className="text-secondary-vivid margin-left-05">*</span>
        </Label>
        <Select
          id={`recorded-species-${index}`}
          //diff-add-start
          validationStatus={catchErrors.species ? "error" : undefined}
          aria-describedby={`species-hint recorded-species-${index}-error-message`}
          //diff-add-end
          // ... other props
        >
          {/* options */}
        </Select>
        <span id="species-hint" className="usa-sr-only">
          Please select the species of the catch.
        </span>
        //diff-add-start
        {catchErrors.species && (
          <ErrorMessage id={`recorded-species-${index}-error-message`}>
            {catchErrors.species}
          </ErrorMessage>
        )}
        //diff-add-end
      </FormGroup>
    </div>
  );
})

Step 4: Apply Validation to End Trip (Practice Exercise)

Now it's your turn! Apply the validation patterns you've learned to the End Trip form. This form needs validation for the trip's end time and weather conditions before allowing users to review their complete trip data.

However, before we can add validation, we need to complete the End Trip form implementation since it's currently missing the required fields and components.

4.1: Update the Data Model

First, you need to enable the endTime and endWeather fields in the data model.

Your task is to add the endTime and endWeather fields to the trip model.

:::info Hint Models are defined in src/index.jsx. :::

:::warning Multi-Step Form Consideration Since this is a multi-step form, you need to handle the fact that endTime and endWeather won't be available when the trip is first created in the StartTrip form.

The Problem: If you make these fields required: true in the model, the StartTrip form will fail validation when trying to create the initial trip record because these fields don't exist yet.

The Solutions:

  1. Option 1: Make the fields optional in the model (required: false)
  2. Option 2: Set them to empty strings when creating the initial trip

Recommended Approach: Use Option 2 and update the Form.create():

// Look for the Form.create call around line 241 and update it to:
await Form.create({ 
  id: newTripId, 
  ...tripData, 
  //diff-add-start
  endTime: "", 
  endWeather: "" 
  //diff-add-end
});

This allows the trip to be created successfully at the start, and these fields will be populated later in the EndTrip form. :::

4.2: Import Required Components

Open src/pages/EndTrip.jsx and add the missing component imports.

Your task: Add the necessary imports at the top of the file.

Components you'll need: :::info Hint
Look at src/pages/StartTrip.jsx to see what components are needed for:

  • Time input field
  • Weather dropdown
  • Error message display :::

4.3: Add Form Fields to the Component

The End Trip form currently only has navigation buttons. You need to add the actual form fields.

Fields to add:

  1. End Time field - similar to the startTime field in Start Trip
  2. End Weather field - similar to the weather field in Start Trip

:::info Hint You'll need:

  • FormGroup with proper error state
  • Label with required marker and error state
  • Input component with validation status
  • Conditional ErrorMessage component :::

4.4: Add Form State Management

You'll need to add state management for the form data and handle changes.

:::info Hint You'll need to add the following:

  • formData state for storing field values
  • handleTimeChange function for time picker
  • handleSelectChange function for dropdowns
  • Form initialization from database/localStorage :::

4.5: Implement Validation Logic

Finally, add the validation patterns you learned from the previous forms.

For validation functions:

:::info Hint

  • Copy the validateRequired helper from src/pages/StartTrip.jsx
  • Add validateForm function that checks both endTime and endWeather
  • Use the same field constants pattern (FIELD_END_TIME, FIELD_END_WEATHER) :::

For setting validation errors:

:::info Hint

  • Use the same pattern as Start Trip
  • const newErrors = validateForm(); :::

For displaying error messages:

:::info Hint

  • Follow the same pattern as previous forms
  • Remember to:
    • Add error prop to FormGroup
    • Add error prop to Label
    • Add validationStatus to the input component
    • Add conditional ErrorMessage component
    • Use proper aria-describedby for accessibility :::

Step 5: Testing Complete Validation Flow

Test the validation system with both invalid and valid data to ensure it works correctly:

5.1: Complete Validation Test

  1. Start Trip (/start):

    • Try submitting empty form → verify error messages appear
    • Fill fields correctly → proceeds to Catch Log
  2. Catch Log (/catch):

    • Try "Add Catch" with empty fields → verify validation errors
    • Test invalid coordinates (latitude > 90) → verify range validation
    • Add valid catches → try "Next" with invalid recorded catches → verify list validation
    • Ensure all catches are valid → proceeds to End Trip
  3. End Trip (/end):

    • Try submitting without end time/weather → verify validation blocks navigation
    • Fill fields correctly → proceeds to Review page

Expected Results:

  • ✅ Errors appear only after submission attempts
  • ✅ Invalid data blocks navigation between steps
  • ✅ Error styling applied consistently across all forms
  • ✅ Valid data clears errors and allows progression

Conclusion

You have successfully implemented comprehensive form validation across all three steps of the trip logging process!

Key Benefits:

  • Data quality: Ensures complete, accurate trip information
  • User experience: Clear guidance when data doesn't meet requirements
  • Accessibility: Screen reader compatible with proper labeling
  • Progressive enhancement: Validation works even if JavaScript fails

In the next lesson, we'll implement the review page where users can see all their trip data before final submission.