diff options
| author | 2021-02-21 07:35:29 +0200 | |
|---|---|---|
| committer | 2021-02-21 07:35:29 +0200 | |
| commit | b1f05fa57c862ce8219e5ca464e794353261f842 (patch) | |
| tree | ff67e7265ad52099181ceb0bf2a0af36f0525fdd /src | |
| parent | Move hCaptcha types library to dev-dependencies (diff) | |
Migrate from public state map to Redux
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.tsx | 4 | ||||
| -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 | ||||
| -rw-r--r-- | src/pages/FormPage.tsx | 38 | ||||
| -rw-r--r-- | src/store/form/actions.ts | 79 | ||||
| -rw-r--r-- | src/store/form/reducers.ts | 29 | ||||
| -rw-r--r-- | src/store/form/store.ts | 4 | ||||
| -rw-r--r-- | src/store/form/types.ts | 5 | 
10 files changed, 285 insertions, 87 deletions
diff --git a/src/App.tsx b/src/App.tsx index 523e583..14e5329 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@  import React, { Suspense } from "react";  import { jsx, css, Global } from "@emotion/react"; +import { Provider } from "react-redux";  import {      BrowserRouter as Router,      Route, @@ -14,6 +15,7 @@ import { PropagateLoader } from "react-spinners";  import { CSSTransition, TransitionGroup } from "react-transition-group";  import globalStyles from "./globalStyles"; +import { store } from "./store/form/store";  const LandingPage = React.lazy(() => import("./pages/LandingPage"));  const FormPage = React.lazy(() => import("./pages/FormPage")); @@ -51,7 +53,7 @@ function App(): JSX.Element {                                  {routes.map(({path, Component}) => (                                      <Route exact key={path} path={path}>                                          <Suspense fallback={<PageLoading/>}> -                                            <Component/> +                                            {path == "/form/:id" ? <Provider store={store}><Component/></Provider> : <Component/>}                                          </Suspense>                                      </Route>                                  ))} 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); diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx index 1e331b9..e9306e3 100644 --- a/src/pages/FormPage.tsx +++ b/src/pages/FormPage.tsx @@ -5,9 +5,10 @@ import { Link } from "react-router-dom";  import React, { SyntheticEvent, useEffect, useState, createRef } from "react";  import { useParams } from "react-router";  import { PropagateLoader } from "react-spinners"; +import { useDispatch, useSelector } from "react-redux";  import HeaderBar from "../components/HeaderBar"; -import RenderedQuestion from "../components/Question"; +import ConnectedRenderedQuestion, { RenderedQuestion } from "../components/Question";  import Loading from "../components/Loading";  import ScrollToTop from "../components/ScrollToTop"; @@ -16,6 +17,8 @@ import colors from "../colors";  import { unselectable }  from "../commonStyles";  import { Question, QuestionType } from "../api/question";  import ApiClient from "../api/client"; +import { FormState } from "../store/form/types"; +import { clean } from "../store/form/actions";  interface PathParams {      id: string @@ -157,11 +160,21 @@ const closedHeaderStyles = css`  function FormPage(): JSX.Element {      const { id } = useParams<PathParams>(); +    const valid = useSelector<FormState, FormState["valid"]>( +        state => state.valid +    ); +    const values = useSelector<FormState, FormState["values"]>( +        state => state.values +    ); + +    const dispatch = useDispatch(); +      const [form, setForm] = useState<Form>();      const [sending, setSending] = useState<boolean>();      const [sent, setSent] = useState<boolean>();      useEffect(() => { +        dispatch(clean());          getForm(id).then(form => {              setForm(form);          }); @@ -203,7 +216,7 @@ function FormPage(): JSX.Element {          const questionRef = createRef<RenderedQuestion>();          refMap.set(question.id, questionRef);          // eslint-disable-next-line @typescript-eslint/no-explicit-any -        return <RenderedQuestion ref={questionRef} focus_ref={createRef<any>()} scroll_ref={createRef<HTMLDivElement>()} question={question} public_state={new Map()} key={index + Date.now()}/>; +        return <ConnectedRenderedQuestion ref={questionRef} focus_ref={createRef<any>()} scroll_ref={createRef<HTMLDivElement>()} question={question} key={index + Date.now()}/>;      });      async function handleSubmit(event: SyntheticEvent) { @@ -220,8 +233,8 @@ function FormPage(): JSX.Element {              if (questionRef && questionRef.current) {                  questionRef.current.validateField();              } -            // In case when field is invalid, add this to invalid fields list. -            if (prop.props.public_state.get("valid") === false) { +            // In case when field is invalid, add this to invalid fields list +            if (valid.get(question.id) === false) {                  invalidFieldIDs.push(i);              }          }); @@ -257,22 +270,27 @@ function FormPage(): JSX.Element {                  case QuestionType.Checkbox: {                      if (typeof options !== "string") { +                        console.log(values); +                        const checkbox_values = values.get(question.id); +                        console.log(checkbox_values);                          const keys: Map<string, string> = new Map();                          options.forEach((val: string, index) => {                              keys.set(val, `${("000" + index).slice(-4)}. ${val}`);                          }); -                        const pairs: { [key: string]: boolean } = { }; -                        keys.forEach((val, key) => { -                            pairs[key] = !!prop.props.public_state.get(val); -                        }); -                        answers[question.id] = pairs; +                        if (checkbox_values instanceof Map) { +                            const pairs: { [key: string]: boolean } = { }; +                            keys.forEach((val, key) => { +                                pairs[key] = !!checkbox_values.get(val); +                            }); +                            answers[question.id] = pairs; +                        }                      }                      break;                  }                  case QuestionType.Code:                  default: -                    answers[question.id] = prop.props.public_state.get("value"); +                    answers[question.id] = values.get(question.id);              }          }); diff --git a/src/store/form/actions.ts b/src/store/form/actions.ts new file mode 100644 index 0000000..8b48a37 --- /dev/null +++ b/src/store/form/actions.ts @@ -0,0 +1,79 @@ +import { Question } from "../../api/question"; + +// All Redux actions that can be triggered +export enum FormAction { +    SET_VALUE = "SET_VALUE", +    SET_ERROR = "SET_ERROR", +    SET_VALID = "SET_VALID", +    CLEAN = "CLEAN" +} + +// This is base for all actions +export interface DefaultFormAction { +    type: FormAction +} + +// Return values for actions +export interface SetValueAction extends DefaultFormAction { +    type: FormAction.SET_VALUE, +    payload: { +        question: Question, +        value: string | Map<string, boolean> | null +    } +} + +export interface SetErrorAction extends DefaultFormAction { +    type: FormAction.SET_ERROR, +    payload: { +        question: Question, +        error: string +    } +} + +export interface SetValidAction extends DefaultFormAction { +    type: FormAction.SET_VALID, +    payload: { +        question: Question, +        valid: boolean +    } +} + +export interface CleanAction extends DefaultFormAction { +    type: FormAction.CLEAN +} + +export type Action = SetValueAction | SetErrorAction | SetValidAction | CleanAction; + +export function setValue(question: Question, value: string | Map<string, boolean> | null): SetValueAction { +    return { +        type: FormAction.SET_VALUE, +        payload: { +            question: question, +            value: value +        } +    }; +} + +export function setError(question: Question, error: string): SetErrorAction { +    return { +        type: FormAction.SET_ERROR, +        payload: { +            question: question, +            error: error +        } +    }; +} + +export function setValid(question: Question, valid: boolean): SetValidAction { +    return { +        type: FormAction.SET_VALID, +        payload: { +            question: question, +            valid: valid +        } +    }; +} + +export function clean(): CleanAction { +    return { type: FormAction.CLEAN }; +} diff --git a/src/store/form/reducers.ts b/src/store/form/reducers.ts new file mode 100644 index 0000000..fcbb33a --- /dev/null +++ b/src/store/form/reducers.ts @@ -0,0 +1,29 @@ +import {Action, FormAction} from "./actions"; +import {FormState} from "./types"; + +export const initialState: FormState = { +    values: new Map(), +    errors: new Map(), +    valid: new Map() +}; + +export function formReducer(state = initialState, action: Action): FormState { +    const new_state = state; +    switch (action.type) { +        case FormAction.SET_VALUE: +            new_state.values.set(action.payload.question.id, action.payload.value); +            break; + +        case FormAction.SET_ERROR: +            new_state.errors.set(action.payload.question.id, action.payload.error); +            break; + +        case FormAction.SET_VALID: +            new_state.valid.set(action.payload.question.id, action.payload.valid); +            break; + +        case FormAction.CLEAN: +            return initialState; +    } +    return new_state; +} diff --git a/src/store/form/store.ts b/src/store/form/store.ts new file mode 100644 index 0000000..4311a2f --- /dev/null +++ b/src/store/form/store.ts @@ -0,0 +1,4 @@ +import { createStore } from "redux"; +import { formReducer } from "./reducers"; + +export const store = createStore(formReducer); diff --git a/src/store/form/types.ts b/src/store/form/types.ts new file mode 100644 index 0000000..1d3e594 --- /dev/null +++ b/src/store/form/types.ts @@ -0,0 +1,5 @@ +export interface FormState { +    values: Map<string, string | Map<string, boolean> | null>, +    errors: Map<string, string>, +    valid: Map<string, boolean> +}  |