Skip to content

Commit 1845646

Browse files
committed
feat: implement BaseField component and refactor field elements to use it
1 parent b1784d6 commit 1845646

File tree

6 files changed

+427
-607
lines changed

6 files changed

+427
-607
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { Button, Form, StatefulButton } from '@openedx/paragon';
2+
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import PropTypes from 'prop-types';
4+
5+
import SwitchContent from '../../../forms/elements/SwitchContent';
6+
import EmptyContent from '../../../forms/elements/EmptyContent';
7+
import EditableItemHeader from '../../../forms/elements/EditableItemHeader';
8+
9+
import messages from '../../messages';
10+
import { FORM_MODE } from '../../constants';
11+
12+
import useFieldController from '../useFieldController';
13+
14+
const BaseField = ({
15+
name: fieldName,
16+
value: fieldValue,
17+
placeholder: fieldPlaceholder,
18+
instructions: fieldInstructions,
19+
label: fieldLabel,
20+
required: isRequired,
21+
restrictions: fieldRestrictions,
22+
errorMessages,
23+
formEditMode,
24+
activeFieldName,
25+
setFormMode,
26+
handleFormSubmit,
27+
saveState,
28+
options: fieldOptions,
29+
renderEditingField,
30+
renderEditableField,
31+
renderEmptyField,
32+
}) => {
33+
const { formatMessage } = useIntl();
34+
35+
const {
36+
draftValue,
37+
setDraftValue,
38+
fieldError,
39+
getFieldDisplayMode,
40+
} = useFieldController({
41+
name: fieldName,
42+
value: fieldValue,
43+
errorMessages,
44+
formEditMode,
45+
activeFieldName,
46+
fieldRestrictions,
47+
});
48+
49+
const handleStartEditing = () => {
50+
setFormMode(FORM_MODE.EDITING, fieldName);
51+
};
52+
53+
const handleCancelEditing = () => {
54+
setFormMode(FORM_MODE.EDITABLE);
55+
setDraftValue(fieldValue);
56+
};
57+
58+
const onSubmit = async (event) => {
59+
event.preventDefault();
60+
handleFormSubmit(fieldName, draftValue);
61+
};
62+
63+
return (
64+
<SwitchContent
65+
expression={getFieldDisplayMode()}
66+
cases={{
67+
editing: (
68+
<div role="dialog" aria-labelledby={`${fieldName}-label`}>
69+
<form data-testid="field-form" onSubmit={onSubmit}>
70+
<Form.Group
71+
controlId={fieldName}
72+
isInvalid={fieldError}
73+
>
74+
75+
{renderEditingField({
76+
fieldName,
77+
fieldLabel,
78+
fieldError,
79+
draftValue,
80+
setDraftValue,
81+
fieldRestrictions,
82+
isRequired,
83+
fieldOptions,
84+
})}
85+
86+
</Form.Group>
87+
88+
<div className="form-group flex-shrink-0 flex-grow-1">
89+
<StatefulButton
90+
type="submit"
91+
state={saveState}
92+
labels={{
93+
default: formatMessage(messages['profile.formcontrols.button.save']),
94+
pending: formatMessage(messages['profile.formcontrols.button.saving']),
95+
complete: formatMessage(messages['profile.formcontrols.button.saved']),
96+
}}
97+
onClick={(e) => {
98+
if (saveState === 'pending') {
99+
e.preventDefault();
100+
}
101+
}}
102+
disabled={!!fieldError}
103+
/>
104+
<Button variant="link" onClick={handleCancelEditing}>
105+
{formatMessage(messages['profile.formcontrols.button.cancel'])}
106+
</Button>
107+
</div>
108+
</form>
109+
</div>
110+
),
111+
editable: renderEditableField({
112+
fieldLabel, fieldName, draftValue, handleStartEditing,
113+
}),
114+
empty: renderEmptyField?.({
115+
fieldName,
116+
fieldLabel,
117+
draftValue,
118+
fieldInstructions,
119+
fieldPlaceholder,
120+
handleStartEditing,
121+
}) || (
122+
<>
123+
<EditableItemHeader content={fieldLabel} />
124+
<EmptyContent onClick={handleStartEditing}>
125+
{fieldInstructions}
126+
</EmptyContent>
127+
<small className="form-text text-muted">
128+
{fieldPlaceholder}
129+
</small>
130+
</>
131+
),
132+
static: draftValue && (
133+
<>
134+
<EditableItemHeader content={fieldLabel} />
135+
<p data-hj-suppress className="h5">{draftValue}</p>
136+
</>
137+
),
138+
}}
139+
/>
140+
);
141+
};
142+
143+
BaseField.propTypes = {
144+
name: PropTypes.string.isRequired,
145+
value: PropTypes.string,
146+
placeholder: PropTypes.string,
147+
instructions: PropTypes.string,
148+
label: PropTypes.string,
149+
required: PropTypes.bool,
150+
restrictions: PropTypes.shape({
151+
max_length: PropTypes.number,
152+
min_length: PropTypes.number,
153+
}),
154+
errorMessages: PropTypes.shape({
155+
required: PropTypes.string,
156+
max_length: PropTypes.string,
157+
min_length: PropTypes.string,
158+
}),
159+
formEditMode: PropTypes.string,
160+
activeFieldName: PropTypes.string,
161+
setFormMode: PropTypes.func,
162+
handleFormSubmit: PropTypes.func,
163+
saveState: PropTypes.oneOf(['default', 'error', 'pending', 'complete']),
164+
renderEditingField: PropTypes.func.isRequired,
165+
renderEditableField: PropTypes.func.isRequired,
166+
renderEmptyField: PropTypes.func,
167+
options: PropTypes.arrayOf(PropTypes.shape({
168+
value: PropTypes.string.isRequired,
169+
label: PropTypes.string.isRequired,
170+
})),
171+
};
172+
173+
BaseField.defaultProps = {
174+
value: '',
175+
placeholder: '',
176+
instructions: '',
177+
label: '',
178+
restrictions: {},
179+
errorMessages: {},
180+
formEditMode: FORM_MODE.STATIC,
181+
activeFieldName: '',
182+
setFormMode: () => {},
183+
handleFormSubmit: () => {},
184+
saveState: 'default',
185+
renderEmptyField: null,
186+
};
187+
188+
export default BaseField;

0 commit comments

Comments
 (0)