client/app/bundles/comments/components/CommentBox/CommentForm/CommentForm.jsx
/* eslint-disable react/no-find-dom-node, react/no-string-refs */
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import _ from 'lodash';
import { injectIntl } from 'react-intl';
import { defaultMessages } from 'libs/i18n/default';
import BaseComponent from 'libs/components/BaseComponent';
const emptyComment = { author: '', text: '' };
class CommentForm extends BaseComponent {
static propTypes = {
isSaving: PropTypes.bool.isRequired,
actions: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])).isRequired,
error: PropTypes.oneOfType([PropTypes.any]),
cssTransitionGroupClassNames: PropTypes.oneOfType([PropTypes.func, PropTypes.any]).isRequired,
// eslint-disable-next-line react/forbid-prop-types
intl: PropTypes.objectOf(PropTypes.any).isRequired,
};
constructor(props, context) {
super(props, context);
this.state = {
formMode: 0,
comment: emptyComment,
};
_.bindAll(this, ['handleSelect', 'handleChange', 'handleSubmit', 'resetAndFocus']);
}
handleSelect(selectedKey) {
this.setState({ formMode: selectedKey });
}
handleChange() {
let comment;
switch (this.state.formMode) {
case 0:
comment = {
author: ReactDOM.findDOMNode(this.refs.horizontalAuthorNode).value,
text: ReactDOM.findDOMNode(this.refs.horizontalTextNode).value,
};
break;
case 1:
comment = {
author: ReactDOM.findDOMNode(this.refs.stackedAuthorNode).value,
text: ReactDOM.findDOMNode(this.refs.stackedTextNode).value,
};
break;
case 2:
comment = {
// This is different because the input is a native HTML element
// rather than a React element.
author: ReactDOM.findDOMNode(this.refs.inlineAuthorNode).value,
text: ReactDOM.findDOMNode(this.refs.inlineTextNode).value,
};
break;
default:
throw new Error(`Unexpected state.formMode ${this.state.formMode}`);
}
this.setState({ comment });
}
handleSubmit(e) {
e.preventDefault();
const { actions } = this.props;
actions.submitComment(this.state.comment).then(this.resetAndFocus);
}
resetAndFocus() {
// Don't reset a form that didn't submit, this results in data loss
if (this.props.error) return;
const comment = { author: this.state.comment.author, text: '' };
this.setState({ comment });
let ref;
switch (this.state.formMode) {
case 0:
ref = ReactDOM.findDOMNode(this.refs.horizontalTextNode);
break;
case 1:
ref = ReactDOM.findDOMNode(this.refs.stackedTextNode);
break;
case 2:
ref = ReactDOM.findDOMNode(this.refs.inlineTextNode);
break;
default:
throw new Error(`Unexpected state.formMode ${this.state.formMode}`);
}
ref.focus();
}
formHorizontal() {
const { formatMessage } = this.props.intl;
return (
<div>
<hr />
<form className="form-horizontal flex flex-col gap-4" onSubmit={this.handleSubmit}>
<div className="flex flex-col gap-0 items-center lg:gap-4 lg:flex-row">
<label htmlFor="horizontalAuthorNode" className="w-full lg:w-2/12 lg:text-end shrink-0">
{formatMessage(defaultMessages.inputNameLabel)}
</label>
<input
type="text"
id="horizontalAuthorNode"
placeholder={formatMessage(defaultMessages.inputNamePlaceholder)}
className="px-3 py-1 leading-4 border border-gray-300 rounded w-full"
ref="horizontalAuthorNode"
value={this.state.comment.author}
onChange={this.handleChange}
disabled={this.props.isSaving}
/>
</div>
<div className="flex flex-col gap-0 items-center lg:gap-4 lg:flex-row">
<label htmlFor="horizontalTextNode" className="w-full lg:w-2/12 lg:text-end shrink-0">
{formatMessage(defaultMessages.inputTextLabel)}
</label>
<input
type="textarea"
id="horizontalTextNode"
placeholder={formatMessage(defaultMessages.inputTextPlaceholder)}
className="px-3 py-1 leading-4 border border-gray-300 rounded w-full"
ref="horizontalTextNode"
value={this.state.comment.text}
onChange={this.handleChange}
disabled={this.props.isSaving}
/>
</div>
<div className="flex flex-col gap-0 lg:gap-4 lg:flex-row">
<div className="hidden lg:block lg:w-2/12 grow-0" />
<button
type="submit"
className="self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800"
disabled={this.props.isSaving}
>
{this.props.isSaving
? `${formatMessage(defaultMessages.inputSaving)}...`
: formatMessage(defaultMessages.inputPost)}
</button>
</div>
</form>
</div>
);
}
formStacked() {
const { formatMessage } = this.props.intl;
return (
<div>
<hr />
<form className="flex flex-col gap-4" onSubmit={this.handleSubmit}>
<div className="flex flex-col gap-0">
<label htmlFor="stackedAuthorNode" className="w-full">
{formatMessage(defaultMessages.inputNameLabel)}
</label>
<input
type="text"
id="stackedAuthorNode"
placeholder={formatMessage(defaultMessages.inputNamePlaceholder)}
className="px-3 py-1 leading-4 border border-gray-300 rounded w-full"
ref="stackedAuthorNode"
value={this.state.comment.author}
onChange={this.handleChange}
disabled={this.props.isSaving}
/>
</div>
<div className="flex flex-col gap-0">
<label htmlFor="stackedTextNode" className="w-full">
{formatMessage(defaultMessages.inputTextLabel)}
</label>
<input
type="text"
id="stackedTextNode"
placeholder={formatMessage(defaultMessages.inputTextPlaceholder)}
className="px-3 py-1 leading-4 border border-gray-300 rounded w-full"
ref="stackedTextNode"
value={this.state.comment.text}
onChange={this.handleChange}
disabled={this.props.isSaving}
/>
</div>
<div className="flex flex-col gap-0">
<button
type="submit"
className="self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800"
disabled={this.props.isSaving}
>
{this.props.isSaving
? `${formatMessage(defaultMessages.inputSaving)}...`
: formatMessage(defaultMessages.inputPost)}
</button>
</div>
</form>
</div>
);
}
// Head up! We have some CSS modules going on here with the className props below.
formInline() {
const { formatMessage } = this.props.intl;
return (
<div>
<hr />
<form className="form-inline flex flex-col lg:flex-row flex-wrap gap-4" onSubmit={this.handleSubmit}>
<div className="flex gap-2 items-center">
<label htmlFor="inlineAuthorNode">{formatMessage(defaultMessages.inputNameLabel)}</label>
<input
type="text"
id="inlineAuthorNode"
placeholder={formatMessage(defaultMessages.inputNamePlaceholder)}
className="px-3 py-1 leading-4 border border-gray-300 rounded"
ref="inlineAuthorNode"
value={this.state.comment.author}
onChange={this.handleChange}
disabled={this.props.isSaving}
/>
</div>
<div className="flex gap-2 items-center">
<label htmlFor="inlineTextNode">{formatMessage(defaultMessages.inputTextLabel)}</label>
<input
type="textarea"
id="inlineTextNode"
placeholder={formatMessage(defaultMessages.inputTextPlaceholder)}
className="px-3 py-1 leading-4 border border-gray-300 rounded"
ref="inlineTextNode"
value={this.state.comment.text}
onChange={this.handleChange}
disabled={this.props.isSaving}
/>
</div>
<div className="flex gap-2">
<button
type="submit"
className="self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800"
disabled={this.props.isSaving}
>
{this.props.isSaving
? `${formatMessage(defaultMessages.inputSaving)}...`
: formatMessage(defaultMessages.inputPost)}
</button>
</div>
</form>
</div>
);
}
errorWarning() {
const { error, cssTransitionGroupClassNames } = this.props;
// If there is no error, there is nothing to add to the DOM
if (!error.error) return null;
const errorData = error.error.response && error.error.response.data;
const errorElements = _.transform(
errorData,
(result, errorText, errorFor) => {
result.push(
<CSSTransition
key={errorFor}
nodeRef={error.nodeRef}
timeout={500}
classNames={cssTransitionGroupClassNames}
>
<li ref={error.nodeRef}>
<b>{_.upperFirst(errorFor)}:</b> {errorText}
</li>
</CSSTransition>,
);
},
[],
);
return (
<div
className="bg-pink-100 p-4 mb-4 border border-pink-200 rounded text-red-800 prose-strong:text-red-800 prose-ul:my-1"
key="commentSubmissionError"
>
<strong>Your comment was not saved!</strong>
<ul>{errorElements}</ul>
</div>
);
}
render() {
let inputForm;
switch (this.state.formMode) {
case 0:
inputForm = this.formHorizontal();
break;
case 1:
inputForm = this.formStacked();
break;
case 2:
inputForm = this.formInline();
break;
default:
throw new Error(`Unknown form mode: ${this.state.formMode}.`);
}
const { formatMessage } = this.props.intl;
// For animation with TransitionGroup
// https://reactcommunity.org/react-transition-group/transition-group
// The 500 must correspond to the 0.5s in:
// client/app/bundles/comments/components/CommentBox/CommentBox.module.scss:6
return (
<div>
<TransitionGroup component={null}>{this.errorWarning()}</TransitionGroup>
<div className="flex gap-1 not-prose">
<button
type="button"
className={`px-6 py-2 font-semibold border-0 rounded ${
this.state.formMode === 0 ? 'text-sky-50 bg-sky-600' : 'text-sky-600 hover:bg-gray-100'
}`}
onClick={() => this.handleSelect(0)}
>
{formatMessage(defaultMessages.formHorizontal)}
</button>
<button
type="button"
className={`px-6 py-2 font-semibold border-0 rounded ${
this.state.formMode === 1 ? 'text-sky-50 bg-sky-600' : 'text-sky-600 hover:bg-gray-100'
}`}
onClick={() => this.handleSelect(1)}
>
{formatMessage(defaultMessages.formStacked)}
</button>
<button
type="button"
className={`px-6 py-2 font-semibold border-0 rounded ${
this.state.formMode === 2 ? 'text-sky-50 bg-sky-600' : 'text-sky-600 hover:bg-gray-100'
}`}
onClick={() => this.handleSelect(2)}
>
{formatMessage(defaultMessages.formInline)}
</button>
{}
</div>
{inputForm}
</div>
);
}
}
export default injectIntl(CommentForm);