diff options
| author | 2021-02-21 07:35:29 +0200 | |
|---|---|---|
| committer | 2021-02-21 07:35:29 +0200 | |
| commit | b1f05fa57c862ce8219e5ca464e794353261f842 (patch) | |
| tree | ff67e7265ad52099181ceb0bf2a0af36f0525fdd /src/components | |
| parent | Move hCaptcha types library to dev-dependencies (diff) | |
Migrate from public state map to Redux
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/InputTypes/Checkbox.tsx | 7 | ||||
| -rw-r--r-- | src/components/InputTypes/Select.tsx | 40 | ||||
| -rw-r--r-- | src/components/InputTypes/index.tsx | 13 | ||||
| -rw-r--r-- | src/components/Question.tsx | 153 |
4 files changed, 137 insertions, 76 deletions
diff --git a/src/components/InputTypes/Checkbox.tsx b/src/components/InputTypes/Checkbox.tsx index 3093caf..b3130c6 100644 --- a/src/components/InputTypes/Checkbox.tsx +++ b/src/components/InputTypes/Checkbox.tsx @@ -3,6 +3,8 @@ import { jsx, css } from "@emotion/react"; import React, { ChangeEvent } from "react"; import colors from "../../colors"; import { multiSelectInput, hiddenInput } from "../../commonStyles"; +import {useSelector} from "react-redux"; +import {FormState} from "../../store/form/types"; interface CheckboxProps { index: number, @@ -53,10 +55,13 @@ const activeStyles = css` `; export default function Checkbox(props: CheckboxProps): JSX.Element { + const values = useSelector<FormState, FormState["values"]>( + state => state.values + ); return ( <label css={[generalStyles, activeStyles]}> <label className="unselected" css={multiSelectInput}> - <input type="checkbox" value={props.option} css={hiddenInput} name={`${("000" + props.index).slice(-4)}. ${props.option}`} onChange={props.handler}/> + <input type="checkbox" value={props.option} css={hiddenInput} name={`${("000" + props.index).slice(-4)}. ${props.option}`} onChange={props.handler} checked={!!values.get(`${("000" + props.index).slice(-4)}. ${props.option}`)}/> <span className="checkmark"/> </label> {props.option}<br/> diff --git a/src/components/InputTypes/Select.tsx b/src/components/InputTypes/Select.tsx index 2d0187a..c0f5425 100644 --- a/src/components/InputTypes/Select.tsx +++ b/src/components/InputTypes/Select.tsx @@ -1,13 +1,26 @@ /** @jsx jsx */ import { jsx, css } from "@emotion/react"; import React from "react"; +import { connect } from "react-redux"; + import { hiddenInput, invalidStyles } from "../../commonStyles"; +import { Question } from "../../api/question"; +import { setValue, SetValueAction } from "../../store/form/actions"; +import { FormState } from "../../store/form/types"; interface SelectProps { options: Array<string>, - state_dict: Map<string, string | boolean | null>, valid: boolean, - onBlurHandler: () => void + onBlurHandler: () => void, + question: Question +} + +interface SelectStateProps { + values: Map<string, string | Map<string, boolean> | null> +} + +interface SelectDispatchProps { + setValue: (question: Question, value: string | Map<string, boolean> | null) => SetValueAction } const containerStyles = css` @@ -143,7 +156,7 @@ const optionStyles = css` } `; -class Select extends React.Component<SelectProps> { +class Select extends React.Component<SelectProps & SelectStateProps & SelectDispatchProps> { handler(selected_option: React.RefObject<HTMLDivElement>, event: React.ChangeEvent<HTMLInputElement>): void { const option_container = event.target.parentElement; if (!option_container || !option_container.parentElement || !selected_option.current) { @@ -151,7 +164,7 @@ class Select extends React.Component<SelectProps> { } // Update stored value - this.props.state_dict.set("value", option_container.textContent); + this.props.setValue(this.props.question, option_container.textContent); // Close the menu selected_option.current.focus(); @@ -178,10 +191,10 @@ class Select extends React.Component<SelectProps> { } focusOption(): void { - if (!this.props.state_dict.get("value")) { - this.props.state_dict.set("value", "temporary"); + if (!this.props.values.get(this.props.question.id)) { + this.props.setValue(this.props.question, "temporary"); this.props.onBlurHandler(); - this.props.state_dict.set("value", null); + this.props.setValue(this.props.question, null); } } @@ -211,4 +224,15 @@ class Select extends React.Component<SelectProps> { } } -export default Select; +const mapStateToProps = (state: FormState, ownProps: SelectProps): SelectProps & SelectStateProps => { + return { + ...ownProps, + values: state.values + }; +}; + +const mapDispatchToProps = { + setValue +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Select); diff --git a/src/components/InputTypes/index.tsx b/src/components/InputTypes/index.tsx index bc65248..e816c9c 100644 --- a/src/components/InputTypes/index.tsx +++ b/src/components/InputTypes/index.tsx @@ -6,10 +6,10 @@ import Select from "./Select"; import ShortText from "./ShortText"; import TextArea from "./TextArea"; -import React, { ChangeEvent } from "react"; +import React, {ChangeEvent} from "react"; -import { QuestionType } from "../../api/question"; -import { QuestionProp } from "../Question"; +import {QuestionType} from "../../api/question"; +import {QuestionDispatchProp, QuestionProp, QuestionStateProp} from "../Question"; const require_options: Array<QuestionType> = [ QuestionType.Radio, @@ -19,14 +19,15 @@ const require_options: Array<QuestionType> = [ ]; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export default function create_input({ question, public_state }: QuestionProp, handler: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void, onBlurHandler: () => void, focus_ref: React.RefObject<any>): JSX.Element | JSX.Element[] { +export default function create_input(props: QuestionProp & QuestionStateProp & QuestionDispatchProp, handler: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void, onBlurHandler: () => void, focus_ref: React.RefObject<any>): JSX.Element | JSX.Element[] { let result: JSX.Element | JSX.Element[]; + const question = props.question; // eslint-disable-next-line // @ts-ignore let options: string[] = question.data["options"]; let valid = true; - if (!public_state.get("valid")) { + if (!props.valid.get(question.id)) { valid = false; } @@ -51,7 +52,7 @@ export default function create_input({ question, public_state }: QuestionProp, h break; case QuestionType.Select: - result = <Select options={options} state_dict={public_state} valid={valid} onBlurHandler={onBlurHandler}/>; + result = <Select options={options} question={question} valid={valid} onBlurHandler={onBlurHandler}/>; break; case QuestionType.ShortText: diff --git a/src/components/Question.tsx b/src/components/Question.tsx index 0af745e..74c4a71 100644 --- a/src/components/Question.tsx +++ b/src/components/Question.tsx @@ -1,11 +1,14 @@ /** @jsx jsx */ import { jsx, css } from "@emotion/react"; import React, { ChangeEvent } from "react"; +import { connect } from "react-redux"; import { Question, QuestionType } from "../api/question"; import { selectable } from "../commonStyles"; import create_input from "./InputTypes"; import ErrorMessage from "./ErrorMessage"; +import { FormState } from "../store/form/types"; +import { setError, SetErrorAction, setValid, SetValidAction, setValue, SetValueAction } from "../store/form/actions"; const skip_normal_state: Array<QuestionType> = [ QuestionType.Radio, @@ -17,14 +20,25 @@ const skip_normal_state: Array<QuestionType> = [ export type QuestionProp = { question: Question, - public_state: Map<string, string | boolean | null>, scroll_ref: React.RefObject<HTMLDivElement>, // eslint-disable-next-line @typescript-eslint/no-explicit-any focus_ref: React.RefObject<any> } -class RenderedQuestion extends React.Component<QuestionProp> { - constructor(props: QuestionProp) { +export type QuestionStateProp = { + values: Map<string, string | Map<string, boolean> | null>, + errors: Map<string, string>, + valid: Map<string, boolean>, +}; + +export type QuestionDispatchProp = { + setValue: (question: Question, value: string | Map<string, boolean> | null) => SetValueAction, + setValid: (question: Question, valid: boolean) => SetValidAction, + setError: (question: Question, error: string) => SetErrorAction +}; + +export class RenderedQuestion extends React.Component<QuestionProp & QuestionStateProp & QuestionDispatchProp> { + constructor(props: QuestionProp & QuestionStateProp & QuestionDispatchProp) { super(props); if (props.question.type === QuestionType.TextArea) { this.handler = this.text_area_handler.bind(this); @@ -33,47 +47,39 @@ class RenderedQuestion extends React.Component<QuestionProp> { } this.blurHandler = this.blurHandler.bind(this); - this.setPublicState("valid", true); - this.setPublicState("error", ""); + props.setValid(props.question, true); + props.setError(props.question, ""); if (!skip_normal_state.includes(props.question.type)) { - this.setPublicState("value", ""); + props.setValue(props.question, ""); } } - setPublicState(target: string, value: string | boolean | null, callback?:() => void): void { - this.setState({[target]: value}, callback); - this.props.public_state.set(target, value); - } - // This is here to allow dynamic selection between the general handler, and the textarea handler. handler(_: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void {} // eslint-disable-line blurHandler(): void { if (this.props.question.required) { - if (!this.props.public_state.get("value")) { - this.setPublicState("error", "Field must be filled."); - this.setPublicState("valid", false); + if (!this.props.values.get(this.props.question.id)) { + this.props.setError(this.props.question, "Field must be filled."); + this.props.setValid(this.props.question, false); } else { - this.setPublicState("error", ""); - this.setPublicState("valid", true); + this.props.setError(this.props.question, ""); + this.props.setValid(this.props.question, true); } } } normal_handler(event: ChangeEvent<HTMLInputElement>): void { - let target: string; - let value: string | boolean; + let value: string | [string, boolean]; switch (event.target.type) { case "checkbox": - target = event.target.name; - value = event.target.checked; + value = [event.target.name, event.target.checked]; break; case "radio": - // This handles radios and ranges, as they are both based on the same fundamental input type - target = "value"; + // This handles radios and ranges, as they are both based on the same fundamental input type if (event.target.parentElement) { value = event.target.parentElement.innerText.trimEnd(); } else { @@ -82,11 +88,19 @@ class RenderedQuestion extends React.Component<QuestionProp> { break; default: - target = "value"; value = event.target.value; } - this.setPublicState(target, value); + if (value instanceof Array) { + let values = this.props.values.get(this.props.question.id); + if (!(values instanceof Map)) { + values = new Map<string, boolean>(); + } + values.set(value[0], value[1]); + this.props.setValue(this.props.question, values); + } else { + this.props.setValue(this.props.question, value); + } // Toggle checkbox class if (event.target.type == "checkbox" && event.target.parentElement !== null) { @@ -97,7 +111,7 @@ class RenderedQuestion extends React.Component<QuestionProp> { const options: string | string[] = this.props.question.data["options"]; switch (event.target.type) { case "text": - this.setPublicState("valid", true); + this.props.setValid(this.props.question, true); break; case "checkbox": @@ -107,29 +121,30 @@ class RenderedQuestion extends React.Component<QuestionProp> { options.forEach((val, index) => { keys.push(`${("000" + index).slice(-4)}. ${val}`); }); - if (keys.every(v => !this.props.public_state.get(v))) { - this.setPublicState("error", "Field must be filled."); - this.setPublicState("valid", false); + const values = this.props.values.get(this.props.question.id); + if (values instanceof Map && keys.every(v => !values.get(v))) { + this.props.setError(this.props.question, "Field must be filled."); + this.props.setValid(this.props.question, false); } else { - this.setPublicState("error", ""); - this.setPublicState("valid", true); + this.props.setError(this.props.question, ""); + this.props.setValid(this.props.question, true); } } break; case "radio": - this.setPublicState("valid", true); - this.setPublicState("error", ""); + this.props.setError(this.props.question, ""); + this.props.setValid(this.props.question, true); break; } } text_area_handler(event: ChangeEvent<HTMLTextAreaElement>): void { // We will validate again when focusing out. - this.setPublicState("valid", true); - this.setPublicState("error", ""); + this.props.setError(this.props.question, ""); + this.props.setValid(this.props.question, true); - this.setPublicState("value", event.target.value); + this.props.setValue(this.props.question, event.target.value); } validateField(): void { @@ -142,7 +157,7 @@ class RenderedQuestion extends React.Component<QuestionProp> { switch (this.props.question.type) { case QuestionType.TextArea: case QuestionType.ShortText: - if (this.props.public_state.get("value") === "") { + if (this.props.values.get(this.props.question.id) === "") { invalid = true; } break; @@ -150,7 +165,7 @@ class RenderedQuestion extends React.Component<QuestionProp> { case QuestionType.Select: case QuestionType.Range: case QuestionType.Radio: - if (!this.props.public_state.get("value")) { + if (!this.props.values.get(this.props.question.id)) { invalid = true; } break; @@ -161,7 +176,8 @@ class RenderedQuestion extends React.Component<QuestionProp> { options.forEach((val, index) => { keys.push(`${("000" + index).slice(-4)}. ${val}`); }); - if (keys.every(v => !this.props.public_state.get(v))) { + const values = this.props.values.get(this.props.question.id); + if (values instanceof Map && keys.every(v => !values.get(v))) { invalid = true; } } @@ -169,36 +185,36 @@ class RenderedQuestion extends React.Component<QuestionProp> { } if (invalid) { - this.setPublicState("error", "Field must be filled."); - this.setPublicState("valid", false); + this.props.setError(this.props.question, "Field must be filled."); + this.props.setValid(this.props.question, false); } else { - this.setPublicState("error", ""); - this.setPublicState("valid", true); + this.props.setError(this.props.question, ""); + this.props.setValid(this.props.question, true); } } componentDidMount(): void { // Initialize defaults for complex and nested fields const options: string | string[] = this.props.question.data["options"]; + const values = this.props.values.get(this.props.question.id); - if (this.props.public_state.size === 0) { - switch (this.props.question.type) { - case QuestionType.Checkbox: - if (typeof options === "string") { - return; - } + switch (this.props.question.type) { + case QuestionType.Checkbox: + if (typeof options === "string" || !(values instanceof Map)) { + return; + } - options.forEach((option, index) => { - this.setPublicState(`${("000" + index).slice(-4)}. ${option}`, false); - }); - break; + options.forEach((option, index) => { + values.set(`${("000" + index).slice(-4)}. ${option}`, false); + }); + this.props.setValue(this.props.question, values); + break; - case QuestionType.Range: - case QuestionType.Radio: - case QuestionType.Select: - this.setPublicState("value", null); - break; - } + case QuestionType.Range: + case QuestionType.Radio: + case QuestionType.Select: + this.props.setValue(this.props.question, null); + break; } } @@ -250,10 +266,10 @@ class RenderedQuestion extends React.Component<QuestionProp> { } `; let valid = true; - if (!this.props.public_state.get("valid")) { + if (!this.props.valid.get(this.props.question.id)) { valid = false; } - const rawError = this.props.public_state.get("error"); + const rawError = this.props.errors.get(this.props.question.id); let error = ""; if (typeof rawError === "string") { error = rawError; @@ -271,4 +287,19 @@ class RenderedQuestion extends React.Component<QuestionProp> { } } -export default RenderedQuestion; +const mapStateToProps = (state: FormState, ownProps: QuestionProp): QuestionProp & QuestionStateProp => { + return { + ...ownProps, + values: state.values, + errors: state.errors, + valid: state.valid + }; +}; + +const mapDispatchToProps = { + setValue, + setValid, + setError +}; + +export default connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(RenderedQuestion); |