public/js/components/FormFields/FormFieldArrayWrapper.js (114 lines of code) (raw):
import React from 'react';
import {PropTypes} from 'prop-types';
import { errorPropType } from '../../constants/errorPropType';
export default class FormFieldArrayWrapper extends React.Component {
static propTypes = {
fieldLabel: PropTypes.string,
fieldName: PropTypes.string,
fieldValue: PropTypes.array,
fieldErrors: PropTypes.arrayOf(errorPropType),
onUpdateField: PropTypes.func,
nested: PropTypes.bool,
numbered: PropTypes.bool,
fieldClass: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element)
])
};
//We store new items in state to avoid sending invalid items back to the server before they're ready
state = {
newItems: [],
childrenVisible: true
};
onAddClick = () => {
this.setState({
newItems: this.state.newItems.concat([undefined])
});
};
renderValue(value, i) {
const updateFn = (newValue) => {
if (value === undefined) {
//It's the first update to a new item - add it to props and remove from newItems
this.setState({
newItems: this.state.newItems.length > 1 ? this.state.newItems.slice(1) : []
});
const update = this.props.fieldValue ? this.props.fieldValue.concat([newValue]) : [newValue];
this.props.onUpdateField(update);
} else {
//Find the value in the array to change
const newFieldValue = this.props.fieldValue.map((oldValue) => {
return value === oldValue ? newValue : oldValue;
});
this.props.onUpdateField(newFieldValue);
}
};
const removeFn = (removeIndex) => {
if (this.props.fieldValue && this.props.fieldValue.length > removeIndex) {
const newFieldValue = this.props.fieldValue.filter((value, currentIndex) => {
return currentIndex !== removeIndex;
});
this.props.onUpdateField(newFieldValue);
} else {
//It must be a new item
this.setState({
newItems: this.state.newItems.length > 1 ? this.state.newItems.slice(1) : []
});
}
};
const moveInArrayFn = (arr, fromIndex, toIndex) => {
arr.splice(toIndex, 0, arr.splice(fromIndex, 1)[0]);
};
const moveFn = (currentIndex, newIndex) => {
this.setState({
childrenVisible: false
}, () => {
/* we only allow to move if item has been updated and indices are within array bounds */
if (value !== undefined && this.props.fieldValue && this.props.fieldValue.length > currentIndex && currentIndex >= 0 && this.props.fieldValue.length > newIndex && newIndex >= 0) {
const arr = this.props.fieldValue.slice();
moveInArrayFn(arr, currentIndex, newIndex);
this.props.onUpdateField(arr);
}
// Atom Workshop used to use Scribe for its rich text editors, but we've now moved to ProseMirror. The app was built in
// the context described in the following paragraph. Our ProseMirror implementation should work without this workaround,
// but the logic doesn't degrade the experience in the application, so I'm leaving that logic undisturbed, along with
// this explanation:
//
// When reordering array elements & the child is a scribe editor, we have no way of passing information
// down to scribe to force a re-render, as scribe cannot handle update changes once it has been
// initialised. To trigger the visual change without a browser refresh, we need to unmount
// and remount the child scribe component. However as we only render the root React component
// with the DOM, and every other downstream component is rendered via React, there is no way to
// remove the children components. Therefore we use this flag in state to identify whether
// they should be visible or not. We hide the component until the updates have passed downstream,
// and then the further change in state below will trigger a re-render & the lifecycle methods of any children.
this.setState({
childrenVisible: true
});
});
};
const hydratedChildren = React.Children.map(this.props.children, (child) => {
return React.cloneElement(child, {
key: `${this.props.fieldName}-${i}`,
fieldName: `${this.props.fieldName}-${i}`,
fieldValue: value,
fieldErrors: this.props.fieldErrors,
formRowClass: 'form__row form__row--flex',
onUpdateField: updateFn
});
});
const renderMoveBtns = () => {
if (i !== 0 && (i+1) < this.props.fieldValue.length) {
return <div>
<button className="btn form__field-btn form__field--move-btn" type="button" onClick= { moveFn.bind(this, i, (i-1) ) } >Move up</button>
<button className="btn form__field-btn form__field--move-btn" type="button" onClick= { moveFn.bind(this, i, (i+1) ) } >Move down </button>
</div>;
} else if (i === 0) {
return <div><button className="btn form__field-btn form__field--move-btn" type="button" onClick= { moveFn.bind(this, i, (i+1) ) } >Move down </button></div>;
} else {
return <div><button className="btn form__field-btn form__field--move-btn" type="button" onClick= { moveFn.bind(this, i, (i-1) ) } >Move up</button></div>;
}
};
return (
<div className={this.props.fieldClass ? this.props.fieldClass : null}>
{this.props.numbered ? <span className="form__field-number">{`${i + 1}. `}</span> : false }
{hydratedChildren}
{renderMoveBtns()}
<button className="btn form__field-btn btn--red" type="button" onClick={removeFn.bind(this, i)}>Delete</button>
</div>
);
}
render () {
const values = (this.props.fieldValue || []).concat(this.state.newItems);
return (
<div className={this.props.nested ? 'form__row form__row--nested' : 'form__row'}>
<div className="form__btn-heading">
<span className="form__label">{this.props.fieldLabel}</span>
</div>
{ this.state.childrenVisible ? values.map((value, i) => this.renderValue(value, i)) : false }
<div className="form__btn-group">
<button className="form__btn-heading__btn form__btn-heading__add" type="button" onClick={this.onAddClick}>Add</button>
</div>
</div>
);
}
}