adds the start of dnd functionality

This commit is contained in:
Jarrod Flesch
2020-03-17 18:17:00 -04:00
parent 8b14e4d118
commit cb284d97a3
9 changed files with 202 additions and 100 deletions

View File

@@ -9,7 +9,7 @@ import './index.scss';
const baseClass = 'icon-button';
const IconButton = ({ iconName, className, ...rest }) => {
const IconButton = React.forwardRef(({ iconName, className, ...rest }, ref) => {
const classes = [
baseClass,
className && className,
@@ -25,14 +25,16 @@ const IconButton = ({ iconName, className, ...rest }) => {
const Icon = icons[iconName] || icons.arrow;
return (
<Button
className={classes}
{...rest}
>
<Icon />
</Button>
<span ref={ref}>
<Button
className={classes}
{...rest}
>
<Icon />
</Button>
</span>
);
};
});
IconButton.defaultProps = {
className: '',

View File

@@ -4,22 +4,22 @@ const splitRowsFromState = (state, name) => {
// Take a copy of state
const remainingState = { ...state };
const rowObject = {};
const rowsFromStateObject = {};
// Loop over all keys from state
// If the key begins with the name of the parent field,
// Add value to rowObject and delete it from remaining state
// Add value to rowsFromStateObject and delete it from remaining state
Object.keys(state).forEach((key) => {
if (key.indexOf(`${name}.`) === 0) {
rowObject[key] = state[key];
rowsFromStateObject[key] = state[key];
delete remainingState[key];
}
});
const rows = unflatten(rowObject);
const rowsFromState = unflatten(rowsFromStateObject);
return {
rows: rows[name] || [],
rowsFromState: rowsFromState[name] || [],
remainingState,
};
};
@@ -33,29 +33,46 @@ function fieldReducer(state, action) {
case 'REMOVE_ROW': {
const { rowIndex, name } = action;
const { rows, remainingState } = splitRowsFromState(state, name);
const { rowsFromState, remainingState } = splitRowsFromState(state, name);
rows.splice(rowIndex, 1);
rowsFromState.splice(rowIndex, 1);
return {
...remainingState,
...(flatten({ [name]: rows }, { maxDepth: 3 })),
...(flatten({ [name]: rowsFromState }, { maxDepth: 3 })),
};
}
case 'ADD_ROW': {
const { rowIndex, name, fields } = action;
const { rows, remainingState } = splitRowsFromState(state, name);
const { rowsFromState, remainingState } = splitRowsFromState(state, name);
// Get names of sub fields
const subFields = fields.reduce((acc, field) => ({ ...acc, [field.name]: {} }), {});
// Add new object containing subfield names to rows array
rows.splice(rowIndex + 1, 0, subFields);
// Add new object containing subfield names to rowsFromState array
rowsFromState.splice(rowIndex + 1, 0, subFields);
return {
...remainingState,
...(flatten({ [name]: rows }, { maxDepth: 3 })),
...(flatten({ [name]: rowsFromState }, { maxDepth: 3 })),
};
}
case 'MOVE_ROW': {
const { moveFromIndex, moveToIndex, name } = action;
const { rowsFromState, remainingState } = splitRowsFromState(state, name);
// copy the row to move
const copyOfMovingRow = rowsFromState[moveFromIndex];
// delete the row by index
rowsFromState.splice(moveFromIndex, 1);
// insert row copyOfMovingRow back in
rowsFromState.splice(moveToIndex, 0, copyOfMovingRow);
return {
...remainingState,
...(flatten({ [name]: rowsFromState }, { maxDepth: 3 })),
};
}

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import AnimateHeight from 'react-animate-height';
import { Draggable } from 'react-beautiful-dnd';
import RenderFields from '../../../RenderFields';
import IconButton from '../../../../controls/IconButton';
@@ -11,63 +12,89 @@ import './index.scss';
const baseClass = 'repeater-section';
const RepeaterSection = ({
addRow, removeRow, rowIndex, parentName, fields, fieldState,
addRow, removeRow, rowIndex, parentName, fields, fieldState, forceContentCollapse,
}) => {
const [isSectionOpen, setIsSectionOpen] = useState(true);
useEffect(() => {
if (forceContentCollapse) setIsSectionOpen(false);
}, [forceContentCollapse]);
return (
<div
className={`${baseClass}`}
<Draggable
draggableId={`row-${rowIndex}`}
index={rowIndex}
>
<div className={`${baseClass}__header`}>
<Pill>
{parentName}
</Pill>
<h4 className={`${baseClass}__header__heading`}>Title Goes Here</h4>
{(providedDrag, snapshot) => (
<div
ref={providedDrag.innerRef}
className={baseClass}
{...providedDrag.draggableProps}
>
<div className={`${baseClass}__header`}>
<Pill>
{parentName}
</Pill>
<h4 className={`${baseClass}__header__heading`}>Title Goes Here</h4>
<div className={`${baseClass}__header__controls`}>
<IconButton
iconName="crosshair"
onClick={addRow}
size="small"
/>
<div className={`${baseClass}__header__controls`}>
<IconButton
iconName="crossOut"
onClick={removeRow}
size="small"
/>
<IconButton
{...providedDrag.dragHandleProps}
iconName="crosshair"
onClick={addRow}
size="small"
/>
<IconButton
className={`${baseClass}__collapse__icon ${baseClass}__collapse__icon--${isSectionOpen ? 'open' : 'closed'}`}
iconName="arrow"
onClick={() => setIsSectionOpen(state => !state)}
size="small"
/>
<IconButton
iconName="crosshair"
onClick={addRow}
size="small"
/>
<IconButton
iconName="crossOut"
onClick={removeRow}
size="small"
/>
<IconButton
className={`${baseClass}__collapse__icon ${baseClass}__collapse__icon--${isSectionOpen ? 'open' : 'closed'}`}
iconName="arrow"
onClick={() => setIsSectionOpen(state => !state)}
size="small"
/>
</div>
</div>
<AnimateHeight
className={`${baseClass}__content`}
height={isSectionOpen ? 'auto' : 0}
duration={150}
>
<RenderFields
key={rowIndex}
fields={fields.map((field) => {
const fieldName = `${parentName}.${rowIndex}.${field.name}`;
return ({
...field,
name: fieldName,
defaultValue: fieldState?.[fieldName]?.value,
});
})}
/>
</AnimateHeight>
</div>
</div>
<AnimateHeight
className={`${baseClass}__content`}
height={isSectionOpen ? 'auto' : 0}
duration={150}
>
<RenderFields
key={rowIndex}
fields={fields.map((field) => {
const fieldName = `${parentName}.${rowIndex}.${field.name}`;
return ({
...field,
name: fieldName,
defaultValue: fieldState?.[fieldName]?.value,
});
})}
/>
</AnimateHeight>
</div>
)}
</Draggable>
);
};
RepeaterSection.defaultProps = {
forceContentCollapse: false,
};
RepeaterSection.propTypes = {
addRow: PropTypes.func.isRequired,
removeRow: PropTypes.func.isRequired,
@@ -75,6 +102,7 @@ RepeaterSection.propTypes = {
parentName: PropTypes.string.isRequired,
fields: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
fieldState: PropTypes.shape({}).isRequired,
forceContentCollapse: PropTypes.bool,
};
export default RepeaterSection;

View File

@@ -6,10 +6,6 @@
position: relative;
border: $stroke-width solid lighten($gray, 20%);
+ .repeater-section {
margin-top: base(.5);
}
&__collapse__icon {
svg {
transition: 150ms linear;

View File

@@ -1,5 +1,6 @@
import React, { useContext, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import FormContext from '../../Form/Context';
import Section from '../../../layout/Section';
@@ -13,6 +14,7 @@ const Repeater = (props) => {
const [rowCount, setRowCount] = useState(0);
const formContext = useContext(FormContext);
const { fields: fieldState, dispatchFields } = formContext;
const [forceContentCollapse, setForceContentCollapse] = useState(false);
const {
label,
@@ -37,32 +39,75 @@ const Repeater = (props) => {
setRowCount(rowCount - 1);
};
const moveRow = (rowIndex, moveToIndex) => {
dispatchFields({
type: 'MOVE_ROW', rowIndex, moveToIndex, name,
});
};
useEffect(() => {
setRowCount(defaultValue.length);
}, [defaultValue]);
return (
<div className={baseClass}>
<Section
heading={label}
className="repeater"
>
{rowCount > 0 && Array.from(Array(rowCount).keys()).map((_, rowIndex) => {
return (
<RepeaterSection
key={rowIndex}
addRow={() => addRow(rowIndex)}
removeRow={() => removeRow(rowIndex)}
rowIndex={rowIndex}
fieldState={fieldState}
fields={fields}
parentName={name}
/>
);
})}
function onBeforeCapture(result) {
setForceContentCollapse(true);
}
</Section>
</div>
function onDragEnd(result) {
if (!result.destination) {
return;
}
const moveFromIndex = result.source.index;
const moveToIndex = result.destination.index;
dispatchFields({
type: 'MOVE_ROW', moveFromIndex, moveToIndex, name,
});
}
return (
<DragDropContext
onDragEnd={onDragEnd}
onBeforeCapture={onBeforeCapture}
>
<div className={baseClass}>
<Section
heading={label}
className="repeater"
rowCount={rowCount}
addInitialRow={() => addRow(0)}
>
{rowCount !== 0
&& (
<Droppable droppableId="repeater-drop">
{(provided) => {
return Array.from(Array(rowCount).keys()).map((_, rowIndex) => {
return (
<div
ref={provided.innerRef}
className={`${baseClass}__row`}
{...provided.droppableProps}
>
<RepeaterSection
key={rowIndex}
addRow={() => addRow(rowIndex)}
removeRow={() => removeRow(rowIndex)}
moveRow={() => moveRow(rowIndex)}
rowIndex={rowIndex}
fieldState={fieldState}
fields={fields}
parentName={name}
forceContentCollapse={forceContentCollapse}
/>
</div>
);
});
}}
</Droppable>
)}
</Section>
</div>
</DragDropContext>
);
};

View File

@@ -1,4 +1,11 @@
@import '../../../../scss/styles.scss';
.field-repeater {
&__row {
+ .field-repeater__row {
margin-top: base(.5);
}
}
.content {
flex-direction: column;
}

View File

@@ -1,14 +1,17 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import AnimateHeight from 'react-animate-height';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import './index.scss';
import IconButton from '../../controls/IconButton';
const baseClass = 'section';
const Section = (props) => {
const { className, heading, children } = props;
const Section = React.forwardRef((props, ref) => {
const {
className, heading, children, rowCount, addInitialRow,
} = props;
const classes = [
baseClass,
@@ -17,18 +20,21 @@ const Section = (props) => {
const [isSectionOpen, setIsSectionOpen] = useState(true);
const iconProps = {};
iconProps.iconName = `${rowCount === 0 ? 'crosshair' : 'arrow'}`;
iconProps.onClick = rowCount === 0 ? () => addInitialRow() : () => setIsSectionOpen(state => !state);
return (
<section className={classes}>
{heading
&& (
<header>
<header ref={ref}>
<h2 className={`${baseClass}__heading`}>{heading}</h2>
<div className={`${baseClass}__controls`}>
<IconButton
className={`${baseClass}__collapse__icon ${baseClass}__collapse__icon--${isSectionOpen ? 'open' : 'closed'}`}
iconName="arrow"
size="small"
onClick={() => setIsSectionOpen(state => !state)}
{...iconProps}
/>
</div>
</header>
@@ -40,14 +46,12 @@ const Section = (props) => {
height={isSectionOpen ? 'auto' : 0}
duration={150}
>
<div>
{children}
</div>
{children}
</AnimateHeight>
)}
</section>
);
};
});
Section.defaultProps = {
className: '',

View File

@@ -28,7 +28,9 @@ section.section {
}
&__content {
@include pad;
> div {
@include pad;
}
}
&__controls {