aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/components/Question.tsx63
-rw-r--r--src/pages/FormPage.tsx36
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}/>