Skip to content

Commit

Permalink
Enhance survey form validation (#136)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohannaPeanut authored Dec 23, 2024
2 parents 3b48c7a + 1e495be commit a87c92b
Show file tree
Hide file tree
Showing 50 changed files with 713 additions and 286 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { blueButtonStyles } from "@/src/core/components/links"
import { AllowedSurveySlugs } from "@/src/survey-public/utils/allowedSurveySlugs"
import deleteTestSurveyResponses from "@/src/survey-responses/mutations/deleteTestSurveyResponses"
import { useMutation } from "@blitzjs/rpc"
import clsx from "clsx"
import { clsx } from "clsx"
import { useRouter } from "next/navigation"

type DeleteButtonProps = {
Expand Down
1 change: 1 addition & 0 deletions src/core/components/forms/LabeledCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const LabeledCheckbox = forwardRef<HTMLInputElement, LabeledCheckboxProps
>
<div className="flex h-5 items-center">
<input
aria-describedby={scope + "Hint"}
type="checkbox"
disabled={disabled || isSubmitting}
readOnly={readonly}
Expand Down
2 changes: 1 addition & 1 deletion src/survey-public/components/Email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Props = {
onClickMore: () => void
}

export const Email: React.FC<Props> = ({ email, onClickMore }) => {
export const Email = ({ email, onClickMore }: Props) => {
const { description, button, title, mailjetWidgetUrl, homeUrl } = email

return (
Expand Down
2 changes: 1 addition & 1 deletion src/survey-public/components/More.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Props = {
more: TMore
}

export const More: React.FC<Props> = ({ more, onClickMore, onClickFinish }) => {
export const More = ({ more, onClickMore, onClickFinish }: Props) => {
useAlertBeforeUnload()

const { title, description, questionText, buttons } = more
Expand Down
25 changes: 13 additions & 12 deletions src/survey-public/components/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { SurveyScreenHeader } from "@/src/survey-public/components/core/layout/SurveyScreenHeader"
import type { TPage } from "@/src/survey-public/components/types"
import { useFormContext } from "react-hook-form"
import { getFormfieldNamesByQuestions } from "../utils/getFormfieldNames"
import { Question } from "./Question"
import { SurveyP } from "./core/Text"
import { SurveyButtonWithAction } from "./core/buttons/SurveyButtonWithAction"
import { SurveyButtonWrapper } from "./core/buttons/SurveyButtonWrapper"
import { SurveyFormErrorsBox } from "./core/form/SurveyFormErrorsBox"

type Props = {
page: TPage
buttonActions: any
completed: boolean
}

export const Page = ({ page, buttonActions, completed }: Props) => {
export const Page = ({ page, buttonActions }: Props) => {
const {
formState: { errors, isSubmitting },
} = useFormContext()

if (!page) return null
const { id: pageId, title, description, questions, buttons } = page

const relevantQuestionNames = getFormfieldNamesByQuestions(questions!)

return (
<section>
<SurveyScreenHeader title={title.de} description={description.de} />
Expand All @@ -23,26 +30,20 @@ export const Page = ({ page, buttonActions, completed }: Props) => {
questions.map((question) => (
<Question className="mb-2" key={`${pageId}-${question.id}`} question={question} />
))}
<SurveyFormErrorsBox formErrors={errors} surveyPart="survey" />
<SurveyButtonWrapper>
{buttons?.map((button) => {
let disabled = false
if (["nextPage", "submit"].includes(button.onClick.action)) {
disabled = !completed
}
return (
<SurveyButtonWithAction
key={`${pageId}-${button.label.de}`}
buttonActions={buttonActions}
relevantQuestionNames={relevantQuestionNames!}
button={button}
disabled={disabled}
disabled={isSubmitting}
/>
)
})}
</SurveyButtonWrapper>
<SurveyP className="text-sm sm:text-sm">
* Pflichtfelder <br />
Um fortzufahren, bitte alle Pflichtfelder ausfüllen.
</SurveyP>
</section>
)
}
80 changes: 56 additions & 24 deletions src/survey-public/components/Question.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {
TFeedbackQuestion,
TQuestion,
TReadOnlyProps,
TSingleOrMultiResponseProps,
TTextProps,
TTextareaProps,
TTextfieldProps,
} from "@/src/survey-public/components/types"
import { getFormfieldName } from "../utils/getFormfieldNames"
import { SurveyH2 } from "./core/Text"
import { SurveyLabeledCheckboxGroup } from "./core/form/SurveyLabeledCheckboxGroup"
import { SurveyLabeledRadiobuttonGroup } from "./core/form/SurveyLabeledRadiobuttonGroup"
Expand All @@ -13,15 +16,17 @@ import { SurveyLabeledTextareaField } from "./core/form/SurveyLabeledTextareaFie

type TSingleOrMultuResponseComponentProps = {
id: number
component: TQuestion["component"] | TFeedbackQuestion["component"]
} & TSingleOrMultiResponseProps

const SingleResponseComponent: React.FC<TSingleOrMultuResponseComponentProps> = ({
const SingleResponseComponent = ({
id,
responses,
}) => (
component,
}: TSingleOrMultuResponseComponentProps) => (
<SurveyLabeledRadiobuttonGroup
items={responses.map((item) => ({
scope: `single-${id}`,
scope: getFormfieldName(component, id),
name: `${id}-${item.id}`,
label: item.text.de,
help: item?.help?.de,
Expand All @@ -30,58 +35,79 @@ const SingleResponseComponent: React.FC<TSingleOrMultuResponseComponentProps> =
/>
)

const MultipleResponseComponent: React.FC<TSingleOrMultuResponseComponentProps> = ({
const MultipleResponseComponent = ({
id,
responses,
}) => (
component,
}: TSingleOrMultuResponseComponentProps) => (
<SurveyLabeledCheckboxGroup
key={id}
items={responses.map((item) => ({
name: `multi-${id}-${item.id}`,
name: `${getFormfieldName(component, id)}-${item.id}`,
label: item.text.de,
help: item?.help?.de,
}))}
/>
)

type TTextResponseComponentProps = {
type TTextfieldResponseComponentProps = {
id: number
} & TTextProps
component: TQuestion["component"] | TFeedbackQuestion["component"]
} & TTextfieldProps

type TTextareaResponseComponentProps = {
id: number
component: TQuestion["component"] | TFeedbackQuestion["component"]
} & TTextareaProps

type TReadOnlyResponseComponentProps = {
id: number
queryId: string
} & TTextProps
component: TQuestion["component"] | TFeedbackQuestion["component"]
} & TReadOnlyProps

const TextResponseComponent: React.FC<TTextResponseComponentProps> = ({
const TextResponseComponent = ({
id,
placeholder,
caption,
maxLength,
}) => (
validation,
component,
}: TTextareaResponseComponentProps) => (
<>
<SurveyLabeledTextareaField
name={`text-${id}`}
name={getFormfieldName(component, id)}
label={""}
placeholder={placeholder?.de}
caption={caption?.de}
maxLength={maxLength}
maxLength={validation?.maxLength}
/>
</>
)

const TextFieldResponseComponent: React.FC<TTextResponseComponentProps> = ({ id, placeholder }) => (
const TextFieldResponseComponent = ({
id,
placeholder,
component,
}: TTextfieldResponseComponentProps) => (
<>
<SurveyLabeledTextField name={`text-${id}`} placeholder={placeholder?.de} label={""} />
<SurveyLabeledTextField
name={getFormfieldName(component, id)}
placeholder={placeholder?.de}
label={""}
/>
</>
)

const ReadOnlyResponseComponent: React.FC<TReadOnlyResponseComponentProps> = ({ id, queryId }) => (
const ReadOnlyResponseComponent = ({ id, queryId, component }: TReadOnlyResponseComponentProps) => (
<>
<SurveyLabeledReadOnlyTextField readOnly name={`text-${id}`} label={""} queryId={queryId} />
<SurveyLabeledReadOnlyTextField
readOnly
name={getFormfieldName(component, id)}
label={""}
queryId={queryId}
/>
</>
)

// TODO type
const CustomComponent = (props: any) => (
<div className="border-2 border-black bg-gray-200 p-1">
<code>
Expand All @@ -101,16 +127,22 @@ const components = {

type Props = { question: TQuestion | TFeedbackQuestion; className?: string }

export const Question: React.FC<Props> = ({ question, className }) => {
export const Question = ({ question, className }: Props) => {
const { id, help, label, component, props } = question
// @ts-expect-error
const Component = components[component] || null

// todo validation: atm multipleResponse is always optional - we have to change this in the future
// @ts-expect-error
const isOptional = component === "multipleResponse" || props?.validation?.optional
return (
<div className={className} key={id}>
<SurveyH2>{label.de} *</SurveyH2>
<SurveyH2>
{label.de} {isOptional && " (optional)"}
</SurveyH2>
{help && <div className="-mt-4 mb-6 text-sm text-gray-400">{help.de}</div>}
{/* @ts-ignore */}
{Component && <Component id={id} {...props} />}
{Component && <Component component={component} id={id} {...props} />}
</div>
)
}
2 changes: 1 addition & 1 deletion src/survey-public/components/Start.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { SurveyButtonWrapper } from "./core/buttons/SurveyButtonWrapper"

type Props = { onStartClick: () => void; startContent: React.ReactNode; disabled: boolean }

export const Start: React.FC<Props> = ({ onStartClick, startContent, disabled }) => {
export const Start = ({ onStartClick, startContent, disabled }: Props) => {
useAlertBeforeUnload()

return (
Expand Down
14 changes: 7 additions & 7 deletions src/survey-public/components/Survey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,26 @@ import { useAlertBeforeUnload } from "../utils/useAlertBeforeUnload"

type Props = {
survey: TSurvey
isPageCompleted: boolean
setStage: (value: SetStateAction<"SURVEY" | "MORE" | "FEEDBACK" | "EMAIL" | "START">) => void
surveyPageProgressProps: {
surveyPageProgress: number
setSurveyPageProgress: (value: SetStateAction<number>) => void
}
}

export const Survey: React.FC<Props> = ({
export const Survey = ({
survey,
setStage,
isPageCompleted,
surveyPageProgressProps: { surveyPageProgress, setSurveyPageProgress },
}) => {
}: Props) => {
const { setProgress } = useContext(ProgressContext)

useAlertBeforeUnload()

// for debugging
const { getValues } = useFormContext()
const {
// for debugging
getValues,
} = useFormContext()
const responsesForDebugging = getValues()

const handleNextPage = () => {
Expand Down Expand Up @@ -66,7 +66,7 @@ export const Survey: React.FC<Props> = ({
<pre>{JSON.stringify(responsesForDebugging, null, 2)}</pre>
</code>
</Debug>
{page && <Page page={page} buttonActions={buttonActions} completed={isPageCompleted} />}
{page && <Page page={page} buttonActions={buttonActions} />}
</>
)
}
3 changes: 1 addition & 2 deletions src/survey-public/components/SurveyInactivePage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { SurveyLayout } from "@/src/survey-public/components/core/layout/SurveyLayout"
import { BlitzPage } from "@blitzjs/next"
import { useEffect } from "react"
import { SurveyScreenHeader } from "./core/layout/SurveyScreenHeader"
import { SurveyLink } from "./core/links/SurveyLink"
import { TSurvey } from "./types"
type Props = {
surveyDefinition: TSurvey
}
const SurveyInactivePage: BlitzPage<Props> = ({ surveyDefinition }) => {
const SurveyInactivePage = ({ surveyDefinition }: Props) => {
useEffect(() => {
const root = document.documentElement
root.style.setProperty("--survey-primary-color", surveyDefinition.primaryColor)
Expand Down
Loading

0 comments on commit a87c92b

Please sign in to comment.