diff options
author | 2021-01-19 14:14:38 +0200 | |
---|---|---|
committer | 2021-01-19 14:14:38 +0200 | |
commit | 0870589dab3fa46f59a1d6b128528c570da74cc7 (patch) | |
tree | a898f18f34fb8cbc1ca092e03624dea4c4090d8f | |
parent | Implement validation for checkboxes (diff) |
Implement before-submit validation (broken, crashing)
-rw-r--r-- | src/components/Question.tsx | 63 | ||||
-rw-r--r-- | src/pages/FormPage.tsx | 36 |
2 files changed, 94 insertions, 5 deletions
diff --git a/src/components/Question.tsx b/src/components/Question.tsx index 81c43b2..df9d18e 100644 --- a/src/components/Question.tsx +++ b/src/components/Question.tsx @@ -19,6 +19,7 @@ const skip_normal_state: Array<QuestionType> = [ export type QuestionProp = { question: Question, public_state: Map<string, string | boolean | null>, + scroll_ref: React.RefObject<HTMLDivElement> } class RenderedQuestion extends React.Component<QuestionProp> { @@ -146,6 +147,66 @@ class RenderedQuestion extends React.Component<QuestionProp> { this.setPublicState("value", event.target.value); } + validateField(): void { + if (!this.props.question.required) { + return; + } + + let invalid = false; + const options: string | string[] = this.props.question.data["options"]; + switch (this.props.question.type) { + case QuestionType.TextArea: + if (this.props.public_state.get("value") === "") { + invalid = true; + } + break; + + case QuestionType.ShortText: + if (this.props.public_state.get("value") === "") { + invalid = true; + } + break; + + case QuestionType.Select: + if (!this.props.public_state.get("value")) { + invalid = true; + } + break; + + case QuestionType.Range: + if (!this.props.public_state.get("value")) { + invalid = true; + } + break; + + case QuestionType.Radio: + if (!this.props.public_state.get("value")) { + invalid = true; + } + break; + + case QuestionType.Checkbox: + if (typeof options !== "string") { + const keys: string[] = []; + options.forEach((val, index) => { + keys.push(`${("000" + index).slice(-4)}. ${val}`); + }); + if (keys.every(v => !this.props.public_state.get(v))) { + invalid = true; + } + } + break; + } + + if (invalid) { + this.setPublicState("error", "Field must be filled."); + this.setPublicState("valid", false); + } else { + this.setPublicState("error", ""); + this.setPublicState("valid", true); + } + } + componentDidMount(): void { // Initialize defaults for complex and nested fields const options: string | string[] = this.props.question.data["options"]; @@ -228,7 +289,7 @@ class RenderedQuestion extends React.Component<QuestionProp> { error = rawError; } - return <div> + return <div ref={this.props.scroll_ref}> <h2 css={[selectable, requiredStarStyles]}> {question.name}<span className={question.required ? "required" : ""}>*</span> </h2> diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx index c49b9fd..6c7cad2 100644 --- a/src/pages/FormPage.tsx +++ b/src/pages/FormPage.tsx @@ -2,7 +2,7 @@ import { jsx, css } from "@emotion/react"; import { Link } from "react-router-dom"; -import React, { SyntheticEvent, useEffect, useState } from "react"; +import React, { SyntheticEvent, useEffect, useState, createRef, RefObject } from "react"; import { useParams } from "react-router"; import HeaderBar from "../components/HeaderBar"; @@ -13,6 +13,7 @@ import ScrollToTop from "../components/ScrollToTop"; import { Form, FormFeatures, getForm } from "../api/forms"; import colors from "../colors"; import { unselectable } from "../commonStyles"; +import { Question } from "../api/question"; interface PathParams { @@ -167,12 +168,36 @@ function FormPage(): JSX.Element { return <Loading/>; } - const questions = form.questions.map((question, index) => { - return <RenderedQuestion question={question} public_state={new Map()} key={index + Date.now()}/>; + const questionsMap: Map<string, JSX.Element> = new Map(); + form.questions.map((question, index) => { + questionsMap.set(question.id, <RenderedQuestion ref={createRef<RenderedQuestion>()} scroll_ref={createRef<HTMLDivElement>()} question={question} public_state={new Map()} key={index + Date.now()}/>); }); function handleSubmit(event: SyntheticEvent) { - questions.forEach(prop => { + // Client-side required validation + const invalidFieldIds: string[] = []; + questionsMap.forEach((prop, id) => { + const question: Question = prop.props.question; + if (!question.required) { + return; + } + + prop.ref.current.validateField(); + // In case when field is invalid, add this to invalid fields list. + if (prop.props.public_state.get("valid") === false) { + invalidFieldIds.push(id); + } + }); + + if (invalidFieldIds.length) { + const firstErrored = questionsMap.get(invalidFieldIds[0]); + if (firstErrored !== undefined) { + firstErrored.props.scroll_ref.current.scrollIntoView({ behavior: "smooth", block: "center" }); + } + return; + } + + questionsMap.forEach(prop => { const question = prop.props.question; // TODO: Parse input from each question, and submit @@ -192,6 +217,9 @@ function FormPage(): JSX.Element { closed_header = <div css={closedHeaderStyles}>This form is now closed. You will not be able to submit your response.</div>; } + const questions: JSX.Element[] = []; + questionsMap.forEach(val => questions.push(val)); + return ( <div> <HeaderBar title={form.name} description={form.description}/> |