Forms

Conform Day Schedule

The quebi Day Schedule wired to Conform. Submits its spans as JSON through a hidden input and derives name, required, and validity from the field metadata.

formconformscheduletimerangevalidation

Bound to a Conform form

The spans submit as JSON. Drag the workshop span longer to push the day past 8 hours and see the validation error.

focus
standup
workshop

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import { useState } from "react"
import type { FieldMetadata } from "@conform-to/react"
import { cn } from "@/lib/utils"
import { DaySchedule, type DaySpan, type DayScheduleProps } from "@/components/day-schedule"
import { Label } from "@/components/field"

/**
 * ConformDaySchedule — DaySchedule wired to Conform.
 *
 * The schedule's value is an array of spans, so it is submitted as a JSON
 * string through a hidden input named after the field. Validate it with a
 * valibot schema that parses the JSON (see the example), and the usual
 * name/default/required/errors are read off the field metadata.
 */

interface ConformDayScheduleProps
  extends Omit<DayScheduleProps, "spans" | "defaultSpans" | "onSpansChange"> {
  // Only name/initialValue/required/errors are read off the metadata; the
  // serialized value is a JSON string regardless of the schema's output type.
  field: FieldMetadata<any, any, string[]>
  label?: string
  /** Used when the field has no initial value. */
  defaultSpans?: DaySpan[]
}

/** Parse the field's initial value, tolerating an absent or malformed payload. */
function parseInitialSpans(initialValue: unknown, fallback: DaySpan[]): DaySpan[] {
  if (typeof initialValue !== "string" || initialValue.length === 0) return fallback
  try {
    const parsed = JSON.parse(initialValue)
    return Array.isArray(parsed) ? (parsed as DaySpan[]) : fallback
  } catch {
    return fallback
  }
}

export function ConformDaySchedule({
  field,
  label,
  defaultSpans = [],
  className,
  ...props
}: ConformDayScheduleProps) {
  const [spans, setSpans] = useState<DaySpan[]>(() =>
    parseInitialSpans(field.initialValue, defaultSpans),
  )
  const hasErrors = !field.valid && !!field.errors

  return (
    <div className="flex flex-col gap-2">
      {label && (
        <Label htmlFor={field.id} className="text-sm text-quebi-fg">
          {label}
          {field.required && <span className="ml-0.5 text-quebi-brand">*</span>}
        </Label>
      )}

      {/* The submitted value. Kept in sync with the interactive schedule below. */}
      <input type="hidden" id={field.id} name={field.name} value={JSON.stringify(spans)} />

      <div
        className={cn(
          "rounded-quebi-md border p-4 transition-colors duration-150",
          hasErrors ? "border-red-500" : "border-quebi-line/10",
        )}
      >
        <DaySchedule
          {...props}
          spans={spans}
          onSpansChange={setSpans}
          aria-invalid={hasErrors || undefined}
          aria-describedby={hasErrors ? field.errorId : undefined}
          className={className}
        />
      </div>

      {hasErrors && (
        <p id={field.errorId} className="text-sm text-red-500">
          {field.errors?.join(", ")}
        </p>
      )}
    </div>
  )
}