From 2bfc691659d3d63face7ecbd4853fb35dee4e820 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Fri, 13 Mar 2020 16:03:26 -0400 Subject: [PATCH] unstyled but working repeater --- src/client/components/forms/Form/index.js | 102 +++++++++++++++--- .../forms/RenderFields/RenderFieldsNotes.md | 35 ++++++ .../components/forms/RenderFields/index.js | 4 +- .../forms/field-types/Repeater/index.js | 82 +++++++++++--- .../forms/field-types/Repeater/index.scss | 3 + .../components/forms/useFieldType/index.js | 6 +- 6 files changed, 202 insertions(+), 30 deletions(-) create mode 100644 src/client/components/forms/RenderFields/RenderFieldsNotes.md create mode 100644 src/client/components/forms/field-types/Repeater/index.scss diff --git a/src/client/components/forms/Form/index.js b/src/client/components/forms/Form/index.js index 79863764d5..d4a0843f63 100644 --- a/src/client/components/forms/Form/index.js +++ b/src/client/components/forms/Form/index.js @@ -1,7 +1,7 @@ -import React, { useState, useReducer } from 'react'; +import React, { useState, useReducer, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import PropTypes from 'prop-types'; -import { unflatten } from 'flat'; +import flatten, { unflatten } from 'flat'; import FormContext from './Context'; import { useLocale } from '../../utilities/Locale'; import { useStatusList } from '../../modules/Status'; @@ -12,26 +12,103 @@ import './index.scss'; const baseClass = 'form'; -const initialFieldState = {}; +const reduceToFieldNames = fields => fields.reduce((acc, field) => { + if (field.name) acc.push(field.name); + return acc; +}, []); +const reindexRows = ({ + fieldName, fields, rowFieldNamesAsArray, totalRows, index: adjustmentIndex, type, +}) => { + return Array.from(Array(totalRows).keys()).reduce((reindexedRows, _, rowIndex) => { + const currentRow = rowFieldNamesAsArray.reduce((fieldAcc, rowFieldName) => { + let newIndex; + switch (type) { + case 'addAfter': + newIndex = rowIndex <= adjustmentIndex ? rowIndex : rowIndex + 1; + if (rowIndex === adjustmentIndex) { + return { + ...fieldAcc, + [`${fieldName}.${newIndex}.${rowFieldName}`]: fields[`${fieldName}.${rowIndex}.${rowFieldName}`], + }; + } + return { + ...fieldAcc, + [`${fieldName}.${newIndex}.${rowFieldName}`]: fields[`${fieldName}.${rowIndex}.${rowFieldName}`], + }; + + case 'remove': + if (rowIndex === adjustmentIndex) return { ...fieldAcc }; + + newIndex = rowIndex < adjustmentIndex ? rowIndex : rowIndex - 1; + return { + ...fieldAcc, + [`${fieldName}.${newIndex}.${rowFieldName}`]: fields[`${fieldName}.${rowIndex}.${rowFieldName}`], + }; + + default: + return { ...fieldAcc }; + } + }, {}); + + return { ...reindexedRows, ...currentRow }; + }, {}); +}; + +const initialFieldState = {}; function fieldReducer(state, action) { - return { - ...state, - [action.name]: { - value: action.value, - valid: action.valid, - }, - }; + switch (action.type) { + case 'replace': + return { + ...action.value, + }; + + default: + return { + ...state, + [action.name]: { + value: action.value, + valid: action.valid, + }, + }; + } } const Form = (props) => { - const [fields, setField] = useReducer(fieldReducer, initialFieldState); + const [fields, dispatchFields] = useReducer(fieldReducer, initialFieldState); const [submitted, setSubmitted] = useState(false); const [processing, setProcessing] = useState(false); const history = useHistory(); const locale = useLocale(); const { addStatus } = useStatusList(); + function adjustRows({ + index, fieldName, fields: fieldsForInsert, totalRows, type, + }) { + const rowFieldNamesAsArray = reduceToFieldNames(fieldsForInsert); + const reindexedRows = reindexRows({ + fieldName, + fields, + rowFieldNamesAsArray, + totalRows, + index, + type, + }); + + const stateWithoutFields = { ...fields }; + Array.from(Array(totalRows).keys()).forEach((rowIndex) => { + rowFieldNamesAsArray.forEach((rowFieldName) => { delete stateWithoutFields[`${fieldName}.${rowIndex}.${rowFieldName}`]; }); + }); + + dispatchFields({ + type: 'replace', + value: { + ...stateWithoutFields, + ...reindexedRows, + }, + }); + } + const { onSubmit, ajax, @@ -143,10 +220,11 @@ const Form = (props) => { className={classes} > { <> {fields.map((field, i) => { const { defaultValue } = field; - const FieldComponent = field.component || fieldTypes[field.type]; + const FieldComponent = fieldTypes[field.type]; if (FieldComponent) { return ( diff --git a/src/client/components/forms/field-types/Repeater/index.js b/src/client/components/forms/field-types/Repeater/index.js index d900854fde..1ffa6f7ba1 100644 --- a/src/client/components/forms/field-types/Repeater/index.js +++ b/src/client/components/forms/field-types/Repeater/index.js @@ -1,30 +1,86 @@ -import React from 'react'; +import React, { useState, useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; + +import FormContext from '../../Form/Context'; import Section from '../../../layout/Section'; import RenderFields from '../../RenderFields'; +import './index.scss'; + const Repeater = (props) => { + const formContext = useContext(FormContext); + const { adjustRows } = formContext; + const { - label, fields, name, defaultValue, + label, + name, + defaultValue: defaultOrSavedValue, + fields, } = props; - let rows = defaultValue.length > 0 ? defaultValue : [{}]; + const [internalRowCount, setInternalRowCount] = useState(1); + useEffect(() => { setInternalRowCount(defaultOrSavedValue.length); }, [defaultOrSavedValue]); + + function addNewRow({ rowIndex }) { + setInternalRowCount(count => count + 1); + adjustRows({ + index: rowIndex + 1, + fieldName: name, + totalRows: internalRowCount, + fields, + type: 'addAfter', + }); + } + + function removeRow({ rowIndex }) { + setInternalRowCount(count => count - 1); + adjustRows({ + index: rowIndex, + fieldName: name, + totalRows: internalRowCount, + fields, + type: 'remove', + }); + } + + const initialRows = defaultOrSavedValue.length > 0 ? defaultOrSavedValue : [{}]; + const iterableInternalRowCount = Array.from(Array(internalRowCount).keys()); return (
- {rows.map((row, i) => { + + {iterableInternalRowCount.map((_, rowIndex) => { return ( - ({ - ...subField, - name: `${name}.${i}.${subField.name}`, - defaultValue: row[subField.name] || null, - }))} - /> - ) + +

{`Repeater Item ${rowIndex}`}

+ + { + return ({ + ...field, + name: `${name}.${rowIndex}.${field.name}`, + defaultValue: initialRows[rowIndex] ? initialRows[rowIndex][field.name] : null, + }); + })} + /> + + + +
+ ); })} +
); diff --git a/src/client/components/forms/field-types/Repeater/index.scss b/src/client/components/forms/field-types/Repeater/index.scss new file mode 100644 index 0000000000..5299cb5b61 --- /dev/null +++ b/src/client/components/forms/field-types/Repeater/index.scss @@ -0,0 +1,3 @@ +.field-repeater { + // background: red; +} diff --git a/src/client/components/forms/useFieldType/index.js b/src/client/components/forms/useFieldType/index.js index 8c6c7bf90c..8160bd657c 100644 --- a/src/client/components/forms/useFieldType/index.js +++ b/src/client/components/forms/useFieldType/index.js @@ -5,7 +5,7 @@ import './index.scss'; const useFieldType = (options) => { const formContext = useContext(FormContext); - const { setField, submitted, processing } = formContext; + const { dispatchFields, submitted, processing } = formContext; const { name, @@ -16,14 +16,14 @@ const useFieldType = (options) => { } = options; const sendField = useCallback((valueToSend) => { - setField({ + dispatchFields({ name, value: valueToSend, valid: required && validate ? validate(valueToSend || '') : true, }); - }, [name, required, setField, validate]); + }, [name, required, dispatchFields, validate]); useEffect(() => { sendField(defaultValue);