Files
web/packages/tracking/lib/useFormTracking.ts
Anton Gunnarsson 66b7af877a Merged in fix/loy-514-fix-validation-tracking-in-signup-form (pull request #3445)
fix(LOY-514): Fix validation error tracking in SignupForm

* Fix issue with form submit handling

* Fix

* Remove browser validation

* Add automatic tracking of validatione rrors


Approved-by: Rasmus Langvad
Approved-by: Matilda Landström
2026-01-19 08:34:24 +00:00

110 lines
2.9 KiB
TypeScript

"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import {
type Control,
type FieldValues,
useFormState,
type UseFormSubscribe,
} from "react-hook-form"
import {
type FormType,
trackFormAbandonment,
trackFormCompletion,
trackFormInputStarted,
trackFormValidationError,
} from "./form"
import type { FieldErrors } from "react-hook-form"
export function useFormTracking<T extends FieldValues>(
formType: FormType,
subscribe: UseFormSubscribe<T>,
control: Control<T>,
nameSuffix: string = ""
) {
const [formStarted, setFormStarted] = useState(false)
const lastAccessedField = useRef<string | undefined>(undefined)
const formState = useFormState({ control })
useEffect(() => {
const unsubscribe = subscribe({
formState: { dirtyFields: true },
callback: (data) => {
if ("name" in data) {
lastAccessedField.current = data.name as string
}
if (!formStarted) {
trackFormInputStarted(formType, nameSuffix, lastAccessedField.current)
setFormStarted(true)
}
},
})
return () => unsubscribe()
}, [subscribe, formType, nameSuffix, formStarted])
useEffect(() => {
if (!formStarted || !lastAccessedField.current || formState.isValid) return
const lastField = lastAccessedField.current
function handleBeforeUnload() {
trackFormAbandonment(formType, lastField, nameSuffix)
}
function handleVisibilityChange() {
if (document.visibilityState === "hidden") {
trackFormAbandonment(formType, lastField, nameSuffix)
}
}
window.addEventListener("beforeunload", handleBeforeUnload)
window.addEventListener("visibilitychange", handleVisibilityChange)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
window.removeEventListener("visibilitychange", handleVisibilityChange)
}
}, [formStarted, formType, nameSuffix, formState.isValid])
useEffect(() => {
if (formState.submitCount === 0) return
if (formState.isValid) return
trackErrors(formState.errors, formType, nameSuffix)
}, [formState.submitCount, formState.errors, formType, nameSuffix])
const trackFormSubmit = useCallback(() => {
if (formState.isValid) {
trackFormCompletion(formType, nameSuffix)
}
}, [formType, nameSuffix, formState.isValid])
return {
trackFormSubmit,
}
}
function trackErrors<T extends FieldValues>(
errors: FieldErrors<T>,
formType: FormType,
nameSuffix: string
) {
const errorKeys = Object.getOwnPropertyNames(errors)
errorKeys.forEach((key) => {
const msg = errors[key]?.message
if (!msg) {
// Handle nested errors
trackErrors(errors[key] as FieldErrors<T>, formType, nameSuffix)
return
}
if (!msg || typeof msg !== "string") return
trackFormValidationError(formType, nameSuffix, msg)
})
}